二、vue响应式原理:处理Proxy get拦截操作中的硬编码和多个对象、属性的依赖收集

174 阅读4分钟

ps:本系列文章衔接较为紧密,请先阅读前面相关文章

副作用函数收集

上一篇文章中我们在get拦截中直接将render函数添加到桶中,这明显是不合理的,我们应该自动将当前正在执行的函数添加到桶中:

// 用一个全局变量存储当前激活的 effect 函数
let activeEffect
// 定义一个帮助我们执行且帮助我们把传进来的函数赋值到全局存储变量中
function effect(fn) {
  // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
  activeEffect = fn
  // 执行副作用函数
  fn()
}

这样我们在get拦截中就可以直接将全局的副作用函数添加到桶中

// 存储副作用函数的桶
const bucket = new Set()

// 原始数据
const data = { message: 'Hello Vue' }
// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    bucket.add(activeEffect)
    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    // 把副作用函数从桶里取出并执行
    bucket.forEach(fn => fn())
  }
})
function render() {
  console.log(dataProxy.message)
}
// 此时应该使用effect来执行render函数
effect(render)

多个对象和多个key的副作用收集存储

上面我们虽然解决了get操作中的硬编码问题,但是我们依然只收集了data中的message,在开发中我们必然是会有很多个响应式对象,且每个对象中也不会只有一组key、value,所以我们要考虑怎么来收集和存储他们。

我们可以在全局定义一个桶来存放所有的数据,一般我们使用WeakMap(使用方法请移步mdn),它的key是要被进行数据劫持的对象(上述代码的data),值是一个Map对象(new Map),这个Map对象的key为被劫持对象的key(上述代码的message),值是key所收集的副作用函数(上述代码的render函数),用Set保存,防止一个副作用函数被多次添加。

至于为什么使用weakmap而不是map,因为weakmap对对象的引用为弱引用,不会影响垃圾回收机制的执行。即当我们的data被回收时候,那么weakmap中的data,和其对应的value都会被回收,这样可以防止已经被回收的对象和相对应收集的依赖继续占用空间。

get收集依赖

先说get中的收集依赖我们应该怎么做

get(target, key) {
  // 我们先获取target对应的Map
  let depsMap = bucket.get(target)
  if (!depsMap) {
    // 首次获取肯定是没有的,我们直接创建一个
    bucket.set(target, (depsMap = new Map()))
  }
  // 获取对应key的依赖集合
  let deps = depsMap.get(key)
  if (!deps) {
    // 第一次没有依然创建一个
    depsMap.set(key, (deps = new Set()))
  }
  // 拿到依赖集合后将当前正在执行的副作用函数添加到集合中
  deps.add(activeEffect)

  // 返回属性值
  return target[key]
}

这样我们就可以收集不同的响应式对象,且每一个key都有其对应的副作用函数集合

set执行依赖

上面我们已经收集好了对应的依赖,现在只需要在改变值的时候取出对应的依赖集合遍历集合即可

// 拦截设置操作
set(target, key, newVal) {
  // 设置属性值
  target[key] = newVal
  // 把副作用函数从桶里取出并执行
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  effects && effects.forEach(fn => fn())
}

这样我们就基本实现了对多个对象极其所有key的依赖函数的收集

封装

对于捕获追踪依赖和触发依赖的操作我们通常将其封装成单独的函数,分别为track和trigger

最终代码为

// 存储副作用函数的桶
const bucket = new WeakMap()

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

function track(target, key) {
  let depsMap = bucket.get(target)
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }
  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }
  deps.add(activeEffect)
}

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  effects && effects.forEach(fn => fn())
}

// 用一个全局变量存储当前激活的 effect 函数
let activeEffect
function effect(fn) {
  // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
  activeEffect = fn
  // 执行副作用函数
  fn()
}
// 测试 (当然也可以)
function render() {
  console.log(dataProxy.message)
  console.log(dataProxy.other)
}
effect(render)
dataProxy.message = 'change'
dataProxy.other = 123
// 运行结果
// Hello Vue
// test
// change
// test
// change
// 123
// 可以看见message和other都收集了render函数的依赖

上面代码并没有增加新的内容,只是对get和set中的一部分操作进行了一个简单的抽取,使代码结构更加清晰,也方便以后代码的复用。

以上基本实现了一个比较简答的响应式系统,但是其中还存在很多问题,后续将会继续探讨。读者也可以自己思考一下有哪些缺陷,该怎么解决。