+-
Vue3 源码入门,实现简易版reactivity

Vue 3.0 中的 reative 和 Vue 2.6 中提供的一个全局 API Vue.observable 相同,都是用于让一个对象可响应,首先来对比一下他们之间的差异:

reactive Vue.observable 基于 Proxy 实现 基于 Object.defineProperty  实现 对  代理对象 进行操作 直接操作  源对象 返回一个可响应的  代理对象 返回一个可响应的  源对象

reactivity工作流程

在 Vue 3.0 中, reactivity 被独立出来,没有任何依赖,可以用于任何想做响应式数据的地方
先抛开源码中实现的复杂判断,来看一下他的主要工作流程
reactivity.png

关键方法实现

本文主要实现了其中几个关键的方法:

reactive

创建响应式对象,在 Proxy 中定义 get 及 set 捕获器,对传入的 源对象 代理对象 进行拦截处理 get 捕获到当前对象的属性也是对象,要进行递归 定义基于 WeakMap 的 reactiveMap 管理代理对象,如果传入的 object 已经有记录,直接返回 此对象代理对象 ,如果没有,按照正常流程走
/**
 * 处理器对象,定义捕获器
 */
const handlers = {
  set(target, key) {
    Reflect.set(...arguments)
    trigger(target, key)
  },
  get(target, key) {
    track(target, key)
    return typeof target[key] === 'object'
      ? reactive(target[key])
      : Reflect.get(...arguments)
  },
}

/**
 * 定义响应式对象,返回proxy代理对象
 * @param {*} object
 */
function reactive(object) {
  if (reactiveMap.has(object)) return reactiveMap.get(object)

  const proxy = new Proxy(object, handlers)

  reactiveMap.set(object, proxy)
  return proxy
}

effect

副作用,创建用于管理 effect 的栈 effectStack ,将 effect 先入栈用于依赖收集,执行一次该 effect ,进入 get 捕获阶段, 捕获完毕之后进入 finally 将其在栈中移出
/**
 * 副作用函数
 * @param {*} fn
 */
function effect(fn) {
  try {
    // 将需要执行的effect入栈,用于依赖收集过程中与key的关系对应
    effectStack.push(fn)

    // 执行该effect,进入proxy的get拦截
    return fn()
  } finally {
    // 依赖收集完毕及所有get流程走完,当前effect出栈
    effectStack.pop()
  }
}

track

effect 执行后数据触发 get 捕获器, 在此过程中调用 track 进行依赖收集 定义 targetMap ,以 WeakMap 的方式收集依赖,管理目标对象 target 及其对应的 key  第二层用于管理 key 及其对应的 effect ,上面流程图可以看到数据的结构和层次划分
/**
 * 依赖收集
 * @param {*} target
 * @param {*} key
 */
function track(target, key) {
  // 初始化依赖Map
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }

  // 第二层依赖使用Set存放key对应的effect
  let dep = depsMap.get(key)
  if (!dep) {
    targetMap.get(target).set(key, (dep = new Set()))
  }

  // 取当前栈中的effect存入第二层依赖中
  const activeEffect = effectStack[effectStack.length - 1]
  activeEffect && dep.add(activeEffect)
}

trigger

修改在 effect 中指定过的内容时会触发 set 捕获器,在此过程中 trigger 负责执行当前 target 下 key 对应的 effect ,完成响应式的过程
/**
 * 触发响应,执行effect
 * @param {*} target
 * @param {*} key
 */
function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (depsMap) {
    const effects = depsMap.get(key)
    effects && effects.forEach(run => run())
  }
}

computed

这里采用简单粗暴的方式,直接返回一个 effect 
/**
 * 计算属性
 * @param {*} fn 
 */
function computed(fn) {
  return {
    get value() {
      return effect(fn)
    },
  }
}

效果展示

对象属性响应式,多层嵌套

const object = {
  o: {
    a: 1
  }
}  
const proxy = reactive(object)

effect(() => {
  console.log(`proxy.o.a: ${proxy.o.a}`)
})
首次调用打印一次,重新赋值后再次响应,调用 effect 

1.jpg

响应式调色器

配置响应式对象,指定其rgb属性,顺便测一下 computed 
const object = {
  r: 0,
  g: 0,
  b: 0
}
const proxy = reactive(object)
const computedObj = computed(() => proxy.r * 2)

effect(() => {
  const { r, g, b } = proxy
  document.getElementById('r').value = r
  document.getElementById('b').value = b
  document.getElementById('g').value = g
  document.getElementById(
    'color'
  ).style.backgroundColor = `rgb(${r},${g},${b})`
  document.getElementById('color_text').innerText = `rgb:${r},${g},${b}`

  const { value } = computedObj
  document.getElementById(
    'computed_text'
  ).innerText = `computed_text: r*2=${value}`
})
拖动 rgb 3个 range 时各自的变化会体现在颜色块上

2.jpg

对象依赖关系 targetMap 结构如下

3.jpg

reactiveMap 结构如下

4.jpg

总结

通过上述内容可以了解到 Vue 3.0 中的响应式原理

reactive 创建响应式对象 effect 副作用,调用自身收集依赖,数据变更后重新调用该函数 track 依赖收集 trigger 触发依赖中对应的 effect  computed 计算属性,对应属性值变更调用其 effect 

过程中还能更加熟悉一些前置基础知识

代理与反射: Proxy  Reflect  JavaScript 标准内置对象: WeakMap  Map  Set  语句执行使用技巧: try  finally 

参考资料

完整代码传送门 Proxy Reflect WeakMap Map Set 语句执行 Vue 3.0 源码