Vue reactive函数实现流程详解
volit_ 人气:01.Reflect
Proxy有着可以拦截对对象各种操作的能力,比如最基本的get和set操作,而Reflect也有与这些操作同名的方法,像Reflect.set()、Reflect.get(),这些方法和它们所对应的对象基本操作完全一致。
const data = { value: '1', get fn() { console.log(this.value); return this.value; } }; data.value; // 1 Reflect.get(data,'value'); // 1
除此之外,Reflect除了和基本对象操作等价外,它还具有第三个参数receiver
,即指定该基础操作的this对象。
Reflect.get(data,'value',{<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->value: '2'}); // 会输出2
对于Proxy,它只能够拦截对象的基本操作,而对于data.fn(),这是一个复合操作,它由一个get操作和一个apply操作组成,即先通过get获取fn的值,然后调用即apply对应的函数。而现在,用我们之前创建的响应式系统来执行一次这个复合操作,我们期望的结果是,在对fn属性绑定的同时,对value的值也进行绑定,因为在fn函数的执行过程中,操作了value值。可实际情况是,value的值并没有进行绑定。
effect(() => { obj.fn(); // 假设obj是一个已经做了响应式代理的Proxy对象 }) obj.value = '2'; // 改变obj.value的值,预想中的响应式操作没有执行
这里就涉及到fn()函数中,this指向的问题了。实际上,在fn函数中,this指向的是原来的data对象,即this.value实际上是data.value,因为操作的是原对象,因此并不会触依赖收集。了解到问题的原因之后,我们就可以用上之前所说的Reflect的特性了,将get操作实际的this对象指定为obj,这样就可以顺利的实现我们我期望的功能了。
const obj = new Proxy(data, { get(target, key, receiver) { // get 接收第三个参数,即操作的调用者,对应obj.fn()就是obj了 track(target, key); return Reflect.get(target, key, receiver); // 将原来直接返回target[key]的操作改为Reflect.get } }
2.Proxy的工作原理
在js中一个对象必须部署包括[[GET]]、[[SET]]在内的11个内部方法,除此之外,函数拥有额外的[[Call]]和[[Construct]]两个方法。而在创建Proxy对象时,指定的拦截函数,实际上就是用来自定义代理对象本身的内部方法和行为,而不是指定。
3.代理Object
(1)代理读取操作
对一个普通对象的所有可能的读取操作:
- 访问属性:obj.foo
- 判断对象或原型上是否存在给定的key;key in obkj
- 使用for … in循环遍历对象
首先对于基本的访问属性,我们可以使用get方法拦截。
const obj = new Proxy(data, { get(target, key, receiver) { // get 接收第三个参数,即操作的调用者,对应obj.fn()就是obj了 track(target, key); return Reflect.get(target, key, receiver); // 将原来直接返回target[key]的操作改为Reflect.get } }
然后,对于in操作符,我们使用has方法进行拦截。
has(target, key) { track(target, key); return Reflect.has(target,key); }
最后,对于for … in操作,我们使用ownKeys方法进行拦截。这里使用和唯一标识ITERATE_KEY和副作用函数绑定,因为对于ownKeys操作来说,无论如何它都是对一个对象上所存在的所有属性进行遍历,并不会产生实际的属性读取操作,因此我们需要用一个唯一的标识来标记ownKeys操作。
ownKeys(target, key) { // 这里将副作用函数和唯一标识ITERATE_KEY绑定了 track(target, ITERATE_KEY); return Reflect.ownKeys(target); },
相应的,在进行赋值操作的时候,也需要相应的对ITERATE_KEY这个标识进行处理
function trigger(target, key) { const depsMap = bucket.get(target); if (!depsMap) return; const effects = depsMap.get(key); const iterateEffects = depsMap.get(ITERATE_KEY); // 读取ITERATE_KEY const effectToRun = new Set(); effects && effects.forEach((fn) => { if (fn !== activeEffect) { effectToRun.add(fn); } }); // 将与 ITERATE_KEY 相关联的副作用函数也添加到 effectsToRun iterateEffects && iterateEffects.forEach((fn) => { if (fn !== activeEffect) { effectToRun.add(fn); } }); effectToRun.forEach((fn) => { if (fn.options.scheduler) { fn.options.scheduler(fn); } else { fn(); } }); }
虽然以上的代码解决了添加属性的问题,但是随之而来的是修改属性的问题。对于for … in循环来说,无论原对象的属性如何修改,对它来说只需要进行一次遍历就好了,因此我们需要区分添加和修改的操作。这里使用Object.prototype.hasOwnProperty检查当前操作的属性是否已经存在于目标对象上,如果是,则说明当前的操作类型是’SET‘,否则说明是’ADD‘。然后将type作为第三个参数,传入trigger函数中。
set(target, key, newVal, receiver) { const type = Object.prototype.hasOwnProperty.call(target, key) ? "SET" : "ADD"; Reflect.set(target, key, newVal, receiver); trigger(target, key, type); },
(2)代理delete操作符
代理delete操作符使用的是deleteProperty方法,因为delete操作符删除属性会导致属性的数量变少,因此当操作类型为DELETE时也要触发一下for … in循环的操作。
deleteProperty(target, key) { // 检查删除的key是否为自身属性 const hadKey = Object.prototype.hasOwnProperty.call(target, key); const res = Reflect.deleteProperty(target, key); if (res && hadKey) { trigger(target, key, "DELETE"); } return res; }, // 当type为ADD或DELETE的时候,才执行ITERATE_KEY相关的操作 if (type === "ADD" || type === "DELETE") { iterateEffects && iterateEffects.forEach((fn) => { if (fn !== activeEffect) { effectToRun.add(fn); } }); }
4.合理的触发响应
(1)完善响应操作
触发修改操作时,若新值和旧值相等,则不需要触发修改响应操作。
set(target, key, newVal, receiver) { const oldVal = target[key]; // 获取旧值 const type = Object.prototype.hasOwnProperty.call(target, key) ? "SET" : "ADD"; const res = Reflect.set(target, key, newVal, receiver); if (oldVal !== newVal) { // 比较新值和旧值 trigger(target, key, type); } return res; },
但是全等有一个特殊情况,就是NaN === NaN的值为false,因此我们需要对NaN进行一个特殊判断。
(2)封装一个reactive函数
其实就是对new Proxy进行了一个简单的封装。
function reactive(obj) { return new Proxy(obj, { get(target, key, receiver) { track(target, key); return Reflect.get(target, key, receiver); }, set(target, key, newVal, receiver) { const oldVal = target[key]; // 获取旧值 const type = Object.prototype.hasOwnProperty.call(target, key) ? "SET" : "ADD"; const res = Reflect.set(target, key, newVal, receiver); if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) { // 比较新值和旧值 trigger(target, key, type); } return res; }, has(target, key) { track(target, key); return Reflect.has(target, key); }, ownKeys(target, key) { // 这里将副作用函数和唯一标识ITERATE_KEY绑定了 track(target, ITERATE_KEY); return Reflect.ownKeys(target); }, deleteProperty(target, key) { // 检查删除的key是否为自身属性 const hadKey = Object.prototype.hasOwnProperty.call(target, key); const res = Reflect.deleteProperty(target, key); if (res && hadKey) { trigger(target, key, "DELETE"); } return res; }, }); }
现在,我们使用reactive创建两个响应式对象,child和parent,然后将child原型设置为parent。然后为child.bar函数绑定副作用函数。当修改child.bar的值的时候,可以看到,副作用函数实际执行了两次。这是因为,child的原型是parent,child本身并没有bar这个属性,所以根据原型链的规则,最终会在parent身上拿到bar这个属性。因为在进行原型链查找的过程中,访问到了parent上的属性,因袭进行了一次额外的绑定操作,所以最终副作用函数执行了两次。
const obj = {}; const proto = { bar: 1, }; const child = reactive(obj); const parent = reactive(proto); Object.setPrototypeOf(child, parent); effect(() => { console.log(child.bar); }); child.bar = 2; // 输出 1 2 2
这里我们比较一下child和parent的拦截函数,可以发现receiver的值都是相同的,发生变化的是target的值,因此我们可以通过比较taregt的值来取消parent触发的那一次响应操作。
// child 的拦截函数 get(target, key, receiver) { // target是原始对象obj // receiver 是child } // parent 的拦截函数 get(target, key, receiver) { // target是proto对象 // receiver 是child }
这里我们通过添加一个raw操作来实现,当访问raw属性的时候,会返回该对象的target值。
function reactive(obj) { return new Proxy(obj, { get(target, key, receiver) { if (key === "raw") { // 添加一个新值 raw return target; } track(target, key); return Reflect.get(target, key, receiver); }, set(target, key, newVal, receiver) { const oldVal = target[key]; // 获取旧值 const type = Object.prototype.hasOwnProperty.call(target, key) ? "SET" : "ADD"; const res = Reflect.set(target, key, newVal, receiver); if (target === receiver.raw) { // 比较target值,如果receiver的target和当前target相同,说明就不是原型链操作。 if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) { // 比较新值和旧值 trigger(target, key, type); } } return res; } } }
5.深响应和浅响应
实际上,前面我们实现的reactive还只是浅层响应,也就是说只有对象的第一层具有响应式反应。比如对于一个obj:{bar{val:1}}对象,当对obj.bar.val进行操作的时候,我们首先从obj中拿到bar,但是这时候的bar只是一个普通对象bar:{val:1},因此无法进行响应式操作。这里我们对Reflect.get获取的值进行一个判断,如果拿到的值是一个对象,递归调用reactive函数,最后拿到一个深层响应的对象。
function reactive(obj) { return new Proxy(obj, { get(target, key, receiver) { if (key === "raw") { return target; } track(target, key); const res = Reflect.get(target, key, receiver); if(typeof res === 'object') { return reactive(res); } return res; } } }
但是我们并非所有时候都期望深层响应,因此我们调整一下reactive函数。
function createReactive(obj, isShallow = false) { return new Proxy(obj, { get(target, key, receiver) { if (key === "raw") { return target; } track(target, key); const res = Reflect.get(target, key, receiver); if (isShallow) return res; // 如果浅层响应,直接返回 if (typeof res === "object") { return reactive(res); } return res; }, set(target, key, newVal, receiver) { const oldVal = target[key]; // 获取旧值 const type = Object.prototype.hasOwnProperty.call(target, key) ? "SET" : "ADD"; const res = Reflect.set(target, key, newVal, receiver); if (target === receiver.raw) { if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) { // 比较新值和旧值 trigger(target, key, type); } } return res; }, has(target, key) { track(target, key); return Reflect.has(target, key); }, ownKeys(target, key) { // 这里将副作用函数和唯一标识ITERATE_KEY绑定了 track(target, ITERATE_KEY); return Reflect.ownKeys(target); }, deleteProperty(target, key) { // 检查删除的key是否为自身属性 const hadKey = Object.prototype.hasOwnProperty.call(target, key); const res = Reflect.deleteProperty(target, key); if (res && hadKey) { trigger(target, key, "DELETE"); } return res; }, }); } function reactive(obj) { return createReactive(obj, true); } function shallowReactive(obj) { return createReactive(obj, false); }
6.只读和浅只读
实现只读其实只需要在createReactiv函数中添上第三个参数isReadOnly。
function createReactive(obj, isShallow = false, isReadOnly = false) { return new Proxy(obj, { set(target, key, newVal, receiver) { if (isReadOnly) { console.warn(`属性${key}是只读的`); return true; } const oldVal = target[key]; // 获取旧值 const type = Object.prototype.hasOwnProperty.call(target, key) ? "SET" : "ADD"; const res = Reflect.set(target, key, newVal, receiver); if (target === receiver.raw) { if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) { // 比较新值和旧值 trigger(target, key, type); } } return res; }, deleteProperty(target, key) { if (isReadOnly) { console.warn(`属性${key}是只读的`); return true; } // 检查删除的key是否为自身属性 const hadKey = Object.prototype.hasOwnProperty.call(target, key); const res = Reflect.deleteProperty(target, key); if (res && hadKey) { trigger(target, key, "DELETE"); } return res; }, } }
当然,对于设置了只读属性的对象的属性,很明显就没必要添加依赖了,所以对于get也要进行相应的修改.
function createReactive(obj, isShallow = false, isReadOnly = false) { return new Proxy(obj, { get(target, key, receiver) { if (key === "raw") { // 通过获取raw属性,拿到初始对象 return target; } if (!isReadOnly) { // 只读情况下不需要建立联系 track(target, key); } const res = Reflect.get(target, key, receiver); if (isShallow) return res; // 如果浅层响应,直接返回 if (typeof res === "object") { // 如果获取的值是对象,递归调用reactive函数,得到深层响应对象 return reactive(res); } return res; }, } }
但是,上述操作只能做到浅只读,深只读实现起来也很简单,判断只读标记然后递归添加只读属性就行了.
function createReactive(obj, isShallow = false, isReadOnly = false) { return new Proxy(obj, { get(target, key, receiver) { if (key === "raw") { // 通过获取raw属性,拿到初始对象 return target; } if (!isReadOnly) { // 只读情况下不需要建立联系 track(target, key); } const res = Reflect.get(target, key, receiver); if (isShallow) return res; // 如果浅层响应,直接返回 if (typeof res === "object" && res !== null) { // 如果获取的值是对象,且只读标记的值为true,递归调用readonly函数,得到深层只读响应对象.否则,递归调用reactive函数,得到深层响应对象 return isReadOnly ? readonly(res) : reactive(res); } return res; },
然后和reactive函数一样,封装一下只读readonly函数.
function readonly(obj) { return createReactive(obj, true, true); } function shallowReadonly(obj) { return createReactive(obj, false, true); }
7.代理数组
(1)读取和修改操作
数组的读取操作:
- 通过索引访问元素,arr[0]
- 访问数组长度,arr.length
- for in循环访问arr对象
- for of循环访问arr对象
- 数组的原型方法,find,concat等
数组的修改操作:
- 通过索引修改数组,arr[0] = 1
- 修改数组长度,arr.length = 1
- 数组的栈、队列方法,arr.push
- 修改数组的原型方法,arr.slice,arr.sort等
对于通过索引访问这一操作,它实际上和普通对象是一样的,都可以通过get直接拦截。但是对于通过索引修改这一操作,就稍有不同了,因为如果当前设置的索引>数组长度的话,相应的也会对数组的长度进行修改,而且在修改数组长度的过程中,还需要对数组长度的修改做出响应。同时,直接修改数组的length属性也会造成影响,如果小于当前数组长度,那么会对差值内元素进行清楚操作,否则则对之前的元素没有影响。
首先我们对应修改数组索引设置这一操作:
function createReactive(obj, isShallow = false, isReadOnly = false) { return new Proxy(obj, { set(target, key, newVal, receiver) { if (isReadOnly) { // 如果对象只读,提示报错信息 console.warn(`属性${key}是只读的`); return true; } const oldVal = target[key]; // 获取旧值 // 判断操作类型,如果是数组类型,则根据索引大小来判断 const type = Array.isArray(target) ? Number(key) < target.length ? "SET" : "ADD" : Object.prototype.hasOwnProperty.call(target, key) ? "SET" : "ADD"; // 获取操作类型 const res = Reflect.set(target, key, newVal, receiver); if (target === receiver.raw) { if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) { trigger(target, key, type, newVal); // 添加第四个参数 } } return res; } } }
然后修改trigger函数,判断是否为数组和ADD操作,然后添加length属性的相关操作
// trigger函数添加第四个参数newVal,即触发响应的值 function trigger(target, key, type) { const depsMap = bucket.get(target); // 首先从对象桶中取出当前对象的依赖表 if (!depsMap) return; const effects = depsMap.get(key); // 从依赖表中拿到当前键值的依赖集合 const iterateEffects = depsMap.get(ITERATE_KEY); // 尝试获取for in循环操作的依赖集合 const effectToRun = new Set(); // 创建依赖执行队列 if (type === "ADD" && Array.isArray(target)) { // 如果操作类型是ADD且对象类型是数组,将length相关依赖添加到待执行队列中 const lengthEffects = depsMap.get("length"); lengthEffects && lengthEffects.forEach((fn) => { if (fn !== activeEffect) { effectToRun.add(effectFn); } }); } if (Array.isArray(target) && key === "length") { // 对于索引大于等于新length值的元素,需要将所有相关联的函数取出添加到effectToRun中待执行 if (key >= newVal) { effects.forEach((fn) => { if (fn !== activeEffect) { effectToRun.add(fn); } }); } }
(2)数组的遍历
首先是for in循环,会影响for in循环的操作主要是根据索引设置数组值和修改数组的length属性,而这两种操作,实际上都是对数组length值的操作,因此我们只需要在onwKeys方法里判断,当前操作的是否是数组,如果是数组的话,就使用length属性作为key并建立联系。
ownKeys(target, key) { // 这里将副作用函数和唯一标识ITERATE_KEY绑定了 track(target, Array.isArray(target) ? "length" : ITERATE_KEY); // 进行依赖收集 return Reflect.ownKeys(target); },
然后是for of循环,它主要是通过和索引和length进行操作,所以不需要进行额外的操作,就可以实现依赖。但是在使用for of循环的时候,会对数组的Symbol.iterator属性进行读取,该属性是一个symbol值,为了避免发生意外错误,以及性能上的考虑,需要对类型为了symbol的值进行隔离。
function createReactive(obj, isShallow = false, isReadOnly = false) { return new Proxy(obj, { get(target, key, receiver) { if (key === "raw") { // 通过获取raw属性,拿到初始对象 return target; } if (!isReadOnly && typeof key !== "symbol") { // 只读情况和key值为symbol的情况下不需要建立联系 track(target, key); } const res = Reflect.get(target, key, receiver); if (isShallow) return res; // 如果浅层响应,直接返回 if (typeof res === "object" && res !== null) { // 如果获取的值是对象,递归调用reactive函数,得到深层响应对象 return isReadOnly ? readonly(res) : reactive(res); } return res; }, } }
(3)数组的查找方法
arr.includes方法在正常情况下是可以正常触发绑定的,因为arr.include方法会在查找过程中访问数组对象的length属性和索引。但是在一些特殊的情况下,比如说数组元素是对象的情况下,在我们目前的响应式系统下,就会出现一些特殊的情况。
const obj = {}; const arr = reactive([arr]); console.log(arr.includes(arr)); // false
运行上述代码,得到的结果为false,这是因为在我们之前代码设计中,如果读取操作取到的值是一个可代理对象,那么我们会继续对这个对象进行代理。而进行继续代理后,得到的对象就是一个全新的对象了。
if (typeof res === "object" && res !== null) { // 如果获取的值是对象,递归调用reactive函数,得到深层响应对象 return isReadOnly ? readonly(res) : reactive(res); }
对此,我们创建一个缓存Map,避免重复创建的问题。
const reactiveMap = new Map(); function reactive(obj) { // 获取当前对象的缓存值 const existionProxy = reactiveMap.get(obj); // 如果当前对象存在缓存值,直接返回 if (existionProxy) return existionProxy; // 否则创建新的响应对象 const proxy = createReactive(obj, true); // 缓存新对象 reactiveMap.set(obj, proxy); return proxy; }
但是这个时候我们又会碰到一个新问题,就是如果传入原始对象,也就是obj的话,也会返回false,这是因为我们会从arr中拿到的是响应式对象,所以我们需要修改arr.includes的默认行为。
const originMethod = Array.prototype.includes; const arrayInstrumentations = { includes: function (...args) { // this是代理对象,先在代理对象中进行查找 let res = originMethod.apply(this, args); if (res === false) { // 如果在代理对象上无法找到,再到原始对象上找 res = originMethod.apply(this.raw, args); } return res; }, }; function createReactive(obj, isShallow = false, isReadOnly = false) { return new Proxy(obj, { get(target, key, receiver) { if (key === "raw") { // 通过获取raw属性,拿到初始对象 return target; } // 如果操作目标是数组,而且key处于arrayInstrumentations之上,那么返回自定义的行为 if (Array.isArray(target) && arrayInstrumentations.hasOwnProperty(key)) { return Reflect.get(arrayInstrumentations, key, receiver); } } } }
除了includes外,需要做类似处理的还有indexof和lastIndexOf
const arrayInstrumentations = {}; ["includes", "indexof", "lastIndexof"].forEach((method) => { const originMethod = Array.prototype[method]; arrayInstrumentations[method] = function (...args) { // this 是代理对象,先在代理对象中查找,将结果存储到 res 中 let res = originMethod.apply(this, args); // res 为 false 说明没找到,通过 this.raw 拿到原始数组,再去其中查找,并更新 res 值 if (res === false || res === -1) { res = originMethod.apply(this.raw, args); } return res; }; });
(4)隐式修改数组的方法
主要有push、pop、shift、unshift和splice,以push为例,push在添加元素的同时,也会读取length属性,而这回导致两个独立的副作用函数相互影响。因此我们也需要重写push操作,来避免这种情况的产生。这里我们添加一个是否进行追踪的标记,在push方法执行之前,将标记置为false
let shouldTrack = true; // 是否进行追踪标记 ["push"].forEach((method) => { const originMethod = Array.prototype[method]; arrayInstrumentations[method] = function (...args) { // 在调用原始方法之前,禁止追踪 shouldTrack = false; // 默认行为 let res = originMethod.apply(this, args); // 在调用原始方法之后,恢复原来的行为,即允许追踪 shouldTrack = true; return res; }; }); function track(target, key) { if (!activeEffect || !shouldTrack) { // 如果没有当前执行的副作用函数,不进行处理 return; } }
最后,修改所以该类行为。
["push", "pop", "shift", "unshift", "splice"].forEach((method) => { const originMethod = Array.prototype[method]; arrayInstrumentations[method] = function (...args) { // 在调用原始方法之前,禁止追踪 shouldTrack = false; // 默认行为 let res = originMethod.apply(this, args); // 在调用原始方法之后,恢复原来的行为,即允许追踪 shouldTrack = true; return res; }; });
加载全部内容