Vue reactive函数实现流程详解

1.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;
  };
});

原文地址:https://blog.csdn.net/weixin_49971653/article/details/128523625