我们想要对一个对象数据进行处理,从而实现更改dom。但如何更改对一个对象数据进行更改呢?
vue2 的双向数据绑定是利⽤ES5 的⼀个 API ,Object.defineProperty()对数据进⾏劫持 结合 发布订阅模式的⽅式来实现的。
vue3 中使⽤了 ES6 的 ProxyAPI 对数据代理,通过 reactive() 函数给每⼀个对象都包⼀层 Proxy,通过 Proxy 监听属性的变化,从⽽ 实现对数据的监控。
这⾥是相⽐于vue2版本,使⽤proxy的优势如下:
我们想要知道如何实现Vue3响应式数据,就要知道proxy这个概念。
Proxy(代理)是一种计算机网络技术,其作用是充当客户端和服务器之间的中间人,转发网络请求和响应。当客户端发送请求时,代理服务器会接收并转发请求到目标服务器,然后将服务器返回的响应转发给客户端。
相当于明星和经纪人,想要找明星办事,需要找他的经纪人,明星的事都交给经纪人做。明星就是源对象,经纪人就相当于proxy。
proxy用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
// 定义一个源对象
let obj = {
name: 'qx',
age: 24
}
// 实现一个Proxy,传入要代理的对象和get和set方法
const proxy = new Proxy(obj, {
// get中返回代理对象的,target代表源对象(也是上面的obj),key代表obj中每个属性
get(target, key) {
return target[key];
},
// set中返回代理对象的,target代表源对象(也是上面的obj),key代表obj中每个属性,value是修改的新值
set(target, key, value) {
target[key] = value
return true
}
})
console.log(proxy)
obj.name = 'xqx'
// 现在打印的是修改后的proxy,看看会变成什么样? 已经修改好了
console.log(proxy)
reactive 用于创建一个响应式对象,该对象可以包含多个属性和嵌套属性。当使用 reactive 创建响应式对象时,返回的对象是一个代理对象,该对象具有与原始对象相同的属性,并且任何对代理对象属性的更改都将触发组件的重新渲染。
既然我们已经知道reactive是个函数,并且返回的是一个代理对象,先把最基本的框架搭出来
function reactive(data) {
return new Proxy(data, {
get(target, key) {
return target[key];
},
set(target, key, value) {
target[key] = value
return true
}
})
}
看似已经完成了,但是当传入非对象时,却报错
提示这个对象是对象类型的,例如数组之类的,并只是{}这个。
const arr = true;
console.log(reactive(arr))
function reactive(data) {
//判断是不是对象,null也是object要排除
if(typeof data === Object && data !== null) return
return new Proxy(data, {
get(target, key) {
return target[key];
},
set(target, key, value) {
target[key] = value
return true
}
})
}
我们知道处理数据就是为了让视图更新,但一个系统离不开副作用函数。
副作用函数,顾名思义,会产生副作用的函数被称为副作用函数。通俗来说,就是这个函数可以影响其他的变量。
来看最基本的副作用函数
<div id="app"></div>
<script>
let obj = {
name: 'qx'
}
function effect(){
app.innerText = obj.name
}
effect()
</script>
现在我们需要通过前面reactive函数来完善一个基本的响应式系统
<body>
<div id="app"></div>
<script>
let obj = {
name: 'qx',
age: 24
}
function reactive(data) {
if(typeof data === Object && data !== null) return
return new Proxy(data, {
get(target, key) {
return target[key];
},
set(target, key, value) {
target[key] = value
return true
}
})
}
const state = reactive({name:'xqx'});
function effect(){
app.innerText = state.name
}
effect()
</script>
</body>
如果多个副作用函数同时引用一个变量,我们需要当变量改变时,每一个副作用函数都要执行。
可以把多个副作用函数放在一个列表里,在每次对对象操作时,执行proxy中的set方法时,对每一个副作用函数进行遍历。
<body>
<div id="app"></div>
<script>
let obj = {name: 'qx'}
let effectBucket = [];
function reactive(data) {
if(typeof data === Object && data !== null) return
return new Proxy(data, {
get(target, key) {
return target[key];
},
set(target, key, value) {
target[key] = value
effectBucket.forEach(fn=>fn())
return true
}
})
}
const state = reactive({name:'xqx'});
function effect(){
app.innerText = state.name
console.log('副作用函数1被执行')
}
effectBucket.push(effect)
function effect1(){
app.innerText = state.name
console.log('副作用函数2被执行')
}
effectBucket.push(effect1)
state.name = 'zs'
</script>
</body>
但是我们要是传两个同样的副作用函数怎么办。
function effect(){
app.innerText = state.name
console.log('副作用函数1被执行')
}
effectBucket.push(effect)
effectBucket.push(effect)
发现列表里有两个重复的effect函数,如果列表很长,foreach也会浪费时间,那么大大浪费性能。es6有个Set数据结构可以帮助我们解决这个问题。
let effectBucket = new Set();
const state = reactive({name:'xqx'});
function effect(){
app.innerText = state.name
console.log('副作用函数1被执行')
}
effectBucket.add(effect) //添加两次
effectBucket.add(effect)
function effect1(){
app.innerText = state.name
console.log('副作用函数2被执行')
}
effectBucket.add(effect1)
console.log(effectBucket)
前面我们只是对一个对象中的属性进行处理,如果多个属性都要更改呢?我们以上的操作会让每一个副作用函数都执行。
假设我们有这样一个结构
let obj = {name: 'qx',age:24}
我想改name属性时,只更新有name的副作用函数,不必把列表里所有副作用函数都更新。这就需要依赖收集。
对每一个副作用函数进行一个保存,当调用副作用函数时,会执行proxy中的get方法,在get方法把当前副作用函数添加列表,就实现了当前依赖属性和副作用函数关联在一起。
具体实现步骤如下:
let obj = {name: 'qx',age:24}
let effectBucket = new Set();
let activeEffect = null; //1.保存当前的副作用函数状态
function reactive(data) {
if(typeof data === Object && data !== null) return
return new Proxy(data, {
get(target, key) {
if(activeEffect != null){ //4. 将当前保存的副作用函数添加到副作用函数列表中
effectBucket.add(activeEffect)
}
return target[key];
},
set(target, key, value) {
target[key] = value
effectBucket.forEach(fn=>fn())
return true
}
})
}
const state = reactive(obj);
function effectName(){
console.log('副作用函数1被执行',state.name)
}
activeEffect = effectName() // 2.将当前副作用函数赋值给activeEffect
effectName() // 3.调用副作用函数,相当于访问proxy的get方法
activeEffect = null; // 5.将副作用函数状态置空,给下一个副作用函数用
function effectAge(){
console.log('副作用函数2被执行',state.age)
}
activeEffect = effectAge()
effectAge()
activeEffect = null;
state.name = 'zs'
再简化一下,对上面重复代码进行一个封装。调用的时候直接调封装后的方法
function registEffect(fn) {
if (typeof fn !== 'function') return;
activeEffect = fn();
fn();
activeEffect = null;
}
Set结构像数组,只是能做到去重,并不能实现不同属性对应不同集合。需要我们改进成一个属性对应多个集合。
另一个数据结构Map出现在面前,它是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
创建一个这样的结构。
let a = {
name: Set(fn,fn),
age:Set(fn,fn)
}
let effectBucket = new Map(); //{name:Set(fn,fn),age:Set(fn,fn)}
let activeEffect = null;
function reactive(data) {
if (typeof data === Object && data !== null) return
return new Proxy(data, {
get(target, key) {
if (activeEffect !== null) {
let deptSet;
if(!effectBucket.get(key)){ //没有得到key,说明没有添加过
deptSet = new Set(); //重新创建一个集合
effectBucket.set(key,deptSet); //每次添加一个属性{name:Set(fn,fn)}结构
}
deptSet.add(activeEffect)
}
return target[key];
},
set(target, key, value) {
target[key] = value
//从副作用桶中依次取出每一个副作用函数执行
let deptSet = effectBucket.get(key);
if(deptSet){
deptSet.forEach(fn => fn())
}
return true
}
})
}
继续封装收集依赖
get
function track(target, key) {
if (!activeEffect) return
let deptSet;
if (!effectBucket.get(key)) { //没有得到key,说明没有添加过
deptSet = new Set(); //重新创建一个集合
effectBucket.set(key, deptSet);
}
deptSet.add(activeEffect)
}
set
function trigger(target, key) {
let deptSet = effectBucket.get(key);
if (deptSet) {
deptSet.forEach((fn) => fn())
}
}
function track(target, key) {
if (!activeEffect) return
let deptMap =effectBucket.get(key);
if (!deptMap) { //没有得到key,说明没有添加过
deptMap = new Map(); //重新创建一个集合
effectBucket.set(target, deptMap);
}
let depSet = deptMap.get(key)
if(!depSet){
depSet = new Set()
deptMap.set(key,depSet)
}
deptSet.add(activeEffect)
}
function trigger(target, key) {
let depMap = effectBucket.get(target)
if(!depMap) return
let deptSet = effectBucket.get(key);
if (deptSet) {
deptSet.forEach((fn) => fn())
}
}
vue2 的双向数据绑定是利⽤ES5 的⼀个 API ,Object.defineProperty()对数据进⾏劫持 结合 发布订阅模式的⽅式来实现的。
vue3 中使⽤了 ES6 的 ProxyAPI 对数据代理,通过 reactive() 函数给每⼀个对象都包⼀层 Proxy,通过 Proxy 监听属性的变化,从⽽ 实现对数据的监控。
这⾥是相⽐于vue2版本,使⽤proxy的优势如下:
defineProperty只能监听某个属性,不能对全对象监听 可以省去for in、闭包等内容来提升效率(直接绑定整个对象即可)
监听数组,不⽤再去单独的对数组做特异性操作,通过Proxy可以直接拦截所有对象类型数据的操作,完美⽀持对数组的监听。
获取props
vue2在script代码块可以直接获取props,vue3通过setup指令传递
API不同
Vue2使⽤的是选项类型API(Options API),Vue3使⽤的是合成型API(Composition API)
建立数据data
vue2是把数据放入data中,vue3就需要使用一个新的setup()方法,此方法在组件初始化构造得时候触发。
生命周期不同
vue2 | vue3 |
---|---|
beforeCreate | setup() 开始创建组件之前,创建的是data和method |
created | setup() |
beforeMount | onBeforeMount 组件挂载到节点上之前执行的函数 |
mounted | onMounted 组件挂载完成后执行的函数 |
beforeUpdate | onBeforeUpdate 组件更新之前执行的函数 |
updated | onUpdated 组件更新完成之后执行的函数 |
beforeDestroy | onBeforeUnmount 组件挂载到节点上之前执行的函数 |
destroyed | onUnmounted 组件卸载之前执行的函数 |
activated | onActivated 组件卸载完成后执行的函数 |
deactivated | onDeactivated |
关于v-if和v-for的优先级:
vue2 在一个元素上同时使用 v-if 和 v-for v-for会优先执行
vue3 v-if 总会优先于 v-for生效
vue2和vue3的diff算法
vue2
vue2 diff算法就是进行虚拟节点对比,并返回一个patch对象,用来存储两个节点 不同的地方,最后用patch记录的消息去局部更新Dom。
vue2 diff算法会比较每一个vnode,而对于一些不参与更新的元素,进行比较是有 点消耗性能的。
vue3
vue3 diff算法在初始化的时候会给每个虚拟节点添加一个patchFlags,patchFlags 就是优化的标识。
只会比较patchFlags发生变化的vnode,进行更新视图,对于没有变化的元素做静 态标记,在渲染的时候直接复用。
Vue 数据双向绑定主要是指:数据变化更新视图
Vue 主要通过以下 4 个步骤来实现数据双向绑定的:
第一步:需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。
第二步:compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。
第三步:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是:
第四步:MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。
Set
Set是一种叫做集合的数据结构,是由一堆无序的、相关联的,且不重复的内存结构组成的组合。集合是以[值,值]的形式存储元素
WeakSet
Map
Map是一种叫做字典的数据结构,每个元素有一个称作key 的域,不同元素的key 各不相同。字典是以[键,值]的形式存储。
WeakMap
对象
为键名(null 除外),不接受其他类型的值作为键名;Vue检测数据的变动是通过Object.defineProperty实现的,所以无法监听数组的添加操作是可以理解的,因为是在构造函数中就已经为所有属性做了这个检测绑定操作。
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function defineGet() {
console.log(`get key: ${key} value: ${value}`)
return value
},
set: function defineSet(newVal) {
console.log(`set key: ${key} value: ${newVal}`)
value = newVal
}
})
}
function observe(data) {
Object.keys(data).forEach(function(key) {
console.log(data, key, data[key])
defineReactive(data, key, data[key])
})
}
let arr = [1, 2, 3]
observe(arr)
原来的Object.defineProperty发现通过索引是可以赋值的,并且也触发了set方法,但是Vue为什么不行呢?
对于对象而言,每一次的数据变更都会对对象的属性进行一次枚举,一般对象本身的属性数量有限,所以对于遍历枚举等方式产生的性能损耗可以忽略不计,但是对于数组而言呢?数组包含的元素量是可能达到成千上万,假设对于每一次数组元素的更新都触发了枚举/遍历,其带来的性能损耗将与获得的用户体验不成正比,故vue无法检测数组的变动。
//这是个深度的修改,某些情况下可能导致你不希望的结果,因此最好还是慎用
this.dataArr = this.originArr
this.$set(this.dataArr, 0, {data: '修改第一个元素'})
console.log(this.dataArr)
console.log(this.originArr) //同样的 源数组也会被修改 在某些情况下会导致你不希望的结果
//因为splice会被监听有响应式,而splice又可以做到增删改。
let tempArr = [...this.targetArr]
tempArr[0] = {data: 'test'}
this.targetArr = tempArr
this.$watch('blog', this.getCatalog, {
deep: true
// immediate: true // 是否第一次触发
});
watch: {
'obj.name'(curVal, oldVal) {
// TODO
}
}