vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > Vue reactive函数

Vue reactive函数实现流程详解

作者:volit_

一个基本类型的数据,想要变成响应式数据,那么需要通过ref函数包裹,而如果是一个对象的话,那么需要使用reactive函数,这篇文章主要介绍了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)代理读取操作

对一个普通对象的所有可能的读取操作:

  首先对于基本的访问属性,我们可以使用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)读取和修改操作

数组的读取操作:

数组的修改操作:

  对于通过索引访问这一操作,它实际上和普通对象是一样的,都可以通过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;
  };
});

到此这篇关于Vue reactive函数实现流程详解的文章就介绍到这了,更多相关Vue reactive函数内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文