Vue3 响应式 reactive、ref、effect 流程解析

3,787 阅读5分钟

我们在使用 vue3 响应式的时候,一般是直接在 setup 里边运用 reactive 或 ref,例如下边这种写法

<script setup>
  let user = reactive({
    age: 1
  })  
  let tree = ref(3)
</script>

然后 user、tree 就和 ui 视图形成了响应式关系。我们在改变数据的时候,视图也会随之改变。

但 vue3 的响应式是能够和视图抽离出来的,上面的方法中其实还隐藏了一层 effect 函数。而本文会先介绍 reactive、ref、effect,后边再介绍setup。

回看原始的响应式。

let a = 1
let b = a + 1
a = 2
// 我们如果想要 b 随着 a 的变化而改变,可以重新赋值一下,也就是重新执行一遍
b = a + 1

再往上一步,我们可以把赋值的过程封装成一个函数,每次 a 改变的时候,就去执行一遍。这也是一种方法呀。

let a = 1
let b
function update() {
  b = a + 1
}
a = 2
update()

而 vue3 的 reactivity 是怎么做的呢?

let a = ref(1)
let b
effect(() => {
  b = a + 1
})
a.value = 2
console.log(b) // 3

它会通过 reactive 或 ref 对原始数据做一层代理,借助 effect 收集依赖,在原始数据改变的时候,去触发依赖,也就是自动执行一遍 effect 的函数。

探索 vue3 中 reactive 的实现方式

import { track, trigger } from "./effect"
function reactive(raw){
  return new Proxy(raw, {
    get(target, key) {
      const res = Reflect.get(target, key)
​
      // 依赖收集
      track(target, key)
      return res
    },
    set(target, key, value) {
      const res = Reflect.set(target, key, value)
​
      // 触发依赖
      trigger(target, key)
      return res
    }
  })
}

我们把 effect 相关的方法独立开来,先来看 reactive 方法。

vue3 的 reactive 其实就是返回一个 Proxy , 它在 get 的时候,先运用 Reflect.get 返回对应值(相当于 target[key]),然后通过 track 方法收集依赖;它在 set 的时候,先运用 Reflect.set 设置对应值(相当于 target[key] = newValue),然后通过 trigger 触发依赖。

但是,它在这一步中,仅仅是返回了一个 Proxy 代理,收集和触发依赖的过程并没有执行。

接着我们来看 effect 方法。

let activeEffect;
class ReactiveEffect {
  constructor(fn) {
    this._fn = fn
  }
  run() {
    activeEffect = this
    this._fn()
  }
}
​
function effect(fn) {
  const _effect = new ReactiveEffect(fn)
  _effect.run()
}
​
const targetMap = new Map()
function track(target, key) {
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
​
  let dep = depsMap.get(key)
  if (!dep) {
    dep = new Set()
    depsMap.set(key, dep)
  }
​
  dep.add(activeEffect)
}
​
function trigger(target, key) {
  const depsMap = targetMap.get(target)
  const dep = depsMap.get(key)
  dep.forEach(effect => effect.run())
}

这里我们借助一个例子来说明

let son = reactive({
  age: 25
})
let dad 
effect(() => {
  dad = son.age + 25
})
son.age = 26
console.log(dad) // 51

effect 首先接收一个函数。然后 vue3 是运用了一个面向对象的思想,在 effect 中新建一个对象,对象里边把传入的 fn 存起来,在 run 方法中把 this 指向挂载到全局变量 activeEffect 中,紧接着执行一遍传入的函数 fn,也就是执行 dad = son.age + 25,执行的过程中其实就触发了 reactive 返回 proxy 中的 get 方法,它会相应执行 effect 里边的 track 方法,收集依赖。

那么收集依赖又是怎么做的呢?

首先,effect 文件中有个公共变量 targetMap ,是 Map 数据类型(允许将对象作为 key )。

接着看 track 方法。在例子中,它接受的 target 变量,也就是 targetMap 的 key 其实就是 { age: 25 } ,这个 target 再对应一个 Map 数据类型 ( depsMap ) 。

depsMap 中,以原始对象的 key ,即 age 作为自身 key,对应着一个 Set 数据类型(其元素具有唯一性),然后把 effect 中创建的依赖对象 _effect 存进去。

最终的数据结构就类似于这样:

targetMap: {
  // key 是对象,value 是 depsMap
  {age: 25} : {
    // key 是对象里边的 key, value 是 dep
    age: [ ...此处存储一个个依赖 ]
  }
}

我们继续看例子,effect 里边的逻辑已经走完了,往下执行 son.age = 26 。显而易见,这会触发 proxy 代理中的 set 方法,执行 trigger,从 depsMap 中根据 target、key 找到收集依赖的 dep,迭代 dep 执行里边的 run 方法,也即是自动执行了 dad = son.age + 25

探索 vue3 中 ref 的实现方式

我们知道 reactive 是用来代理复杂数据的,而 ref 一般用于 数字、字符串、布尔值 这类简单数据。那么我们收集依赖的时候,就不需要复杂的 targetMap 了,只需要一个 Set 数据结构就够了。

我们先把 track 和 trigger 中的最后一步提取出来

function track(target, key) {
  ......
  // dep.add(activeEffect)
  trackEffects(dep)
}
function trackEffects(dep) {
  dep.add(activeEffect)
}
​
function trigger(target, key) {
  ......
  // dep.forEach(effect => effect.run())
}
function triggerEffects(dep) {
  dep.forEach(effect => effect.run())
}

接着看 ref 的简单实现

function ref(raw) {
  return new RefImpl(raw)
}
class RefImpl {
  constructor(raw) {
    this._value = raw
    this.dep = new Set()
  }
  get value() {
    trackEffects(this.dep)
    return this._value
  }
  set value(newValue) {
    this._value = newValue
    triggerEffects(this.dep)
  }
}

再看个简单的例子

const apple = ref(1)
let banana
effect(() => {
  banana = apple.value + 2
})
apple.value = 2
console.log(banana) // 4

首先看第一步 const apple = ref(1) ,ref 函数内部新建一个 RefImpl 实例,实例内部把传入的值存为私有变量 _value ,同时新建一个存储依赖的 dep 。

get value 的时候收集依赖到 dep 里边,同时返回值;set value 的时候设置新值,同时触发依赖。同样的,这里仅仅返回实例对象,并没有执行 get 和 set 方法。

接着看 effect 里边,和 reactive 那个例子是类似的,先创建一个 ReactiveEffect 对象,再把传入的方法执行一遍。

执行的过程中,读取了 apple.value ,触发 RefImpl 的 get 方法。此时,trackEffects 方法会把 effect 新建的 ReactiveEffect 实例对象存储到 RefImpl 自身的 dep 中,这里就不是存储到 effect 文件里边的公共变量 targetMap 了。

接着看下一步 apple.value = 2 ,触发 RefImpl 的 set 方法,把自己的 dep 拿出来,迭代执行里边的 run 方法。如此便自动执行了 banana = apple.value + 2

学习参考: 崔效瑞 mini-vue