ShawDubie

Vue 系列(四):完善响应式系统

在上一篇文章中,我们简单认识了一下 Vue 中的响应式系统。其实 Vue 的响应式系统是非常复杂的,本篇我们将在上一篇文章的基础上,继续深入,实现一个更加完善的响应式系统。

Proxy 和 Reflect


我们在上一篇文章通过 Proxy 实现了对原始对象 set 和 get 的拦截:

export const reactive = (obj: any) => {
  return new Proxy(obj, {
    get(target: any, key: string) {
      track(target, key);
      return target[key];
    },
    set(target: any, key: string, value: any) {
      target[key] = value;
      trigger(target, key);
      return true;
    }
  });
};

可以看到,我们在设置和获取对象某个属性值的时候,使用的都是原始数据 target,比如:target[key]。但是在 Vue 中,她没有直接操作原始数据,而是通过 Reflect 来实现的。在具体介绍 Reflect 之前,我们先通过一个例子来看看之前的实现方式会产生什么问题:

// 原始数据
const target = { 
  a: 1,
  get b() {
    return this.a;
  }
}

// 响应式数据
const data = reactive(target);

// 注册副作用函数
effect(() => {
  console.log('b----->', data.b);
})

// 修改响应式数据的值
setTimeout(() => {
  data.a++;
}, 1000);

在上面的例子中,我们首先定义了一个原始数据 target,在该对象内有两个属性:a、b,其中 b 是一个 getter,她会返回 a 的值。接着我们通过 reactive 方法对 target 进行代理,得到一个响应式数据 data。然后我们注册了一个副作用函数来读取 data.b。最后我们写了一个定时器,1秒之后修改 a 的值。

理想情况下,我们希望修改 a 的值,会触发副作用函数的执行,因为我们在副作用函数中访问了 data.b,而 b 属性会执行 return this.a,这样就将 a 与副作用函数联系起来了。可是当我们执行 data.a++ 之后并不会触发副作用函数的执行。那这是为什么呢?接下来我们分析一下副作用函数执行的时候发生了什么。

effect(() => {
  console.log('b----->', data.b);
})

当副作用函数执行 data.b,会触发 get 方法的执行:

get(target: any, key: string) {
  track(target, key);
  return target[key];
}

可以看到,我们是通过 target[key] 来返回属性值的,这句代码等价于 target.b。当执行 target.b 的时候会执行 getter 方法:

get b() {
  return this.a;
}

这里的 this 就是 target。分析到这里相信你已经能够发现问题了,我们通过 target.a 读取值并不会进行副作用函数的收集,因为 target 是原始数据,不是响应式数据。这就是为什么前面的例子没有触发副作用函数执行的原因。那么我们要怎么解决这个问题呢,在 Vue 中是通过 Reflect 来实现的:

export const reactive = (obj: any) => {
  return new Proxy(obj, {
    get(target: any, key: string, receiver: any) {
      track(target, key);
      return Reflect.get(target, key, receiver);
    },
    ...
  });
};

在上面的代码中,我们不再通过原始数据 target 来获取属性的值,而是通过 Reflect 来重新实现。可以看到,在 set 方法中我们增加了一个参数 receiver,这个参数用来表示谁在读取属性。同样,Reflect.get 中的第三个参数也有这样的含义。这时我们再来看看最初的那个例子:

effect(() => {
  console.log('b----->', data.b);
})

这时再执行 data.b,依然会触发 get 方法的执行,只不过这次执行的是如下代码:

get(target: any, key: string, receiver: any) {
  track(target, key);
  return Reflect.get(target, key, receiver);
}

这时我们不再通过原始数据 target 来访问属性了,这里的 receiver 就是响应式数据 data,当我们访问 data.b,依然会执行 getter 方法:

get b() {
  return this.a;
}

但此时的 this 就不再是 target,而是 data。因为 data 是响应式数据,所以就会在 a 与副作用函数之间建立联系,当再次修改 a 的值时,就能触发响应式函数的执行了。

