七、vue响应式原理:Reflect,getter属性与for...in响应式

175 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

Reflect是什么

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers (en-US)的方法相同。Reflect不是一个函数对象,因此它是不可构造的。

Reflect与Proxy相辅相成,在我们的响应式系统实现中发挥着重要作用

receiver

不知道大家有没有注意到,前面我们在Proxy的get中是直接返回的target[key],这样其实在某些情况下是有问题的,比如:我们有一个拥有getter属性的对象,我们对这个对象进行代理

// 原始数据
const data = {
  _foo: 0,
  get foo() {
    return this._foo
  }
}
effect(() => {
  console.log(dataProxy.foo)
})
dataProxy._foo++
// 0

执行代码发现只打印了一次,我们应该希望改变_foo时要再次执行相关依赖,那么为什么没有执行呢?分析一下代码,我们打印dataProxy.foo,会执行getter属性的foo函数,返回this._foo的值,那么此时的this是指向的谁呢?

因为我们在Proxy的get里面直接返回的是target[key],所以很显然,foo函数的this隐式绑定为其调用者:data对象,所以这里_foo属性并没有触发Proxy的get handler,因此并没有被我们的响应式系统所收集,我们在对_foo进行修改操作的时候也不会重新执行对应的副作用函数。

解决方法相信大家也能想到,只要我们改变this的指向为代理后的dataProxy对象不就好了吗。那么怎么实现呢,其实Proxy的get handler有第三个参数:receiver,这个参数就是代理后的对象。Reflect也有一个静态方法:get,这个方法可以帮助我们获取一个对象的属性值,并且它也有第三个参数,这个参数这里我们可以简单理解为指定对应getter、setter属性的this,所以我们只需要用Reflect.set即可完美解决:

// 对原始数据的代理
const dataProxy = new Proxy(data, {
  // 拦截读取操作
  get(target, key, receiver) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    track(target, key)
    // 返回属性值
    return Reflect.get(target, key, receiver)
  },
  // 拦截设置操作
  set(target, key, newVal, receiver) {
    // 设置属性值
    // target[key] = newVa
    // set同理
    Reflect.set(target, key, newVal, receiver)
    // 把副作用函数从桶里取出并执行
    trigger(target, key)
  }
})

ownKeys

当我们在副作用函数中使用for...in操作符的时候,我们也希望是响应式的。

for...in语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。Proxy提供了相应的拦截handler:ownKeys

所以我们只需要在ownKeys里面收集依赖,然后在合适的地方去执行收集的依赖即可。

收集依赖

我们首先来实现收集依赖,这一步非常简单:

const ITERATE_KEY = Symbol()
new Proxy(obj, {
  // 省略部分代码
  ownKeys(target) {
    track(target, ITERATE_KEY)
    return Reflect.ownKeys(target)
  }
})

由于for...in操作没有具体的key,所以我们在track收集依赖的时候声明一个symbol值代替

执行依赖

相对于收集依赖,执行依赖就要考虑更多的情况。由于我们是对对象属性的遍历,所以我们应该只关心对象属性的增删,不考虑值的变化。

对象属性增加

对象属性的增加发生在Proxy的set handler中,所以我们在set对应值的时候应该判断该操作是修改值的操作还是增加属性的操作,这其实很简单,只需要判断target原来有没有相应的key

new Proxy(data, {
  // 拦截设置操作
  set(target, key, newVal, receiver) {
    // 判断原对象有没有key 有type为EDIT 没有为ADD
    const type = Object.prototype.hasOwnProperty.call(target, key) ? 'EDIT' : 'ADD'
    // 设置属性值
    const res = Reflect.set(target, key, newVal, receiver)
    // 把副作用函数从桶里取出并执行 type传入trigger 在trigger中取出ITERATE_KEY对应的依赖集合执行
    trigger(target, key, type)
    return res
  }
})
function trigger(target, key, type) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  // for...in 对应的key依赖
  const iterateEffects = depsMap.get(ITERATE_KEY)

  const effectsToRun = new Set()
  effects && effects.forEach(effectFn => {
    if (effectFn !== activeEffect) {
      effectsToRun.add(effectFn)
    }
  })
  // 判断type为ADD时添加到effectsToRun集合执行
  if (type === 'ADD') {
    iterateEffects && iterateEffects.forEach(effectFn => {
      if (effectFn !== activeEffect) {
        effectsToRun.add(effectFn)
      }
    })
  }
  effectsToRun.forEach(effectFn => {
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn)
    } else {
      effectFn()
    }
  })
  // effectsToRun.forEach(effectFn => effectFn())
}

上面代码我们在set handler中判断是否是增加属性的操作,然后将type传入trigger,trigger根据type去获取执行对应依赖

对象属性删除

在我们对对象的属性进行删除时也应该执行对应的依赖,Proxy提供了对应的handler:deleteProperty,

我们只需要在这里执行对应依赖即可:

// 对原始数据的代理
const dataProxy = new Proxy(data, {
  deleteProperty(target, key) {
    // 判断key是否存在
    const exist = Object.prototype.hasOwnProperty.call(target, key)
    // 删除操作 res是否删除成功
    const res = Reflect.deleteProperty(target, key)
    if (exist && res) {
      // 存在且删除成功触发依赖执行
      trigger(target, key, 'DELETE')
    }
    return res
  }
})

在trigger中,我们只需要判断type时增加|| type === 'DELETE'即可

重复赋值

在set handler中,我们并没有判断新值跟旧值是否相同,在新旧值相等时,我们不应该去执行收集的依赖

// 对原始数据的代理
const dataProxy = new Proxy(data, {
  // 拦截设置操作
  set(target, key, newVal, receiver) {
    // 获取旧值
    const oldVal = target[key]
    const type = Object.prototype.hasOwnProperty.call(target, key) ? 'EDIT' : 'ADD'
    // 设置属性值
    const res = Reflect.set(target, key, newVal, receiver)
    // 判断新旧值是否相同且都不为NaN 把副作用函数从桶里取出并执行
    if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
      trigger(target, key, type)
    }
    // trigger(target, key, type)
    return res
  },
})