【拆解Vue3】reactive是如何实现的?(下篇)

867 阅读7分钟

我正在参与掘金创作者训练营第6期, 点击了解活动详情

本篇内容基于【拆解Vue3】reactive是如何实现的?(上篇)实现。

响应式中的for...in

在Vue中,我们也可以在副作用函数中使用for...in循环来枚举响应式对象的属性。

const data = {
  foo: 1
}
const obj = reactive(data)
effect(() => {
  for(const key in obj) {
    console.log(key)
  }
})

在JavaScript中,任何操作的底层实现都基于基本语义方法,for...in也不例外。在for...in中,使用了Reflect.ownKeys(obj)来获取只属于对象自身的属性名,因此,我们可以在Proxy中利用ownKeys(),来对for...in的Reflect.ownKeys(obj)进行拦截。

const bothNaN = (newValue, oldValue) => !(newValue === newValue || oldValue === oldValue)
const isEqual = (newValue, oldValue) => bothNaN(newValue, oldValue) || oldValue === newValue
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, key)
      if(key === 'raw') {
        return target
      }
      return Reflect.get(target, key, receiver)
    },
    set(target, key, newValue, receiver) {
      const oldValue = target[key]
      if(target === receiver.raw && !isEqual(newValue, oldValue)) {
        target[key] = newValue
        trigger(target, key)
      }
      return Reflect.set(target, key, newValue, receiver)
    },
    ownKeys(target) { // (1)
      track(target, ITERATE_KEY) // (2)
      return Reflect.ownKeys(target)
    }
  })
}

在(1)处,我们按照之前的思路,增加了拦截函数ownKeys(target)。但在拦截收集副作用函数的时候犯了难,ownKeys仅能拦截到target对象,无法拦截具体的key。因此,我们需要把key补上,同时能够满足key是唯一的,这里可以使用Symbol()生成全局唯一的key,供track收集指定的for...in副作用函数。在(2)处,我们引入了Symbol类型的值ITERATE_KEY来唯一表示for...in副作用函数。

QQ截图20220830165829.png

然而,当我们尝试运行修改后的代码,reactive还是无法实现对for...in的响应式。仔细想想也能够理解,我们在收集的时候,keySymbol类型的ITERATE_KEY,而我们修改的是obj.foo,此时触发的keyfoo而不是ITERATE_KEY。所以,我们需要修改触发部分的逻辑,当全局的ITERATE_KEY存在时,ITERATE_KEY对应的副作用函数也应该被触发。

function trigger(target, key) {
  const propsMap = objsMap.get(target)
  if(!propsMap) return
  const fns = propsMap.get(key)
  const iterateFns = propsMap.get(ITERATE_KEY) // (3)

  const otherFns = new Set()
  fns && fns.forEach(fn => {
    if(fn !== activeEffect) {
      otherFns.add(fn)
    }
  })
  iterateFns && iterateFns.forEach(fn => { // (4)
    if(fn !== activeEffect) {
      otherFns.add(fn)
    }
  })
  otherFns.forEach(fn => {
    if(fn.options.scheduler) {
      fn.options.scheduler(fn)
    } else {
      fn()
    }
  })
}

按照我们的思路,我们在(3)处获取到ITERATE_KEY对应的副作用函数,并在(4)处触发它。

QQ截图20220830170543.png

看上去,我们似乎已经能够实现对for...in的响应式了。别急,再回味一下。我们要实现的是for...in的响应式,响应式对象的特性是什么?是响应式对象修改时触发对应的副作用函数重新执行。这里for...in遍历得到的是key,也就是属性名,因此,只要响应式对象不再新增属性,for...in所在的副作用函数就不应该被执行。再具体点,在执行obj.bar = 2时,新增属性触发副作用函数重新执行是正常的,而在执行obj.foo++时,obj.foo是已有属性,副作用函数不应该被触发。

总结一下刚刚的思路其实就两点,for...in所在的副作用函数,仅在新增属性时才能被触发,而设置属性时,不会被触发。既然如此,我们就需要在trigger时进行区分,仅对新增操作做出响应。

const TriggerType = {
  SET: 'SET',
  ADD: 'ADD',
}

