一篇搞懂Vue2、Vue3响应式源码的原理
既白biu 人气:0前言
我们在编写Vue2,Vue3代码的时候,经常会在data中定义某些数据,然后在template用到的时候,可能会在多处用到这些数据,通过对这些数据的操作,可以达到改变视图的作用,即所谓数据驱动视图。
我们可以通过Mustache 语法,让data可以在页面上显示,随着data的变化,视图中也会随之改变。
那么,这种响应式操作在Vue2、Vue3中是怎么实现的呢?
Vue2响应式操作
响应式函数的封装
在进行响应式操作前,我们需要简单大致封装一个响应式函数,参数接收的是函数,凡是传入到响应式函数的函数,就是需要响应式的,其他默认定义的函数是不需要响应式的。
我们需要用一个数组将他们收集起来,(现在暂时使用函数,最好的办法是放入Set中,下文会讲),代码如下:
// 封装一个响应式的函数 let reactiveFns = [] function watchFn(fn) { reactiveFns.push(fn) }
等到我们需要执行这些函数的时候(什么时候需要执行是后话,先简单提一下),可以遍历这个数组然后执行:
reactiveFns.forEach(fn => {<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E--> fn() })
Depend类的封装
我们需要封装一个Depend类,这个类的作用是:这个类用于管理某一个对象的某一个属性的所有响应式函数。一个对象里面可能会有多个属性并且有他们对应的值,我们可能用到了这个对象里面多个属性,所以我们要给这里的每个用到的属性建立一个属于自己的类,用来管理对这个属性有依赖的所有函数。
所以我们得想办法拿到刚才在响应式函数里面传进去的函数,这里我们可以用activeReactiveFn暂时保存刚才传进去的函数。
所以我们对响应式函数的封装进行重构一下,如下:
// 保存当前需要收集的响应式函数 let activeReactiveFn = null // 封装一个响应式的函数 function watchFn(fn) { activeReactiveFn = fn fn() activeReactiveFn = null }
因为某个属性可能会用多个函数进行依赖,所有在这个类的内部我们会定义一个Set, reactiveFns = new Set(),定义成Set而不是数组是因为Set数据结构没有重复的数据,从而防止了重复的操作。
这里定义了一个depend方法可以将activeReactiveFn在有值的情况下,放入reactiveFns中,notify函数就是将这些收集了的函数进行执行。
class Depend { constructor() { this.reactiveFns = new Set() } depend() { if (activeReactiveFn) { this.reactiveFns.add(activeReactiveFn) } } notify() { this.reactiveFns.forEach(fn => { fn() }) } }
监听对象的变化
在Vue2中使用的监听对象的变化使用的方法是:使用Object.defineProperty。
我们可以封装一个reactive函数,参数传入一个对象,函数内部对这个对象进行监听,遍历这个对象,获取所有的属性和属性值,对每个属性使用Object.defineProperty,在Object.defineProperty第三个参数中,get和set方法中,在set方法中,修改值为新的值之后,之前提到,每个属性都要有属于自己的Depend对象,那么如何获取这个对象呢?
那这里还有个问题,有不同的对象,对象里面又有多个属性,那么这该如何解决呢?
可以定义一个WeakMap将各个对象保存成Map形式,然后在每个单一对象里面,我们可以用Map形式保存属性的Depend类,如图所示:
那么如何根据对象名,属性名获取depend呢?可以在getDepend函数实现,参数传入对象名,属性名
,代码如下:
// 封装一个获取depend函数 const targetMap = new WeakMap() function getDepend(target, key) { // 根据target对象获取map的过程 let map = targetMap.get(target) if (!map) { map = new Map() targetMap.set(target, map) } // 根据key获取depend对象 let depend = map.get(key) if (!depend) { depend = new Depend() map.set(key, depend) } return depend }
获取到属性特定的depend后,回到原来的话题,那么在set方法中,修改值为新的值之后,获取到属性特定的depend后,要调用depend里面的notify方法,使对这个属性有依赖的所有函数执行,也就是对数据进行更新。
在get方法中,在返回属性值之前,要先获取到属性特定的depend后,调用depend里面的depend方法,将对此属性依赖的函数保存下来。
代码如下:
function reactive(obj) { Object.keys(obj).forEach(key => { let value = obj[key] Object.defineProperty(obj, key, { get: function() { const depend = getDepend(obj, key) depend.depend() return value }, set: function(newValue) { value = newValue const depend = getDepend(obj, key) depend.notify() } }) }) return obj }
至此,Vue2的响应式操作就已经实现了
所有代码以及测试代码如下:
// 保存当前需要收集的响应式函数 let activeReactiveFn = null class Depend { constructor() { this.reactiveFns = new Set() } depend() { if (activeReactiveFn) { this.reactiveFns.add(activeReactiveFn) } } notify() { this.reactiveFns.forEach(fn => { fn() }) } } // 封装一个响应式的函数 function watchFn(fn) { activeReactiveFn = fn fn() activeReactiveFn = null } // 封装一个获取depend函数 const targetMap = new WeakMap() function getDepend(target, key) { // 根据target对象获取map的过程 let map = targetMap.get(target) if (!map) { map = new Map() targetMap.set(target, map) } // 根据key获取depend对象 let depend = map.get(key) if (!depend) { depend = new Depend() map.set(key, depend) } return depend } function reactive(obj) { Object.keys(obj).forEach(key => { let value = obj[key] Object.defineProperty(obj, key, { get: function() { const depend = getDepend(obj, key) depend.depend() return value }, set: function(newValue) { value = newValue const depend = getDepend(obj, key) depend.notify() } }) }) return obj } // 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2) const objProxy = reactive({ name: "cy", // depend对象 age: 18 // depend对象 }) const infoProxy = reactive({ address: "安徽省", height: 1.88 }) watchFn(() => { console.log(infoProxy.address) }) infoProxy.address = "北京市" const foo = reactive({ name: "foo" }) watchFn(() => { console.log(foo.name) }) foo.name = "aaa" foo.name = "bbb" // 安徽省 // 北京市 // foo // aaa // bbb
Vue3响应式操作
Proxy、Reflect
Proxy:
在Vue2中,使用Object.defineProperty来监听对象的变化,但是这样做有什么缺点呢?
首先,Object.defineProperty设计的初衷,不是为了去监听截止一个对象中所有的属性的。
我们在定义某些属性的时候,初衷其实是定义普通的属性,但是后面我们强行将它变成了数据属性描述符。
其次,如果我们想监听更加丰富的操作,比如新增属性、删除属性,那么Object.defineProperty是无能为力的。
所以我们要知道,存储数据描述符设计的初衷并不是为了去监听一个完整的对象
在ES6中,新增了一个Proxy类,这个类从名字就可以看出来,是用于帮助我们创建一个代理的:
也就是说,如果我们希望监听一个对象的相关操作,那么我们可以先创建一个代理对象(Proxy对象);
之后对该对象的所有操作,都通过代理对象来完成,代理对象可以监听我们想要对原对象进行哪些操作;
如果我们想要侦听某些具体的操作,那么就可以在handler中添加对应的捕捉器(Trap):
set函数有四个参数:
target:目标对象(侦听的对象);
property:将被设置的属性key;
value:新属性值;
receiver:调用的代理对象;
get函数有三个参数:
target:目标对象(侦听的对象);
property:被获取的属性key;
receiver:调用的代理对象
实例代码如下;
const obj = { name: "cy", age: 18 } const objProxy = new Proxy(obj, { // 获取值时的捕获器 get: function(target, key) { console.log(`监听到对象的${key}属性被访问了`, target) return target[key] }, // 设置值时的捕获器 set: function(target, key, newValue) { console.log(`监听到对象的${key}属性被设置值`, target) target[key] = newValue } }) console.log(objProxy.name) console.log(objProxy.age) objProxy.name = "kobe" objProxy.age = 30 console.log(obj.name) console.log(obj.age) // 监听到对象的name属性被访问了 { name: 'cy', age: 18 } // cy // 监听到对象的age属性被访问了 { name: 'cy', age: 18 } // 18 // 监听到对象的name属性被设置值 { name: 'cy', age: 18 } // 监听到对象的age属性被设置值 { name: 'kobe', age: 18 } // kobe // 30
Reflect:
Reflect也是ES6新增的一个API,它是一个对象,字面的意思是反射。
那么这个Reflect有什么用呢?
它主要提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法;
比如Reflect.getPrototypeOf(target)类似于 Object.getPrototypeOf();
比如Reflect.defineProperty(target, propertyKey, attributes)类似于Object.defineProperty() ;
如果我们有Object可以做这些操作,那么为什么还需要有Reflect这样的新增对象呢?
这是因为在早期的ECMA规范中没有考虑到这种对 对象本身 的操作如何设计会更加规范,所以将这些API放到了Object上面;
但是Object作为一个构造函数,这些操作实际上放到它身上并不合适;
另外还包含一些类似于 in、delete操作符,让JS看起来是会有一些奇怪的;
所以在ES6中新增了Reflect,让我们这些操作都集中到了Reflect对象上;
Reflect中常见的方法:
那么我们可以将之前Proxy案例中对原对象的操作,都修改为Reflect来操作;
我们发现在使用getter、setter的时候有一个receiver的参数,它的作用是什么呢?
如果我们的源对象(obj)有setter、getter的访问器属性,那么可以通过receiver来改变里面的this
Vue3响应式
Vue3响应式使用的是Proxy,我们需要在Vue2的reactive函数里面进行一些改变:
function reactive(obj) { return new Proxy(obj, { get: function(target, key, receiver) { // 根据target.key获取对应的depend const depend = getDepend(target, key) // 给depend对象中添加响应函数 depend.depend() return Reflect.get(target, key, receiver) }, set: function(target, key, newValue, receiver) { Reflect.set(target, key, newValue, receiver) // depend.notify() const depend = getDepend(target, key) depend.notify() } }) }
其他方面的代码同Vue2基本没啥变化,Vue3的响应式操作就已经实现了
所有代码以及测试代码如下:
// 保存当前需要收集的响应式函数 let activeReactiveFn = null class Depend { constructor() { this.reactiveFns = new Set() } depend() { if (activeReactiveFn) { this.reactiveFns.add(activeReactiveFn) } } notify() { this.reactiveFns.forEach(fn => { fn() }) } } // 封装一个响应式的函数 function watchFn(fn) { activeReactiveFn = fn fn() activeReactiveFn = null } // 封装一个获取depend函数 const targetMap = new WeakMap() function getDepend(target, key) { // 根据target对象获取map的过程 let map = targetMap.get(target) if (!map) { map = new Map() targetMap.set(target, map) } // 根据key获取depend对象 let depend = map.get(key) if (!depend) { depend = new Depend() map.set(key, depend) } return depend } function reactive(obj) { return new Proxy(obj, { get: function(target, key, receiver) { // 根据target.key获取对应的depend const depend = getDepend(target, key) // 给depend对象中添加响应函数 depend.depend() return Reflect.get(target, key, receiver) }, set: function(target, key, newValue, receiver) { Reflect.set(target, key, newValue, receiver) // depend.notify() const depend = getDepend(target, key) depend.notify() } }) } // 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2) const objProxy = reactive({ name: "cy", // depend对象 age: 18 // depend对象 }) const infoProxy = reactive({ address: "安徽省", height: 1.88 }) watchFn(() => { console.log(infoProxy.address) }) infoProxy.address = "北京市" const foo = reactive({ name: "foo" }) watchFn(() => { console.log(foo.name) }) foo.name = "bar" // 安徽省 // 北京市 // foo // bar
加载全部内容