Skip to main content

Vue Reactivity 响应式库

@vue/reactivity(下称 VueReactivity) 是 Vue3 内部实现响应式编程的包。它和很多响应式编程的库一样,都提供类似的 API 和实现原理。

Vue 组件中的 data 从组件挂载开始,就通过这个库赋予了响应式结构,之后的 set 操作都会触发组件渲染动作。

实现原理

VueReactivity 和很多响应式库都有相似的实现原理:依赖追踪 + Proxy。

  • 依赖追踪是隐式地找出观察者需要的依赖,并在依赖变化时通知观察者。
  • Proxy 是数据代理 API,可以给目标对象 get、set 等操作装饰一些额外的功能。

依赖追踪

依赖追踪在单线程下的 JavaScript 很容易实现。单线程可以保证某一时刻只能操作一个对象,那么在出现对象操作时,可以把当前所处的上下文记下来,当上下文结束时,就可以知道这个上下文访问了哪些对象,并观察它们。

一般这个上下文指的是某个函数。而操作目标对象时,需要把这个对象加入到当前的依赖列表。

let dependencies = []

function trackDeps(fn: Function) {
  dependencies = []
  fn()
  return Array.from(dependencies)
}

// user-code
let person = { name: 'luo' }

const trackedDeps = trackDeps(() => {
  dependencies.push(person)
  console.log('person name is ' + String(person.name))
})
// [person]

Proxy

注意上方的例子 user-code 处,可以看到这个函数必须调用 dependencies.push(person) 才能把依赖记下来。而 Proxy 可以为常规的对象操作施加额外的操作,这些操作对用户并不可见。这样就可以实现用户在不需要感知的情况下,记下需要的依赖。

let dependencies = []

function trackDeps(fn: Function) {
  dependencies = []
  fn()
  return Array.from(dependencies)
}
function makeProxy(target) {
  return new Proxy(target, {
    get: (obj, key) => {
      // 装饰额外的操作
      dependencies.push(target)
      // 原本的读 name 操作
      return Reflect.get(obj, key)
    }
  })
}

// user-code
let person = makeProxy({ name: 'luo' })

const trackedDeps = trackDeps(() => {
  // 用户定义函数中,不再需要显式通知依赖
  console.log('person name is ' + String(person.name))
})
// [person]

在具体的实现中还需要处理缓存、数组等更为复杂的情况。这里只描述核心原理。

概念 & Snippet

Ref

最细粒度的响应式对象,可以用来给原始类型装箱拆箱。

let num = ref(1)
unref(num)
// 1
num.value 
// 1
num.value = 2
// 2

Reactive

只能包装对象的响应式对象,只能为普通对象、Array、Map、Set、WeakMap、WeakSet。

let person = reactive({ name: 'luo' })
person.name
// luo
person.name = 'al1ce'
// al1ce

Effect

副作用是对依赖有反应的观察者对象,带有一个回调函数,函数执行后会产生依赖,依赖变化时反过来触发回调。会对任何响应式对象有反应。

let dep1 = reactive({ name: 'appbar' })
let dep2 = reactive({ age: 17 })
let dep3 = reactive(['react', 'vue', 'vite', 'mobx'])

effect1 = effect(() => {
  console.log('dep1 name: ' + dep1.name)
})
effect2 = effect(() => {
  console.log('dep2 age: ' + dep2.age)
})
effect3 = effect(() => {
  console.log('dep3 skills: ' + dep3.join(','))
})

// 对象 setter 触发副作用回调
dep1.name = 'apptitle'
dep2.age = 17.9
dep3.push('rollup')

// 取消观察者
stop(effect1)
stop(effect2)
stop(effect3)

Computed

特殊的副作用,它对依赖变化产生反应的同时,自己本身也可以是一个依赖。

let timer = {
  second: ref(0),
  handle: -1,
  start: () => timer.handle = setInterval(() => timer.second.value++, 1000),
  stop: () => clearInterval(timer.handle),
}

let formatted = computed(() => String(timer.second.value) + ' second')
let printEffect = effect(() => console.log(formatted.value))

// 开始,10秒后取消任务
timer.start()
effect(() => {
  if (timer.second.value == 10) {
    timer.stop()
    stop(printEffect)
  }
})

附录

Github vuejs/core

VueReactivity 可以脱离 Vue 应用单独使用。