我们通过 Reflect 解决了上面遇到的问题,其实 Reflect 和 Proxy 仿佛是两个孪生对象,Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。但她们又彼此互补。我们通过 Proxy 重写对象中的行为,然后通过 Reflect 完成默认行为。关于 Reflect 更多了解可以参考阮一峰老师的文章

我们将原来的 reactive 方法通过 Reflect 重新实现如下:

完整代码
export const reactive = (obj: any) => {
  return new Proxy(obj, {
    get(target: any, key: string, receiver: any) {
      track(target, key);
      const res = Reflect.get(target, key, receiver);
      return res;
    },
    set(target: any, key: string, value: any, receiver: any) {
      const res = Reflect.get(target, key, value, receiver);
      trigger(target, key);
      return res;
    }
  });
};

重新理解对象的「读取」和「设置」


在上一篇文章中,我们说实现一个响应式数据的核心就是拦截数据的 读取 和 设置 操作。我们在前面的例子中也的确实现了对对象的 get 和 set 的拦截。但这只是狭义上的「读取」和「设置」。实际上对象的「读取」和「设置」不止于 get 和 set。

对一个对象的读取操作有如下几种方式:

  • 访问属性:data.a

  • 判断对象或原型上是否存在某个属性:a in data

  • 遍历对象:for(const key in data)

对一个对象的设置操作有如下几种方式:

  • 设置或者增加属性:data.a = 2

  • 删除属性:delete data.a

那么接下来,我们就逐个讨论如何拦截这些操作。

拦截 in 操作符

当我们要判断某个对象中是否存在某个属性时,可以通过 in 操作符来实现:

const data = reactive({ a: 1 });

console.log('a' in data ) // true

其实上面的例子等价于:

const data = reactive({ a: 1 });

console.log(Reflect.has(data, 'a')) // true

我们知道 Reflect 中的方法和 Proxy 中的一样,那么我们要想拦截 in 操作符,就只需要实现 Proxy 中的 has 方法即可:

const reactive = (obj: any) => {
  return new Proxy(obj, {
    ...
    // 拦截 in 操作
    has(target: any, key: string) {
      track(target, key);
      return Reflect.has(target, key);
    }
    ...
  });
}
拦截 for...in 操作符

我们通过 for...in 来遍历对象,并获取对象的属性:

const data = { a: 1, b: 2 };

for(const key in data) {
  console.log('key--->', key);
}

// key---> a
// key---> b

其实 for...in 的底层实现使用了 Reflect.ownKeys,我们可以将上面的代码替换成:

const data = { a: 1, b: 2 };

for(const key of Reflect.ownKeys(data)) {
  console.log('key--->', key);
}

那我们要拦截 for...in 就只需要实现 Proxy 中的 ownKeys:

// 遍历对象的绑定字段
const ITERATOR_KEY = Symbol();

const reactive = (obj: any) => {
  return new Proxy(obj, {
    ...
    // 拦截 for...in 操作
    ownKeys(target: any) {
      // 通过 ITERATOR_KEY 作为副作用函数的关联key
      track(target, ITERATOR_KEY);
      return Reflect.ownKeys(target);
    }
    ...
  });
}

我们定义了一个唯一的 key 作为标识,因为 Reflect.ownKeys 返回的是一个集合,我们没有办法像之前一样与某个特定的属性进行绑定,所以定义了一个全局的常量 ITERATOR_KEY,我们通过该值来与触发 for...in 的副作用函数进行关联。既然对副作用函数进行了收集,那么我们何时触发副作用函数的执行呢?

因为 for...in 是一个遍历的行为,只有改变对象属性的数量才会影响遍历行为。那么什么情况下会改变一个对象属性的数量呢——添加和删除属性。那么我们只需要在添加和删除属性的时候去触发副作用函数的执行即可,我们先来看一下添加行为(删除会在后面介绍):

// 触发副作用函数执行的类型
enum TRIGGER_TYPE {
  SET = 'SET', // 修改
  ADD = 'ADD', // 增加
  DELETE = 'DELETE', // 删除
}

