Vue3 源码内参<二>Reactivity核心

449 阅读4分钟

Reactivity 核心

前言

本节代码演示地址,配合源码食用更开胃噢!

Vue3 最核心的就是响应式机制的变化,那么和 Vue2 响应式的区别是什么?

  • 颗粒度更细致,增加静态节点、事件侦听缓存机制
    • diff 算法就不会去 patch
  • 使用 proxy 来替代 Object.defineProperty()
    • 两者的优劣对比
    • 例如 proxy 多达 13 种拦截方式,且可以对对象进行逐一监控。而 Object.defineProperty() 是对整个对象。等等

图解

本文阅读时间大约为12分钟,请听笔者娓娓道来。

实现步骤

Vue3 响应式就是通过 get(取值操作) 的时候收集依赖,set(赋值操作) 的时候触发依赖。

咱们目前只实现核心部分,不涉及边缘 case

创建 proxy 对数据进行代理

需要实现的单测

// effect.spec.ts
it('effect', () => {
    // reactive 核心:get 收集依赖 set 触发依赖
    const user = reactive({
      age: 10
    })
});
export function reactive(raw) {
  return new Proxy(raw, {
    get(target, key) {
      const res = Reflect.get(target, key)
      // Proxy 要和 Reflect 配合使用
      // Reflect.get 中 receiver 参数,保留了对正确引用 this(即 admin)的引用,该引用将 Reflect.get 中正确的对象使用传递给 get
      // 不管 Proxy 怎么修改默认行为,你总可以在 Reflect 上获取默认行为
      track(target, key)
      return res
    },
    set(target, key, value) {
      // set 操作是会放回 true or false
      // set() 方法应当返回一个布尔值。
      // 返回 true 代表属性设置成功。
      // 在严格模式下,如果 set() 方法返回 false,那么会抛出一个 TypeError 异常。
      const res = Reflect.set(target, key, value)
      trigger(target, key)
      return res
    }
  })
}

收集依赖

Vue3 是通过 effect 函数来收集与响应式变量相关的 fn。

需要实现的单测

describe('test effect', () => {
  it('effect', () => {
    const user = reactive({
      age: 10
    })
    let nextAge
    effect(() => {
      nextAge = user.age + 1
    })
    expect(nextAge).toBe(11)
  });
});

实现 effect 函数,并将传入的函数执行。

export class ActiveEffect {
  private _fn: any
  constructor(fn) {
    this._fn = fn
  }
  run() {
    this._fn()
  }
}

export function effect(fn) {
  // 通过构造 ActiveEffect 来提供多种属性和方法
  const _effect = new ActiveEffect(fn)
  _effect.run()
}

当函数执行nextAge = user.age + 1的时候,是要先 get 到 user.age 的值,然后再 set 给 nextAge。

export function reactive(raw) {
  return new Proxy(raw, {
    get(target, key) {
      // 拿到 user.age 的值
      const res = Reflect.get(target, key)
      // 收集依赖
      track(target, key)
      return res
    },
  })
}

实现 track,Vue3 是通过 Map 和 Set 对 fn 和 变量 进行收集处理。Map 的好处就是,键可以是对象,而 Set 能保证 fn 唯一。

举个🌰

// 例如你定义了这样一个变量
const foo = reactive({num:1,age:18})
// 在 Vue3 中是这样保存的
targetMap={
  {num:1,age:18}:{
     num:(fn1,fn2),
     age:(fn1,fn2)
  }
}
// 如果有多个,如下
targetMap(weakMap) = {
     target1(Map): {
       key1(dep_Set): (fn1,fn2,fn3...)
       key2(dep_Set): (fn1,fn2,fn3...)
     },
    target2(Map): {
       key1(dep_Set): (fn1,fn2,fn3...)
       key2(dep_Set): (fn1,fn2,fn3...)
       },
}
// 定义全局变量 targetsMap
// WeakSet 的好处就是 WeakSet 中的对象都是弱引用
const targetsMap = new WeakSet()
// 收集依赖
export function track(target, key) {
  // reactive 传入的是一个对象 {}
  // 收集关系: targetsMap 收集所有依赖 然后 每一个 {} 作为一个 depsMap
  // 再把 {} 里面的每一个变量作为 dep(set 结构) 的 key 存放所有的 fn
  let depsMap = targetsMap.get(target)
  // 不存在的时候 要先初始化
  if (!depsMap) {
    depsMap = new Map()
    targetsMap.set(target, depsMap)
  }
  let dep = depsMap.get(key)
  if (!dep) {
    dep = new Set()
    depsMap.set(key, dep)
  }

  // 要存入的是一个 fn,这个 fn 是 effect 中传入并执行的函数
  // 所以要利用一个全局变量
  dep.add(activeEffect)
}

let activeEffect
export class ActiveEffect {
  ...
  run() {
    // === 新增 ===
    activeEffect = this
  }
}

触发依赖

刚刚咱们实现了触发 get 的时候收集依赖,现在来实现触发 set 的时候触发依赖。

需要实现的单测

describe('test effect', () => {
  it('effect', () => {
    // update
    // user.age++ => user.age = use.age + 1
    user.age++
    expect(nextAge).toBe(12)
  });
});
export function reactive(raw) {
  return new Proxy(raw, {
    ...
    set(target, key, value) {
      const res = Reflect.set(target, key, value)
      trigger(target, key)
      return res
    }
  })
}
// 触发依赖
// 我们只需要通过相应的 dep,然后遍历执行 fn,所有的依赖的值就会改变
export function trigger(target, key) {
  let depsMap = targetsMap.get(target)
  let dep = depsMap.get(key)
  for (const effect of dep) {
    effect.run()
  }
}

总结

通过 TDD 单测驱动学习,更有效的让你了解运行的机制。Vue3 的源码内也提供了大量的测试。本节,咱们大概的讲述了收集依赖和触发依赖的过程,本节调试代码链接在文章开头。学习源码一定要多思考,为什么会这么做?这样做有什么好处?等等。最后,推荐大家,学习 Vue3 源码时,可以跟着 mini-vue3 作者一起敲。有想加群的小伙伴,评论区联系我呀!(没人带,举步维艰!有人带,事半功倍!)