function trigger(target, key, type) {
  const propsMap = objsMap.get(target)
  if(!propsMap) return
  const fns = propsMap.get(key)
  const iterateFns = propsMap.get(ITERATE_KEY) 

  const otherFns = new Set()
  fns && fns.forEach(fn => {
    if(fn !== activeEffect) {
      otherFns.add(fn)
    }
  })
  if(type === TriggerType.ADD) { // (5)
    iterateFns && iterateFns.forEach(fn => {
      if(fn !== activeEffect) {
        otherFns.add(fn)
      }
    })
  }
  otherFns.forEach(fn => {
    if(fn.options.scheduler) {
      fn.options.scheduler(fn)
    } else {
      fn()
    }
  })
}

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, key)
      if(key === 'raw') {
        return target
      }
      return Reflect.get(target, key, receiver)
    },
    set(target, key, newValue, receiver) {
      const oldValue = target[key]
      const type = Object.prototype.hasOwnProperty.call(target, key) ? TriggerType.SET : TriggerType.ADD // (6)
      if(target === receiver.raw && !isEqual(newValue, oldValue)) {
        target[key] = newValue
        trigger(target, key, type)
      }
      return Reflect.set(target, key, newValue, receiver)
    },
    ownKeys(target) {
      track(target, ITERATE_KEY)
      return Reflect.ownKeys(target)
    }
  })
}

按照思路,我们在(6)处判断当前的key是否已经是target的属性,如果是,则为SET操作,反之则为ADD操作。我们把type作为参数传给trigger,(5)处增加了一个判断,仅在ADD操作时,才会触发for...in对应的副作用函数。

QQ截图20220830174259.png

删除响应式对象中的属性

前文提到过,在JavaScript中,任何操作的底层实现都基于基本语义方法,这点同样适用于对删除操作的拦截。例如,我们可以通过delete obj.foo删除响应式对象objfoo属性,这个行为依赖JavaScript提供的内部方法[[Delete]],该内部方法我们可以通过Proxy提供的deleteProperty处理器函数进行拦截。

    deleteProperty(target, key) {
      const hadKey = Object.prototype.hasOwnProperty.call(target, key)
      const res = Reflect.deleteProperty(target, key)
      if(hadKey && res) {
        trigger(target, key, TriggerType.DELETE)
      }
      return res;
    }

reactive中,我们新增了deleteProperty()方法用来拦截删除操作。这里需要注意的是,我们需要检查被删除属性是否存在于响应式对象上,当属性存在时才能进行删除。同时,删除也会减少响应式对象的属性数量,因此,也应该触发for...in副作用函数。

QQ截图20220830182624.png

浅响应与深响应

看到这里,我们已经完成了简单对象的响应式实现。接着,让我们定义一个复杂的响应式对象,看看能否产生预期的响应式效果。

const data = {
  foo: {
    bar: 1
  }
}
const obj = reactive(data)
effect(() => {
  console.log(obj.foo.bar)
})

QQ截图20220830183531.png

可以看到,内部对象失去了响应式。回忆一下reactive的整个实现过程,其实我们实现的响应式本质上是一种浅响应,响应式内部的对象没有进行响应式处理,它们仅仅是普通的对象。这点想明白了,后面的思路也就清晰了,我们只需要递归遍历响应式对象,给每个对象包上响应式就可以了。

const bothNaN = (newValue, oldValue) => !(newValue === newValue || oldValue === oldValue)
const isEqual = (newValue, oldValue) => bothNaN(newValue, oldValue) || oldValue === newValue
function createReactive(obj, isShallow = false) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, key)
      if(key === 'raw') {
        return target
      }
      const res = Reflect.get(target, key, receiver)
      if(isShallow) return res // (7)
      if(typeof res === 'object' && res !== null) {
        return reactive(res) // (8)
      }
      return res
    },
    set(target, key, newValue, receiver) {
      const oldValue = target[key]
      const type = Object.prototype.hasOwnProperty.call(target, key) ? TriggerType.SET : TriggerType.ADD
      if(target === receiver.raw && !isEqual(newValue, oldValue)) {
        target[key] = newValue
        trigger(target, key, type)
      }
      return Reflect.set(target, key, newValue, receiver)
    },
    ownKeys(target) {
      track(target, ITERATE_KEY)
      return Reflect.ownKeys(target)
    },
    deleteProperty(target, key) {
      const hadKey = Object.prototype.hasOwnProperty.call(target, key)
      const res = Reflect.deleteProperty(target, key)
      if(hadKey && res) {
        trigger(target, key, TriggerType.DELETE)
      }
      return res
    }
  })
}