const reactive = (obj: any) => {
  return new Proxy(obj, {
    ...
    set(target: any, key: string, value: any, receiver: any) {
      // 判断对象中是否有该属性
      // 如果有,则本次操作为修改属性值
      // 如果没有,则本次操作为增加新的属性
      const triggerType = target.hasOwnProperty(key) ? TRIGGER_TYPE.SET : TRIGGER_TYPE.ADD;
      const res = Reflect.set(target, key, value, receiver);
      trigger(target, key, triggerType);
      return res;
    }
    ...
  })
}

其实对添加属性的拦截就是通过 set 来实现的,但是修改某个属性的值也会触发 set,所以我们通过 target.hasOwnProperty(key) 来判断对象中是否存在某个属性。然后调用 trigger 方法,可以看到我们给 trigger 方法增加了一个参数 triggerType,该参数就是用来标识此次是修改行为还是添加行为。现在我们来看看 trigger 方法的实现:

const trigger = (target: any, key: string | symbol, triggerType: TRIGGER_TYPE) => {
  const depsMap = bucket.get(target);
  if (depsMap) {
    ...
    // 取出 for...in 的副作用函数集合
    const iteratorEffects = depsMap.get(ITERATOR_KEY);
    // 当 新增或者删除属性的时候才触发副作用函数的执行
    if (iteratorEffects && (triggerType === TRIGGER_TYPE.ADD || triggerType === TRIGGER_TYPE.DELETE)) {
      const effectsToRun: Set<EffectFunction> = new Set();
      iteratorEffects.forEach(fn => {
        if (fn !== activeEffect) {
          effectsToRun.add(fn);
        }
      })
      effectsToRun.forEach(fn => fn());
    }
    ...
  }
};

可以看到,当给对象添加属性或者删除属性的时候,我们将与 ITERATOR_KEY 相关联副作用函数取出来逐个执行。

这样我们通过 ownKeys 和 ITERATOR_KEY 完成了对 for...in 的拦截。当给对象添加属性的时候,我们通过重新实现 set 和 trigger 完成对副作用函数的执行操作。

拦截 delete 操作符

我们通过 delete 来删除对象中的某个属性:

const data = reactive({ a: 1, b: 2 });

delete data.a;

上面的代码等价于:

const data = reactive({ a: 1, b: 2 });

Reflect.deleteProperty(data, 'a');

那么我们可以使用 Proxy 中的 deleteProperty 方法对 delete 进行拦截:

const reactive = (obj: any) => {
  return new Proxy(obj, {
    ...
    // 拦截删除操作
    deleteProperty(target: any, key: string) {
      // 判断对象中是否有该属性
      const hasKey = target.hasOwnProperty(key);
      // 删除操作
      const res = Reflect.deleteProperty(target, key);
      // 如果删除成功且对象中有该属性,则触发副作用函数
      if (hasKey && res) {
        trigger(target, key, TRIGGER_TYPE.DELETE);
      }
      return res;
    }
    ...
  })
}

我们首先通过 hasOwnProperty 判断对象中是否有该属性,然后执行删除操作。当对象中存在该属性且被删除成功之后,这时就改变了对象中属性的数量,如前面我们说过的那样,此时需要触发与 for...in 关联的副作用函数的执行。

至此我们就拦截了对象所有的读取和设置行为,完整代码如下:

查看代码
type EffectFunction = {
  (): void,
  deps: Array<Set<EffectFunction>>
}

// 触发副作用函数执行的类型
enum TRIGGER_TYPE {
  SET = 'SET', // 修改
  ADD = 'ADD', // 增加
  DELETE = 'DELETE', // 删除
}

let activeEffect: EffectFunction | null = null;
const effectStack: Array<EffectFunction> = [];
const bucket: WeakMap<any, Map<string | symbol, Set<EffectFunction>>> = new WeakMap();
// 遍历对象的绑定字段
const ITERATOR_KEY = Symbol();

