一、vue3.0 响应式系统的实现原理
vue 2.x 响应式实现原理 利用 Object.defineProperty() 方法作为对象添加 get() 和 set() 方法来侦测对象的变化,缺陷如下:
- 性能较差
- 在对象上新增属性是无法被侦测的
- 改变数组的 length 属性是无法被侦测的
为此 vue 3.0 使用 ES6 的 Proxy 取代 Object.defineProperty()方法,性能更加优异,而且数组和对象一样,可以直接触发 get() 和 set() 方法。 Proxy 称为代理,是一种可以拦截并改变底层 JavaScript 引擎操作的包装器。
调用 new Proxy(target, handler) 可以为一个目标对象创建一个代理,代理可以拦截 JavaScript 引擎内部目标的底层对象操作,这些底层操作被拦截后会触发响应特定操作的陷阱 函数。在调用 Proxy 构造函数时,需要传入两个参数,target 为目标对象,handler 是一个包含陷阱函数的处理器对象。
简单介绍 proxy
js/** * 处理对象 * @type {{}} */ const baseHandler = { // 陷阱函数,读取属性值时触发 // 参数 target 是目标对象 // 参数property 是要获取的属性名 // 参数 receiver 是 Proxy 对象或继承 Proxy 的对象 get (target, property, receiver) { console.log('获取值') return target[property] }, // 陷阱函数,写入属性值时触发 // 参数 value 是新的属性值 set (target, property, value, receiver) { console.log('设置值') }, // 陷阱函数,删除属性触发 deleteProperty(target, property) { console.log('删除属性') } } // 目标对象 const target = { name: '张三' } // 为目标对象创建代理对象 const proxy = new Proxy(target, baseHandler); // 读取属性的值 console.log('proxy.name--->', proxy.name) // 设置属性值 proxy.name = '李四' // 删除属性 delete proxy.name
针对代理对象的相关操作就会触发处理器对象中的对应陷阱函数,在陷阱函数中就可以为目标对象的属性访问添加自定义的业务逻辑。
针对对象监听
js/** * 判断某个值是否是对象 * @param val * @return {boolean} */ const isObj = (val) => { return val !== null && typeof val === 'object'; } /** * 响应式核心方法 * @param target * @return {*} */ const reactive = (target) => { return createReactiveObject(target) } /** * 创建响应式对象的方法 * @param target */ const createReactiveObject = (target) => { // 如果 target 不是对象,则直接返回 if (!isObj(target)) { return target; } const baseHandler = { get (target, property, receiver) { console.log('获取值'); const result = Reflect.get(target, property, receiver); return result; }, set (target, property, value, receiver) { console.log('设置值'); const result = Reflect.set(target, property, value, receiver) return result }, deleteProperty(target, property) { console.log('属性删除'); return Reflect.deleteProperty(target, property); } } const observed = new Proxy(target, baseHandler); return observed; } const proxy = reactive({ name: '张三' }); proxy.name = '李四'; console.log('proxy.name--->', proxy.name)
针对对象的属性值是对象
js/** * 判断某个值是否是对象 * @param val * @return {boolean} */ const isObj = (val) => { return val !== null && typeof val === 'object'; } /** * 响应式核心方法 * @param target * @return {*} */ const reactive = (target) => { return createReactiveObject(target) } /** * 创建响应式对象的方法 * @param target */ const createReactiveObject = (target) => { // 如果 target 不是对象,则直接返回 if (!isObj(target)) { return target; } const baseHandler = { get (target, property, receiver) { console.log('获取值'); const result = Reflect.get(target, property, receiver); return isObj(result) ? reactive(result) : result; }, set (target, property, value, receiver) { console.log('设置值'); const result = Reflect.set(target, property, value, receiver) return result }, deleteProperty(target, property) { console.log('属性删除'); return Reflect.deleteProperty(target, property); } } const observed = new Proxy(target, baseHandler); return observed; } const proxy = reactive({ name: '张三', address: { city: '北京' }}); proxy.address.city = '上海';
访问 proxy.address 会引起 get 陷阱调用,由于 address 属性本身也是一个对象,因此为该属性也创建代码。
上述代码运行结果为:
获取值
设置值
考虑下面两种情况,第一种情况的代码如下:
javascriptconst target = { name: '张三' }; const proxy1 = reactive(target); const proxy2 = reactive(target);
上述代码对同一个目标对象进行了多次代理,如果每次返回一个不同的代理对象,是没有意义的,要解决这个问题,可以在为目标对象初次创建代理后,以目标对象为key,代理为 value, 保存到一个 Map 中,然后在每次创建代理前,对目标对象进行判断,如果已经存在代理对象,则直接返回代理对象,而不再新创建代理对象。
第二种情况的代码如下:
javascriptconst target = { name: '张三' }; const proxy1 = reactive(target); const proxy2 = reactive(proxy1);
上述代码对一个目标对象进行了代理,然后又对该代理对象进行了代理,这也是无意义的,也需要进行区分,解决方法与第一种情况类似,不过是以代理对象为 key,目标对象为 value, 保存到一个 Map 中,然后在每次创建代理前,判断传入的目标对象是否本身就是代理对象,如果是,则直接返回该目标对象(原目标对象的代理对象)。
定义两个 WeakMap 对象,分别保存目标对象到代理对象的映射,以及代理对象到目标对象的映射,并添加判断逻辑。修改后的代码如下:
js/** * 判断某个值是否是对象 * @param val * @return {boolean} */ const isObj = (val) => { return val !== null && typeof val === 'object'; } /** * 响应式核心方法 * @param target * @return {*} */ const reactive = (target) => { return createReactiveObject(target) } // 存放目标对象 => 代理对象 const toProxy = new WeakMap(); // 存放代理对象 => 目标对象 const toRow = new WeakMap(); /** * 创建响应式对象的方法 * @param target */ const createReactiveObject = (target) => { // 如果 target 不是对象,则直接返回 if (!isObj(target)) { return target; } const proxy = toProxy.get(target); // 如果目标对象已经有代理对象,则直接返回该代理对象 if (proxy) { return proxy; } // 如果目标对象是代理对象,并且有对应的真实对象,那么直接返回 if (toRow.has(target)) { // 这里的 target 是代理对象 return target; } const baseHandler = { get (target, property, receiver) { console.log('获取值'); const result = Reflect.get(target, property, receiver); return isObj(result) ? reactive(result) : result; }, set (target, property, value, receiver) { console.log('设置值'); const result = Reflect.set(target, property, value, receiver) return result }, deleteProperty(target, property) { console.log('属性删除'); return Reflect.deleteProperty(target, property); } } const observed = new Proxy(target, baseHandler); toProxy.set(target, observed); toRow.set(observed, target); return observed; } const proxy = reactive({ name: '张三', address: { city: '北京' }}); proxy.address.city = '上海';
向数组添加元素而导致 set 陷阱响应两次的问题。
javascriptconst proxy = reactive([1, 2, 3]); proxy.push(4)
上述代码会导致 set 陷阱触发两次,因为 push() 方法向数组中添加元素的同时还会修改数组的长度,因此有两次 set 陷阱的触发;一次是将数组索引为 3 的位置设置为 4 而触发; 一次是修改数组的 length 属性为 4 而触发。假如在 set 陷阱函数中更新视图,那么就会出现更新两次情况。
为了避免出现上述情况,需要在 set 陷阱函数中区分是新增属性还是修改属性,同时对属性值的修改做一个判断,如果要修改的属性的新值与旧值,则无须进行任何操作。
js/** * 判断当前对象是否有指定属性 * @param target * @param property */ const hasOwn = (target, property) => { return target.hasOwnProperty(property) } /** * 判断某个值是否是对象 * @param val * @return {boolean} */ const isObj = (val) => { return val !== null && typeof val === 'object'; } /** * 响应式核心方法 * @param target * @return {*} */ const reactive = (target) => { return createReactiveObject(target) } // 存放目标对象 => 代理对象 const toProxy = new WeakMap(); // 存放代理对象 => 目标对象 const toRow = new WeakMap(); /** * 创建响应式对象的方法 * @param target */ const createReactiveObject = (target) => { // 如果 target 不是对象,则直接返回 if (!isObj(target)) { return target; } const proxy = toProxy.get(target); // 如果目标对象已经有代理对象,则直接返回该代理对象 if (proxy) { return proxy; } // 如果目标对象是代理对象,并且有对应的真实对象,那么直接返回 if (toRow.has(target)) { // 这里的 target 是代理对象 return target; } const baseHandler = { get (target, property, receiver) { console.log('获取值'); const result = Reflect.get(target, property, receiver); return isObj(result) ? reactive(result) : result; }, set (target, property, value, receiver) { // 判断目标对象是否已经存在该属性 const hasProperty = hasOwn(target, property); const oldValue = Reflect.get(target, property); const result = Reflect.set(target, property, value, receiver); if (!hasProperty) { console.log('新增属性') } else if (oldValue !== value) { console.log('修改属性') } return result; }, deleteProperty(target, property) { console.log('属性删除'); return Reflect.deleteProperty(target, property); } } const observed = new Proxy(target, baseHandler); toProxy.set(target, observed); toRow.set(observed, target); return observed; } const proxy = reactive({ name: '张三', address: { city: '北京' }}); proxy.address.city = '上海';
针对上述使用 push() 方法向数组中添加元素的代码,修改后的响应式代码只会输出"新增属性",原因是添加元素后,数组长度已经是4,当 push() 方法修改数组长度为4时,新增和旧值 相同,因此不进行任何操作。完善后的响应式代码也避免了对属性值进行无意义的修改。
vue3.0 的依赖收集,所谓依赖,就是指当数据发生变化时,要通知谁。
javascript... // 保存 effect 的数组,以栈的形式存储 const effectStack = []; const effect = (fn) => { // 创建响应式 effect const effect = createReactiveEffect(fn); // 默认先执行一次 effect,本质上调用的是传入的 fn 函数 effect(); } // 创建响应式 effect const createReactiveEffect = (fn) => { // 响应式 effect const effect = () => { try { // 将 effect 保存到全局数组 effectStack 中,以栈的形式存储 effectStack.push(effect); return fn(); } finally { // 调用完依赖后,弹出 effect effectStack.pop(); } } return effect; } ... const proxy = reactive({ name: '张三' }); effect( () => { console.log(proxy.name); }); proxy.name = '李四';
上述代码的运行结果为:
text获取值 张三 修改属性
从输出结果中可以看出,除了默认执行一次的 effect 外,当 name属性发生变化时, effect 并没有被执行。为了在对象属性发生变化时,让 effect 再次执行,需要将对象的属性与 effect 进行关联,这可以采用 Map 来存储。考虑到一个属性可能会关联多个依赖,那么存储的映射关系应该是以对象属性为 key,保存所有 effect 的 Set 对象为 value,之所以 选择 Set 而不是数组,是因为 Set 中不能保存重复的元素。
另外,属性毕竟是对象的属性,不可能脱离对象而单独存在,要跟踪不同对象属性的依赖,还需要一个 WeakMap,将对象本身作为 key,保存所有属性与依赖映射关系的 Map 作为 value, 存储到这个 WeakMap 中。
定义好数据结构后,编写一个依赖收集函数 track()
javascript// 保存对象与其属性依赖关系的 Map, key是对象,value 是 Map const targetMap = new WeakMap(); // 跟踪收集 const track = (target, property) => { // 获取全局数组 effectStack 中的依赖 const effect = effectStack[effectStack.length - 1]; // 如果存在依赖 if (effect) { // 取出该对象对应的 Map let depsMap = targetMap.get(target); // 如果不存在,则以目标对象为 key,新建的 Map 为 value,保存到 targetMap 中 if (!depsMap) { targetMap.set(target, depsMap = new Map()); } // 从 Map 中取出该属性对应的所有 effect let deps = depsMap.get(property) // 如果不存在,则以属性为 key,新建的 Set 为 value,保存到 depsMap 中 if (!deps) { depsMap.set(property, deps = new Set()); } // 判断 Set 中是否已经存在 effect,如果没有,则添加到 deps 中 if (!deps.has(effect)) { deps.add(effect) } } }
当属性发生变化时,触发属性关联的所有 effect 执行,为此,在编写一个trigger()函数。
javascript// 执行属性关系的所有 effect const trigger = (target, type, property) => { const depsMap = targetMap.get(target); if (depsMap) { let deps = depsMap.get(property); // 将当前属性关系的所有 effect 依次执行 if (deps) { deps.forEach(effect => { effect(); }); } } }
依赖收集的函数和触发依赖的函数构建完成,需要在某个地方去收集依赖和触发依赖执行,收集依赖放到触发器对象中的 get 陷阱中,而触发依赖实在属性发生变化时执行依赖,自然是 放到 set 陷阱中。
修改 createReactiveObject() 函数,添加 track() 和 trigger() 函数的调用。
js/** * 判断当前对象是否有指定属性 * @param target * @param property */ const hasOwn = (target, property) => { return target.hasOwnProperty(property) } /** * 判断某个值是否是对象 * @param val * @return {boolean} */ const isObj = (val) => { return val !== null && typeof val === 'object'; } /** * 响应式核心方法 * @param target * @return {*} */ const reactive = (target) => { return createReactiveObject(target) } // 存放目标对象 => 代理对象 const toProxy = new WeakMap(); // 存放代理对象 => 目标对象 const toRow = new WeakMap(); // 保存对象与其属性依赖关系的 Map, key是对象,value 是 Map const targetMap = new WeakMap(); // 跟踪收集 const track = (target, property) => { // 获取全局数组 effectStack 中的依赖 const effect = effectStack[effectStack.length - 1]; // 如果存在依赖 if (effect) { // 取出该对象对应的 Map let depsMap = targetMap.get(target); // 如果不存在,则以目标对象为 key,新建的 Map 为 value,保存到 targetMap 中 if (!depsMap) { targetMap.set(target, depsMap = new Map()); } // 从 Map 中取出该属性对应的所有 effect let deps = depsMap.get(property) // 如果不存在,则以属性为 key,新建的 Set 为 value,保存到 depsMap 中 if (!deps) { depsMap.set(property, deps = new Set()); } // 判断 Set 中是否已经存在 effect,如果没有,则添加到 deps 中 if (!deps.has(effect)) { deps.add(effect) } } } // 执行属性关联的所有 effect const trigger = (target, type, property) => { const depsMap = targetMap.get(target); if (depsMap) { let deps = depsMap.get(property); // 将当前属性关系的所有 effect 依次执行 if (deps) { deps.forEach(effect => { effect(); }); } } } /** * 创建响应式对象的方法 * @param target */ const createReactiveObject = (target) => { // 如果 target 不是对象,则直接返回 if (!isObj(target)) { return target; } const proxy = toProxy.get(target); // 如果目标对象已经有代理对象,则直接返回该代理对象 if (proxy) { return proxy; } // 如果目标对象是代理对象,并且有对应的真实对象,那么直接返回 if (toRow.has(target)) { // 这里的 target 是代理对象 return target; } const baseHandler = { get (target, property, receiver) { console.log('获取值'); const result = Reflect.get(target, property, receiver); // 收集依赖 track(target, property); return isObj(result) ? reactive(result) : result; }, set (target, property, value, receiver) { // 判断目标对象是否已经存在该属性 const hasProperty = hasOwn(target, property); const oldValue = Reflect.get(target, property); const result = Reflect.set(target, property, value, receiver); if (!hasProperty) { console.log('新增属性') trigger(target, 'add', property) } else if (oldValue !== value) { trigger(target, 'set', property) console.log('修改属性') } return result; }, deleteProperty(target, property) { console.log('属性删除'); return Reflect.deleteProperty(target, property); } } const observed = new Proxy(target, baseHandler); toProxy.set(target, observed); toRow.set(observed, target); return observed; } const proxy = reactive({ name: '张三', address: { city: '北京' }}); proxy.address.city = '上海';