function reactive(obj) { // (9)
  return createReactive(obj)
}

function shallowReactive(obj) { // (10)
  return createReactive(obj, true)
}

在上面这段代码实现中,在(8)处我们实现了递归遍历每一个子对象,并赋予其响应式能力。此外,考虑到我们的响应式需要分为浅响应和深响应,这里我们封装了一个工厂函数createReactive,默认实现深响应。如需实现浅响应,在(7)处直接返回get拦截结果就可以避免深响应。(9)(10)处,我们利用createReactive工厂函数,可以分别创建深响应对象reactive与浅响应对象shallowReactive

QQ截图20220830232036.png

只读的响应式对象

只读意味着仅支持对象的读取,而不支持修改。具体到响应式对象上,只读就是仅支持get操作而不支持set操作,不要忘记,删除也是一种修改,我们对只读对象的deleteProperty也要进行处理。当用户尝试对只读对象进行修改时,需要抛出对应的警告。

const bothNaN = (newValue, oldValue) => !(newValue === newValue || oldValue === oldValue)
const isEqual = (newValue, oldValue) => bothNaN(newValue, oldValue) || oldValue === newValue
function createReactive(obj, isShallow = false, isReadonly = false) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, key)
      if(key === 'raw') {
        return target
      }
      const res = Reflect.get(target, key, receiver)
      if(isShallow) return res
      if(typeof res === 'object' && res !== null) {
        return isReadonly ? readonly(res) : reactive(res) // (11)
      }
      return res
    },
    set(target, key, newValue, receiver) {
      if(isReadonly) { // (12)
        console.warn(`响应式对象的属性${key}是只读的,不能修改!`)
        return true
      }
      const oldValue = target[key]
      const type = Object.prototype.hasOwnProperty.call(target, key) ? TriggerType.SET : TriggerType.ADD
      if(target === receiver.raw && !isEqual(newValue, oldValue)) {
        target[key] = newValue
        trigger(target, key, type)
      }
      return Reflect.set(target, key, newValue, receiver)
    },
    ownKeys(target) {
      track(target, ITERATE_KEY)
      return Reflect.ownKeys(target)
    },
    deleteProperty(target, key) {
      if(isReadonly) { // (13)
        console.warn(`响应式对象的属性${key}是只读的,不能删除!`)
        return true
      }
      const hadKey = Object.prototype.hasOwnProperty.call(target, key)
      const res = Reflect.deleteProperty(target, key)
      if(hadKey && res) {
        trigger(target, key, TriggerType.DELETE)
      }
      return res
   }
  })
} 
function readonly(obj) {
  return createReactive(obj, false, true)
}

在(12)(13)处,我们分别对修改和删除操作进行了处理。同时,为了与上一小节的浅响应与深响应对应,这里我们利用isReadonly开关,递归实现了深只读。接下来让我们试着用用它。

const data = {
  foo: {
    bar: 1
  }
}
const obj = readonly(data)

QQ截图20220830234127.png

到这里,其实已经完成了对只读功能的实现。功能完成了之后,自然要考虑一下,有没有可以优化的点。有了,我们无法触发setdeleteProperty就无法通过trigger来触发收集到的副作用函数。既然副作用函数不能被触发,那get时收集副作用函数的操作就显得不必要了。

get(target, key, receiver) {
  if(!isReadonly) { // (14)
    track(target, key)
  }

  if(key === 'raw') {
    return target
  }

  const res = Reflect.get(target, key, receiver)
  if(isShallow) return res
  if(typeof res === 'object' && res !== null) {
    return isReadonly ? readonly(res) : reactive(res)
  }
  return res
}

(14)处,我们仅允许非只读的响应式对象进行副作用函数的收集。

参考资料