const reactive = (obj: any) => {
  return new Proxy(obj, {
    get(target: any, key: string, receiver: any) {
      const res = Reflect.get(target, key, receiver);
      return res
    },
    // 拦截 in 操作
    has(target: any, key: string) {
      track(target, key);
      return Reflect.has(target, key);
    },
    // 拦截 for...in 操作
    ownKeys(target: any) {
      // 通过 ITERATOR_KEY 作为副作用函数的关联key
      track(target, ITERATOR_KEY);
      return Reflect.ownKeys(target);
    },
    // 拦截删除操作
    deleteProperty(target: any, key: string) {
      if (isReadOnly) {
        console.warn(`属性 ${key} 是只读的`)
        return true;
      }
      // 判断对象中是否有该属性
      const hasKey = target.hasOwnProperty(key);
      // 删除操作
      const res = Reflect.deleteProperty(target, key);
      // 如果删除成功且对象中有该属性,则触发副作用函数执行
      if (hasKey && res) {
        trigger(target, key, TRIGGER_TYPE.DELETE);
      }
      return res;
    },
    set(target: any, key: string, value: any, receiver: any) {
      // 判断对象中是否有该属性
      // 如果有,则本次操作为修改属性值
      // 如果没有,则本次操作为增加新的属性
      const triggerType = target.hasOwnProperty(key) ? TRIGGER_TYPE.SET : TRIGGER_TYPE.ADD;
      const res = Reflect.set(target, key, value, receiver);
      trigger(target, key, triggerType);
      return res;
    }
  });
}

const track = (target: any, key: string | symbol) => {
  if (!activeEffect) {
    return;
  }
  let depsMap = bucket.get(target);
  if (!depsMap) {
    depsMap = new Map();
    bucket.set(target, depsMap);
  }
  let deps = depsMap.get(key);
  if (!deps) {
    deps = new Set();
    depsMap.set(key, deps);
  }
  deps.add(activeEffect);
  activeEffect.deps.push(deps);
};

const trigger = (target: any, key: string | symbol, triggerType: TRIGGER_TYPE) => {
  const depsMap = bucket.get(target);
  if (depsMap) {
    const effects = depsMap.get(key);
    // 取出 for...in 的副作用函数集合
    const iteratorEffects = depsMap.get(ITERATOR_KEY);
    // 当 新增属性和删除属性的时候才触发副作用函数的执行
    if (iteratorEffects && (triggerType === TRIGGER_TYPE.ADD || triggerType === TRIGGER_TYPE.DELETE)) {
      const effectsToRun: Set<EffectFunction> = new Set();
      iteratorEffects.forEach(fn => {
        if (fn !== activeEffect) {
          effectsToRun.add(fn);
        }
      })
      effectsToRun.forEach(fn => fn());
    }
    if (effects) {
      const effectsToRun: Set<EffectFunction> = new Set();
      effects.forEach(fn => {
        if (fn !== activeEffect) {
          effectsToRun.add(fn);
        }
      })
      effectsToRun.forEach(fn => fn());
    }
  }
};

const cleanup = (fn: EffectFunction) => {
  fn.deps.forEach(item => item.delete(fn));
  fn.deps.length = 0;
}

export const effect = (fn: Function) => {
  const effectFn = () => {
    cleanup(effectFn);
    activeEffect = effectFn;
    effectStack.push(effectFn);
    fn();
    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1]
  }
  effectFn.deps = [] as Array<Set<EffectFunction>>;
  effectFn();
}

优化 trigger 的执行时机


在前面的介绍中,我们重新认识了对象的「读取」和「设置」并详细介绍了如何去拦截不同的操作行为。前面介绍的拦截操作里面大部分都是对副作用函数的收集。既然收集了副作用函数那当然需要触发副作用函数的执行,接下来我们就详细介绍一下怎么合理地触发副作用函数的执行。

我们先来看一个例子:

const data = reactive({ a: 1 })

effect(() => console.log('a=====>', data.a)); // a=====>1

setTimeout(() => {
  data.a = 1; 
}, 1000) // a=====>1

在上面的例子中我们首先定义了一个响应式数据 data,然后通过一个副作用函数读取了 data.a。一秒之后我们修改 data.a = 1,依然会打印出 a=====>1。

