赞
踩
在讲解 Vue3 的响应式原理时,我们需要了解一些知识,Proxy 和 Reflect,如果已经知道的可以自行跳过
Proxy 是一个类,所以在使用时需要通过 new 关键字才创建实例对象使用
Proxy 接收两个参数:
捕获器有很多,常用的还是我们的 get
和 set
,其余的大家有兴趣可以自行了解
get 接受三个参数:
当然,这个形参可以自己随意更改名称,我们看一下具体的使用,如下:
const objProxy = new Proxy(obj, {
// 获取属性值时的捕获器
get(target, key) {
// - target:获取被Proxy代理的对象
// - key:获取代理对象当前被使用的key值
return target[key]
}
})
receiver 在后续用到我们在具体讲解
set 接收四个参数:
具体使用如下:
const objProxy = new Proxy(obj, {
// 设置属性值时的捕获器
set(target, key, newVlue) {
// - target:获取被Proxy代理的对象
// - key:获取代理对象当前被使用的key值
// - newVlue:修改的新值
target[key] = newVlue
}
})
receiver 还是一样,在后续用到我们在具体讲解
对象
,字面意思是反射
操作JavaScript对象的方法
,有点像Object中操作对象的方法
对 对象本身 的操作如何设计会更加规范
,所以将这些API放到了Object上面
Object作为一个构造函数
,这些操作实际上放到它身上不合适
类似于 in、delete操作符
,让 JS 看起来有一些奇怪新增了Reflect
,让我们这些操作符都集中到了 Reflect 对象上那我们先看一下原来 Proxy 中的 get 捕获器是怎么实现的,如下:
const objProxy = new Proxy(obj, {
get(target, key) {
// 直接通过 target[key] 返回,还是在源对象 obj 身上查找 key,然后在返回对应的值
return target[key]
}
})
我们通过 Reflect 的话怎么在 Proxy 中的 get 捕获器返回值呢,如下:
const objProxy = new Proxy(obj, {
get(target, key, receiver) {
// 通过前面介绍 Reflect 的方法,我们也知道它的方法于 Proxy 的捕获器一一对应的,那么就可以知道
// Reflect.get 同样具备 get 捕获器的三个参数,就可以通过 Reflect.get(target, key) 达到 target[key] 的效果
return Reflect.get(target, key)
}
})
那么这样的区别是什么,避免了直接在语言内部去操作原对象
,我们通过告诉 Reflect.get 方法对象是哪个,需要获取的 key 是哪个,从而返回一个对应的值,因为我们一开始使用代理对象的原因就是为了避免直接对原对象进行一个直接操作,注意这里是直接操作而非不操作形成独立的个体
那么 set 捕获器就可以改造成这样,如下:
const objProxy = new Proxy(obj, {
set(target, key, newValue) {
Reflect.set(target, key, newValue)
}
})
当然,这里的 Reflect.set 方法有一个区别,他会返回一个布尔值
我们先来看一段示例代码,如下:
// 在对象内部是可以定义 get 和 set 方法的
const obj = {
_name: 'zs',
get name() {
return this._name
},
set name(newVal) {
this._name = newVal
}
}
// 可以看到,我们没有修改直接修改 _name,也可以正常操作_name
obj.name = 'ls'
console.log(obj.name) // ls
console.log(obj) // { _name: 'ls', name: [Getter/Setter] }
那么现在我们改写成 Proxy 和 Reflect,代码如下:
// 在对象内部是可以定义 get 和 set 方法的 const obj = { _name: 'zs', get name() { return this._name }, set name(newVal) { this._name = newVal } } const objProxy = new Proxy(obj, { get(target, key) { console.log('get被触发', key) // get被触发 name return Reflect.get(target, key) }, set(target, key, val) { console.log('set被触发', key) Reflect.set(target, key, val) } }) objProxy.name = 'ls' console.log(objProxy.name)
我们通过实例看到,只有 objProxy.name 访问的时候触发了一次,我们只拦截到了访问 name,但是正在 obj 对象里面的 get 通过 this 访问 _name 的时候,并没触发我们的 get 捕获器
,那我们做的一些拦截操作就没有意义了,因为这时候它的 this 还是指向 obj 这个对象的
那我们有没有一种方式可以让他的 this 指向是指向我们的代理对象呢?receiver
就可以帮助我们实现这一点,receiver 是什么我们通过打印看一下,如图:
通过这个图我们可以看见 receiver 是我们代理的对象,而 target 是原始对象,还记得我们的 Reflect 与 Proxy 的捕获器是同步的吗,那么 Reflect 也具备 receiver 方法,因此我们写出如下代码:
// 在对象内部是可以定义 get 和 set 方法的 const obj = { _name: 'zs', get name() { return this._name }, set name(newVal) { this._name = newVal } } const objProxy = new Proxy(obj, { get(target, key, receiver) { console.log('get被触发', key, receiver) // get被触发 name return Reflect.get(target, key, receiver) }, set(target, key, val) { console.log('set被触发', key) Reflect.set(target, key, val) } }) objProxy.name = 'ls' console.log(objProxy.name)
现在我们再看一下打印结果,如图:
这次访问 _name 就被拦截到了,证明改变 this 指向成功
在函数中,可能有些函数需要响应式,有些函数不需要,因此为了方便复用和管理,我们会进行一个函数封装
因此,我们会封装一个可以让函数实现响应式的函数
,如下:
const reactiveFns = []
function observe(fn) {
reactiveFns.push(fn)
}
这个函数实现了什么,当传入一个需要实现响应式的函数,我们就把这个函数传入 reactiveFns 数组,收集起来,最后一旦检测到更改就把这个数组里面的函数遍历并执行
到了这一步,相信响应式的基础模型就搭建好了,我们只需要在这个基础之上完善即可,那么现在开始优化第一步,将数组、添加、执行放入一个类中,便于管理和使用
class Depned { constructor() { this.reactiveFns = [] } // 收集依赖 addDepend(fn) { this.reactiveFns.push(fn) } // 派发更新 notify() { this.reactiveFns.forEach(fn => { fn() }) } } const depend = new Depned() // 监听需要实现响应式的函数 function observe(fn) { depend.addDepend(fn) }
那么封装了好了依赖类后,那么我们现在还需要完善什么呢?是不是这个手动执行对我们不太友好呢,这时候就可以使用 Proxy 的 get 和 set 捕获器来自动监听对象变化,并配合 Reflect 使用,如下:
const obj = { name: 'zs', age: 18 } class Depend { constructor() { this.reactiveFns = [] } addDepend(fn) { this.reactiveFns.push(fn) } notify() { this.reactiveFns.forEach(fn => { fn() }) } } const depend = new Depend () function observe(fn) { depend.addDepend(fn) } const objProxy = new Proxy(obj, { get(target, key, receiver) { return Reflect.get(target, key, receiver) }, set(target, key, newValue, receiver) { Reflect.set(target, key, newValue, receiver) // 检测到重新设置的时候执行 notify 更新 depend.notify() } }) observe(function foo() { console.log(`${objProxy.name}的朋友是ww`, 'foo函数内部--1') console.log(`我是${objProxy.name}`, 'foo函数内部--2') }) observe(function Age() { console.log(`${objProxy.age}岁了`, 'Age函数内部') }) function bar() { console.log('普通函数 bar') } objProxy.name = 'ls' // 无需在此处属性更改后的手动派发更新
我们来看一下输出结果,如图:
输出好像正常了,没有什么问题,确实是更改 name 属性就会重新执行依赖函数,但是你有没有发现函数 Age 也执行了,按照我们起初的设想,应该是 name 改动就只执行依赖 name 属性的函数啊,这是为什么呢,因为我们在添加监听依赖的时候,是直接把所有的函数放入了执行数组,所以不管任何值有变动,都会重新执行全部的依赖函数,那这就不符合我们的需求了,并且以后我们肯定不止一个 obj 对象啊,还有其他的 obj1、obj2…等等
针对上面的问题,我们现在来做一个解决,我们先来说一下实现的效果,我们是应该给每一个对象都为一个单独的模块,其次这个对象的每一个属性又作为一个单独的区域,如果这个对象的某一个属性变化,我们就从这个对象模块中寻找对应的属性区域,来执行这个属性区域内对应的依赖函数
好了,现在我们知道需要的实现的效果,我们应该怎么实现呢?我们可以利用 WeakMap 和 Map 的数据结构来实现,我们来看一下下面的关系图:
那么上图如果不够清晰的话,我们这里在使用伪代码讲述,如下:
// 1、首先来看一下我们的 obj1 对象 const obj1 = { name: 'zs', age: 18 } // 2、其次我们将 obj1 中的 属性 作为 Map 对象的键值,那么有了键,值是什么,值就是依赖与此属性的那些代码(即我们设置好用于收集的 Depend 类) // - 由此我们可与将第二点转为如下伪代码 const obj1Map = new Map() obj1Map.set('name', '依赖name属性的Depend') obj1Map.set('age', '依赖age属性的Depend') // 3、我们根据 obj1 对象的转化方法,将 obj2 也可以存入 Map 中 const obj2 = { address: '长沙' } const obj2Map = new Map() obj2Map.set('address', '依赖address属性的Depend') // 当然还可以有更多的对象...这里不在赘述了 // 4、当我们经过上述的转化之后,就可以将每一个对象都分别保存在每一个 Map 对象中,我们就可以将这些 Map 对象都存入 WeakMap 对象里面 const wm = new WeakMap() // 5、我们这里可以将 obj1 这个对象名称作为键,objMap 作为值 wm.set(obj1, obj1Map) wm.set(obj2, obj2Map) // 6、当我们得出最后的 WeakMap 数据结构后,如果检测到某个对象的属性改变之后,就可以通过逐层获取获取到属于这个属性依赖的Depend,并将其遍历取出并执行 // 7、通过键名 【obj1】:wm.get(obj1) ===> 可以取出在 WeakMap 中的值(即 obj1Map) // 8、通过键名 【name】:obj1Map.get(name) ===> 可以取出在 Map 中的值(即 name 属性的 Depend) // 9、最后执行属性的 Depend 即可实现我们的需求
我们前面讲了收集到依赖之后应该做什么,并如何执行,那我们现在就需要来实现如何才能将这个依赖收集起来呢?
我们之前写的 observe 函数收集依赖是不管三七二十一就放入 depend 中,显然这种收集方式无法实现我们的构想,那么我们就可以做出改变
触发代理中的 get 捕获器
属性所属于那个对象
,属性名称是什么
一个值存入 Map 对象
,也就正确的收到了依赖那么我们是不是可以创建一个函数来帮我们完成这些重复的步骤呢?如下:
// 获取 depend 函数 function getDepedn(target, key) { // 1、首先在 WeakMap 中获取 Map let map = wm.get(target) // 2、若 Map 不存在 if (!map) { // 3、 如果不存在则创建一个 Map map = new Map() // 4、 将新建的 Map 存入 WeakMap 中 wm.set(target, map) } // 5、上面保证了 map 是一定存在的,那我们就需要从 Map 中获取 depend let depend = map.get(key) // 6、若 depend 不存在 if (!depend) { // 7、创建 Depend depend = new Depend() // 8、将新建的 Depend 存入 map map.set(key, depend) } // 9、返回最后的 depend return depend }
通过上面这个函数我们可以正确的创建 depend,如果存在也可以帮我们获取 depend,现在我们有了这个辅助函数之后,我们还在回到之前的 observe 函数,我们可以对他改造一下,在接受到函数的时候,自动执行一次,用于触发 get 捕获器,如下:
function observe(fn) {
fn()
}
调用的时候就会触发 get 捕获器,同时在此刻调用 getDepend 函数来帮我们获取正确的 Depend 依赖,如下:
const objProxy = new Proxy(obj, {
get(target, key, receiver) {
// 获取对应的 depend
const depend = getDepedn(target, key)
// 给 depend 添加对应的函数,怎么获取呢?
depend.addDepend()
return Reflect.get(target, key, receiver)
},
set(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
}
})
可以看到我在上面留下了一个疑问,是啊,我们现在可以获取到 depend 了,那将那个函数存入里面呢?我在 get 捕获器里面无法拿到这个函数啊,注意,重点来了,我们不这里不能获取,但是在 observe 函数中是不是可以获取,那么我们就可以定义一个全局变量
,然后在 observe 中把函数赋值给这个全局变量
,然后在执行传入的函数,执行时触发 get 捕获器,那么此时就可以通过全局变量获取对应的函数了,具体实现如下:
// 定义一个全局变量,接收函数 let globalFn = null function observe(fn) { globalFn = fn fn() globalFn = null } const objProxy = new Proxy(obj, { get(target, key, receiver) { // 获取对应的 depend const depend = getDepedn(target, key) // 给 depend 添加对应的函数 depend.addDepend(globalFn) return Reflect.get(target, key, receiver) }, set(target, key, newValue, receiver) { Reflect.set(target, key, newValue, receiver) } })
然后我们在补上重新设置时会触发 set 捕获器,此时就可以执行 depend 中的派发更新方法了,如下:
const objProxy = new Proxy(obj, { get(target, key, receiver) { // 获取对应的 depend const depend = getDepedn(target, key) // 给 depend 添加对应的函数 depend.addDepend(globalFn) return Reflect.get(target, key, receiver) }, set(target, key, newValue, receiver) { Reflect.set(target, key, newValue, receiver) // 获取对应的 depend 执行派发更新 const depend = getDepedn(target, key) console.log(depend.reactiveFns, '-----') depend.notify() } })
以上我们就基本实现了完整的响应式,我们现在可以来看一下目前位置完整的代码,如下:
const obj = { name: 'zs', age: 18 } class Depend { constructor() { this.reactiveFns = [] } addDepend(fn) { this.reactiveFns.push(fn) } notify() { this.reactiveFns.forEach(fn => { fn() }) } } const depend = new Depend() // 定义一个全局变量,接收函数 let globalFn = null function observe(fn) { globalFn = fn fn() globalFn = null } // 创建存储 Map 的 WeakMap const wm = new WeakMap() /** * 封装获取 depend 的函数 * @param {Object} target 原对象名称 * @param {String} key 对象的属性名 * @returns depend */ function getDepedn(target, key) { // 1、首先在 WeakMap 中获取 Map let map = wm.get(target) // 2、若 Map 不存在 if (!map) { // 3、 如果不存在则创建一个 Map map = new Map() // 4、 将新建的 Map 存入 WeakMap 中 wm.set(target, map) } // 5、上面保证了 map 是一定存在的,那我们就需要从 Map 中获取 depend let depend = map.get(key) // 6、若 depend 不存在 if (!depend) { // 7、创建 Depend depend = new Depend() // 8、将新建的 Depend 存入 map map.set(key, depend) } // 9、返回最后的 depend return depend } const objProxy = new Proxy(obj, { get(target, key, receiver) { const depend = getDepedn(target, key) depend.addDepend(globalFn) return Reflect.get(target, key, receiver) }, set(target, key, newValue, receiver) { Reflect.set(target, key, newValue, receiver) const depend = getDepedn(target, key) depend.notify() } }) observe(function foo() { console.log(`${objProxy.name}的朋友是ww`, 'foo函数内部--1') console.log(`我是${objProxy.name}`, 'foo函数内部--2') }) observe(function Age() { console.log(`${objProxy.age}岁了`, 'Age函数内部') }) objProxy.name = 'ls' objProxy.age = 101
我们也可以做一些小优化,比如 depend.addDepend(globalFn) 这一步放入 depend 方法内部,如下:
let globalFn = null class Depend { constructor() { this.reactiveFns = [] } addDepend(fn) { this.reactiveFns.push(fn) } notify() { this.reactiveFns.forEach(fn => { fn() }) } depend() { if (globalFn) { this.addDepend(globalFn) } } } const objProxy = new Proxy(obj, { get(target, key, receiver) { const depend = getDepedn(target, key) depend.depend() // 直接执行即可,无须关心传入的参数 return Reflect.get(target, key, receiver) }, set(target, key, newValue, receiver) { Reflect.set(target, key, newValue, receiver) const depend = getDepedn(target, key) depend.notify() } })
上面我们已经实现了响应式的完整流程,但是还存在一些不和谐的地方,比如我们一共函数中存在多次调用同一个属性的情况,那么在收集依赖的时候就会多次收集这同一个函数,会导致在执行时也会执行多次这个函数,但是实际上我们是不是只需要执行一次
当然解决这个问题也很简单,因为数组可以重复存储一个元素,但是 Set 就会,所以我们将普通的数组改为 Set 即可,或者对数组进行去重的操作,如下:
class Depend { constructor() { this.reactiveFns = new Set() } addDepend(fn) { this.reactiveFns.add(fn) } notify() { this.reactiveFns.forEach(fn => { console.log(fn) fn() }) } depend() { if (globalFn) { this.addDepend(globalFn) } } }
现在我们代理的对象是我们手动设置的 obj 对象,但是实际中我们不可能只有这一个对象,所以我们也需要来动态的监听这些对象,将它们通过代理的方式实现响应式
因此我们可以将对象通过代理变成响应式这一步进行一个封装,如下:
// 只需要传入一个对象,就可以返回一个经过响应式处理的对象 function reactiveObj(obj) { return new Proxy(obj, { get(target, key, receiver) { const depend = getDepedn(target, key) depend.depend() return Reflect.get(target, key, receiver) }, set(target, key, newValue, receiver) { Reflect.set(target, key, newValue, receiver) const depend = getDepedn(target, key) depend.notify() } }) } const obj = { name: 'zs', age: 18 } const objProxy = new reactiveObj(obj) const info = { address: '长沙' } const infoProxy = new reactiveObj(info)
好了我们现再来看一下完整的代码,如下:
// 定义一个全局变量,接收函数 let globalFn = null // 依赖类 class Depend { constructor() { this.reactiveFns = new Set() } addDepend(fn) { this.reactiveFns.add(fn) } notify() { this.reactiveFns.forEach(fn => { console.log(fn) fn() }) } depend() { if (globalFn) { this.addDepend(globalFn) } } } // 监听函数 function observe(fn) { globalFn = fn fn() globalFn = null } // 创建存储 Map 的 WeakMap const wm = new WeakMap() function getDepedn(target, key) { // 1、首先在 WeakMap 中获取 Map let map = wm.get(target) // 2、若 Map 不存在 if (!map) { // 3、 如果不存在则创建一个 Map map = new Map() // 4、 将新建的 Map 存入 WeakMap 中 wm.set(target, map) } // 5、上面保证了 map 是一定存在的,那我们就需要从 Map 中获取 depend let depend = map.get(key) // 6、若 depend 不存在 if (!depend) { // 7、创建 Depend depend = new Depend() // 8、将新建的 Depend 存入 map map.set(key, depend) } // 9、返回最后的 depend return depend } function reactiveObj(obj) { return new Proxy(obj, { get(target, key, receiver) { const depend = getDepedn(target, key) depend.depend() return Reflect.get(target, key, receiver) }, set(target, key, newValue, receiver) { Reflect.set(target, key, newValue, receiver) const depend = getDepedn(target, key) depend.notify() } }) } const obj = { name: 'zs', age: 18 } const objProxy = new reactiveObj(obj) observe(function foo() { console.log(`${objProxy.name}的朋友是ww`, 'foo函数内部--1') console.log(`我是${objProxy.name}`, 'foo函数内部--2') }) observe(function Age() { console.log(`${objProxy.age}岁了`, 'Age函数内部') }) objProxy.name = 'ls' objProxy.age = 101 const info = { address: '长沙' } const infoProxy = new reactiveObj(info) observe(function address() { console.log('依赖于info.address的函数被执行', infoProxy.address) }) infoProxy.address = '杭州'
其实两者之间的区别很小,Vue3 使用了 Proxy,Vue2 使用了 Object.defineProperty,代码如下:
function reactiveObj(obj) { // 遍历对象的 key 值 Object.keys(obj).forEach(key => { // 存储当前 key 的值 let temp = obj[key] Object.defineProperty(obj, key, { get() { const depend = getDepedn(obj, key) depend.depend() return temp }, set(val) { temp = val const depend = getDepedn(obj, key) depend.notify() } }) }) // 最后返回 obj return obj }
当然这里大家可能对 Object.defineProperty 这个方法不太了解,叫做属性描述符,后面我会开一篇新文章单独讲解 Vue2 的实现,当然只是简单的实现,因为其他步骤与 Vue3 差不多
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。