Vue3中的响应式API是怎么实现的?(computed篇)

206 阅读4分钟

Vue的响应式原理已经有很多人说过了,读取时响应式数据会收集对应的副作用函数,设置响应式数据时触发副作用函数执行。这是最基本的Vue响应式工作原理,但是Vue借助响应系统实现了许多API,如watch(),watchEffect(),computed(),ref()和reactive()等。然后我就通过几篇文章介绍一下这几个API的实现原理。

在开始之前先简单实现一个effect函数,可以用它来注册副作用函数:

let activateEffect
function effect(fn, options = {}) {
  const effectFn = () => {
      activateEffect = effectFn
      // 返回fn的结果
      const res = fn()
      return res
  }
  // 将options添加到effectFn上
  effectFn.options = options
  // 保存所有与该副作用函数相关的依赖
    effectFn.deps = []
  // 如果option中不指定懒执行
  if (!options.lazy) {
    effectFn()
  }
  return effectFn
}

effect接受两个参数,effect的回调和options,如果options指定了懒执行那么就只返回包装函数effectFn,effectFn返回回调的执行结果res。

然后实现收集副作用函数的track:

// 使用weakMap来保存每个target对应的依赖映射,target回收后对应的依赖映射也一并回收
let bucket = new WeakMap()
// 追踪依赖
function track(target, key) {
  if (!activateEffect) return
  let depsMap = bucket.get(target)
  //找不到就新建一个,注意是使用的Map
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }
  let deps = depsMap.get(key)
  //找不到就新建一个
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }
  // 收集全局唯一的activeEffect
  deps.add(activateEffect)
  // 让每个副作用函数都能知道时谁收集了自己,用于解除跟踪
  activateEffect.deps.push(deps)
}

这个函数用来在读取响应式数据时收集activeEffect。

和触发副作用函数重新执行的trigger:

// 触发执行
function trigger(target, key) {
  // 寻找target
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(target)
  const effectToRun = new Set()
  // 如果注册activeEffect的过程中,改变了响应式数据,会导致又track又trigger的过程,引发无线递归。所以只用去掉activateEffect的副作用函数
  effects &&
    effects.forEach((fn) => {
      if (fn !== activateEffect) {
        effectToRun.add(fn)
      }
    })

  effectToRun.forEach((fn) => {
    // 如果注册副作用函数的过程中添加了scheduler,那么把fn作为参数调用scheduler
    if (fn.options.scheduler) {
      fn.options.scheduler(fn)
    } else {
      fn()
    }
  })
}

trigger在设置响应式数据时触发重新执行,但是如果副作用函数设置了scheduler时,那么以依赖为参数触发sheduler。

还有一点值得注意就是,如果track的过程中改变了响应式数据,会导致trigger执行,但是这个副作用函数现在正在首次执行中(所以他也是activeEffect),所以导致了递归,解决办法就是触发副作用函数时去掉activeEffect。

Computed:

先来看看computed(),computed这个API接受一个响应式数据的getter,computed的返回值在getter依赖的响应式数据变更后返回新的值:

image.png computed还有一个特点是只有在读取它的返回值的时候,这个getter才会执行,所以可以推断出它是一个懒执行的effect:

// options中指定lazy时会直接返回包装函数effectFn,执行它可以得到getter的结果
const effectFn = effect(getter, { lazy:true })

因为我们知道computed返回值也是一个key为value的getter,所以可以给出computed的简单实现:

function Computed(getter) {
    const effectFn = effect(getter, { lazy:true })  
    const obj = {
        get value() {
            return effect()
        }
    }  
    return obj
}

这样就实现了通过computed的返回值obj的obj.value访问我们想要的getter的返回值,但是这么做还不够,一是因为getter是有缓存机制的,而且computed的返回值也是响应式,会根据getter响应式数据的变化而变化。

为了实现缓存机制,需要在getter中的响应式数据不变时,只读取缓存,在getter中的响应式数据变化时重新计算。另外还需要实现computed返回值的响应式,读取obj.value的时候收集副作用函数,getter的数据更改的时候,触发副作用函数:

function Computed(getter) {
  let value
  // 用来判断是否需要重新计算
  let _dirty = true

  const effectFn = effect(getter, {
    lazy: true,
    scheduler() {
      // 如果setter的数据改变了,会触发scheduler,_dirty更新为true,代表需要重新计算
      // 同时手动触发trigger,让依赖于computed返回值的副作用函数重新执行得到新值
      _dirty = true
      // trigger时会重新读取obj.value,拿到新值
      trigger(obj, 'value')
    }
  })

  const obj = {
    get value() {
    // 只有数据为“脏”时,才重新计算value
      if (_dirty) {
      // 通过执行effectFn()收集getter,也就相当于收集scheduler,
      // 而scheduler又会触发computed返回值的依赖
        value = effectFn()
        _dirty = false
      }
    // 读取computed返回值时,收集对应的依赖
    // 只要在副作用函数中读取过computed返回值,就会被收集,无论值是否为“脏”
      track(obj, 'value')
      return value
    }
  }

  return obj
}

这样就实现了computed的三个功能,读取时执行getter拿到值、值的缓存、computed返回值的响应式。 如果值不为脏,那么读取时只会读取缓存,只有响应式数据变化了,才会获得新值。另外setter依赖的响应式数据的变化,不会执行setter本身,而是根据调度系统执行trigger,trigger中运行computed返回值的副作用函数,重新读取computed返回值,然后重新计算effectFn得到结果。实现响应式。

剩余API会在后篇讲解。

参考:《Vue.js设计与实现》