上面的例子显然是不符合我们预期的,因为我们压根就没有改变原来的值,却触发了响应。那么我们需要处理新旧值相同的情况,只有当新旧值不相等的时候才去触发响应:

查看完整代码
const reactive = (obj: any) => {
  return new Proxy(obj, {
    set(target: any, key: string, value: any, receiver: any) {
      // 旧值
      const oldValue = target[key];
      // 判断对象中是否有该属性
      // 如果有,则本次操作为修改属性值
      // 如果没有,则本次操作为增加新的属性
      const triggerType = target.hasOwnProperty(key) ? TRIGGER_TYPE.SET : TRIGGER_TYPE.ADD;
      const res = Reflect.set(target, key, value, receiver);
      // 新旧值不一样才触发副作用函数执行
      // 处理NaN的情况
      if (oldValue !== value && (oldValue === oldValue || value === value)) {
        trigger(target, key, triggerType);
      }
      return res;
    }
  })
}

在上面的代码中我们修改了原来的 set 方法。先取出旧值,然后将旧值与新值进行比较,如果不一样才触发 trigger 的执行。但是会有个特殊情况—— NaN,因为:

const a = NaN
const b = NaN

a === b // false
a !== b // true

所以我们通过 oldValue === oldValue || value === value 来处理 NaN 的情形。

接下来我们再来看一个例子:

const bar = {}
const foo = { a: 1 }
const child = reactive(bar);
const parent = reactive(foo);

Object.setPrototypeOf(child, parent)

effect(() => console.log('child.a===>', child.a)); // child.a===>1

setTimeout(() => {
  child.a = 2;
}, 1000);

// 会打印两次 child.a===>2
// child.a===>2
// child.a===>2

在上面的例子中我们先定义了两个普通对象:bar、foo。然后将两个对象通过 reactive 方法生成响应式对象:child 和 parent,并使用 Object.setPrototypeOf 将 parent 设置为 child 的原型。接下来我们通过副作用函数读取 child.a 的值,然后一秒之后设置 child.a = 2。按照我们的期望,应该会打印一次 child.a===>2,但是当们重新修改 child.a 之后,打印了两次 child.a===>2。那么这是为什么呢?我们先来逐步分析一下整个流程:

  • 执行 child.a 首先会触发 set 的执行,child.a 与副作用函数建立联系

  • child 中没有 a 属性,会去访问 child 的原型,也就是执行 parent.a。由于 parent 是响应式数据,也会导致副作用函数被收集

  • 执行 child.a = 2,触发与 child.a 关联的副作用函数的执行,打印 child.a===>2

  • child 中没有 a 属性,仍然回去访问其原型,也就是执行 parent.a = 2,这时又会触发 parent 的 set 方法执行,打印 child.a===>2

所以最终被打印了两次。既然被打印了两次,那我们的解决办法也很简单,只需要屏蔽其中的一次打印即可。当我们设置 child.a = 2 时,首先会执行 child 的拦截函数:

set(target: any, key: string, value: any, receiver: any) {
  ...
}

这时 target 是原始对象 bar,receiver 是代理对象 child。但 bar 中不存在 a 属性,这时又会执行 parent 的拦截函数:

set(target: any, key: string, value: any, receiver: any) {
  ...
}

此时 target 是原始对象 foo,但 receiver 依然是代理对象 child,而不是 foo 的代理对象 parent。发现了这个问题之后,我们就有了解决思路,只有满足:当 receiver 是 target 的代理对象时,再触发副作用函数的执行。这样我们就能屏蔽其中的一次打印了。那我们怎么才能确定 receiver 是 target 的代理对象 呢?,我们可以通过这种方式来实现:

// receiver 通过该字段访问原始数据
const TARGET_KEY = Symbol();

const reactive = (obj: any) => {
  return new Proxy(obj, {
    get(target: any, key: string | symbol, receiver: any) {
      // 当使用 TARGET_KEY 访问对象时,返回原始数据
      if (key === TARGET_KEY) {
        return target;
      }
      const res = Reflect.get(target, key, receiver);
      return res
    }
  })
}

在上面的代码中,我们首先定义了一个唯一的key:TARGET_KEY,当代理对象访问该属性的时候可以得到其原始数据,比如:

child[TARGET_KEY] === bar // true
parent[TARGET_KEY] === foo // true 

这样我们就能在 trigger 方法执行之前判断 receiver 是否是 target 的代理对象了:

const reactive = (obj: any) => {
  return new Proxy(obj, {
    set(target: any, key: string, value: any, receiver: any) {
      // 旧值
      const oldValue = target[key];
      // 判断对象中是否有该属性
      // 如果有,则本次操作为修改属性值
      // 如果没有,则本次操作为增加新的属性
      const triggerType = target.hasOwnProperty(key) ? TRIGGER_TYPE.SET : TRIGGER_TYPE.ADD;
      const res = Reflect.set(target, key, value, receiver);
      // 相等说明 receiver 是 target 的代理对象
      if (target === receiver[TARGET_KEY]) {
        // 新旧值不一样才触发副作用函数执行
        // 处理NaN的情况
        if (oldValue !== value && (oldValue === oldValue || value === value)) {
          trigger(target, key, triggerType);
        }
      }
      return res;
    }
  })
}

在上面的代码中,我们添加了 target === receiver[TARGET_KEY] 这样一行代码来判断 receiver 是否是 target 的代理对象,当满足条件的时候才会触发更新,这样就能解决我们上面因为原型链而导致的问题了。

深响应与浅响应


我们前面实现的响应式系统其实都是浅响应的,比如:

const data = reactive({ a: { b: 1 } });

effect(() => console.log('data.a.b--->', data.a.b)); // data.a.b--->1

setTimeout(() => {
  data.a.b = 2;
}, 1000); // 不会触发响应

当我们执行上面例子的时候,不会触发响应,这是因为,当执行 data.a.b 首先会执行 data.a,这时会触发 Reflect.get 的执行。但是在拦截 get 的时候,我们是直接返回了 Reflect.get 的结果,所以这时会返回:

{ b: 1 }

但是上面的对象不是响应式的,所以当我们修改 data.a.b 的时候不会触发响应,要想实现深层次的响应,我们需要对 Reflect.get 返回的结果做一层包装:

const reactive = (obj: any) => {
  return new Proxy(obj, {
    get(target: any, key: string | symbol, receiver: any) {
      // 当使用 TARGET_KEY 访问对象时,返回原始数据
      if (key === TARGET_KEY) {
        return target;
      }
      const res = Reflect.get(target, key, receiver);
      // 如果返回值不为null,且返回值类型为object
      if (res && typeof res === 'object') {
        // 递归调用 reactive
        return reactive(res);
      }
      return res
    }
  })
}

当 Reflect.get 的返回值不为 null,且返回值类型为 object 时,递归地调用 reactive 将其包装成响应式数据。这样就实现了深响应。但是有时候我们并不需要一个数据是深响应的,所以我们需要对 reactive 方法进行调整,让其也支持浅响应:

完整代码
const createReactive = (obj: any, isShallow = false) => {
  return new Proxy(obj, {
    get(target: any, key: string | symbol, receiver: any) {
      // 当使用 TARGET_KEY 访问对象时,返回原始数据
      if (key === TARGET_KEY) {
        return target;
      }
      const res = Reflect.get(target, key, receiver);
      // 浅响应或浅只读直接返回
      if (isShallow) {
        return res;
      }
      if (res && typeof res === 'object') {
        // 深响应递归调用reactive
        return reactive(res);
      }
      return res
    },
    // 省略其他方法
  });
}

// 深响应
const reactive = (obj: any) => createReactive(obj);

// 浅响应
const shallowReactive = (obj: any) => createReactive(obj, true);

我们将原来 reactive 方法中的内容抽离了出来,重新实现了一个工具方法 createReactive,然后我们就可以通过该方法来实现深响应和浅响应了。

只读与浅只读


前面我们说了深响应和浅响应,其实只读和浅只读也可以通过 createReactive 方法来实现。在 Vue 中也有一些数据是需要设计成「只读」,比如 props。当一个数据为「只读」时,我们是无法修改的,只会收到一条警告信息,那么接下来我们就通过 createReactive 来实现「只读」和「浅只读」。

如果一个一个数据为「只读」数据,那么当我们修改和删除其值的时候都是不被允许的,而且既然该数据无法修改,我们也没有必要在其被读取的时候去收集相关的副作用函数:

完整代码
const createReactive = (obj: any, isShallow = false, isReadOnly = false) => {
  return new Proxy(obj, {
    get(target: any, key: string | symbol, receiver: any) {
      // 当使用 TARGET_KEY 访问对象时,返回原始数据
      if (key === TARGET_KEY) {
        return target;
      }
      // 非只读的时候才收集
      if (!isReadOnly) {
        track(target, key);
      }
      const res = Reflect.get(target, key, receiver);
      // 浅响应或浅只读直接返回
      if (isShallow) {
        return res;
      }
      if (res && typeof res === 'object') {
        // 深只读和深响应递归调用相应函数
        return isReadOnly ? readOnly(res): reactive(res);
      }
      return res
    },
    // 拦截删除操作
    deleteProperty(target: any, key: string) {
      if (isReadOnly) {
        console.warn(`属性 ${key} 是只读的`)
        return true;
      }
      // 判断对象中是否有该属性
      const hasKey = target.hasOwnProperty(key);
      // 删除操作
      const res = Reflect.deleteProperty(target, key);
      // 如果删除成功且对象中有该属性,则触发副作用函数
      if (hasKey && res) {
        trigger(target, key, TRIGGER_TYPE.DELETE);
      }
      return res;
    },
    set(target: any, key: string, value: any, receiver: any) {
      if (isReadOnly) {
        console.warn(`属性 ${key} 是只读的`)
        return true;
      }
      // 旧值
      const oldValue = target[key];
      // 判断对象中是否有该属性
      // 如果有,则本次操作为修改属性值
      // 如果没有,则本次操作为增加新的属性
      const triggerType = target.hasOwnProperty(key) ? TRIGGER_TYPE.SET : TRIGGER_TYPE.ADD;
      const res = Reflect.set(target, key, value, receiver);
      // 相等说明 receiver 是 target 的代理对象
      if (target === receiver[TARGET_KEY]) {
        // 新旧值不一样才触发副作用函数执行
        // 处理NaN的情况
        if (oldValue !== value && (oldValue === oldValue || value === value)) {
          trigger(target, key, triggerType);
        }
      }
      return res;
    }
  });
}

// 深只读
const readOnly = (obj: any) => createReactive(obj, false, true);

// 浅只读,第二个参数为 true
const shallowReadOnly = (obj: any) => createReactive(obj, true, true);

在上面的代码中,我们在 get 方法中增加了如下逻辑:

 // 非只读的时候才收集
if (!isReadOnly) {
  track(target, key);
}

因为数据是只读的,所以我们没有必要进行相应的副作用函数收集,如果某个数据是深只读的,我们也需要递归调用 readOnly:

if (res && typeof res === 'object') {
  // 深只读和深响应递归调用相应函数
  return isReadOnly ? readOnly(res): reactive(res);
}

因为只读数据不能被删除和修改,所以我们在 delete 和 set 方法中都增加了:

if (isReadOnly) {
  console.warn(`属性 ${key} 是只读的`)
  return true;
}

这样当用户尝试修改和删除只读数据时,就会收到警告。「深只读」和「浅只读」只需要修改 isShallow 的值即可:

// 深只读
const readOnly = (obj: any) => createReactive(obj, false, true);

// 浅只读,第二个参数为 true
const shallowReadOnly = (obj: any) => createReactive(obj, true, true);

最后


通过本文,我们已经写出了一个更加完善的响应式系统了,但是现在的响应式系统只能处理简单的对象。还有一些其他数据我们依然无法将其包装成响应式数据,比如数组,在后面的文章中,我们还会继续完善这个响应式系统。本篇文章中的例子,请戳