五、vue响应式原理:调度器和懒执行

291 阅读4分钟

在前面几个章节中我们已经针对普通对象实现了一个相对全面的响应式系统,但是有些环节的执行我们希望是可控的:比如effect函数调用,我们有时候希望不要立马执行副作用函数而是拿到副作用函数在希望执行的时间执行;又比如在触发依赖集合的执行时我们也希望能可控的去执行,而不是立马遍历的执行,下面我们来具体分析

调度器

在编写vue时,我们如果在一次操作中对某个响应式对象的属性进行了多次修改,其实对应的render并不会执行多次,而是会等所有更改完成后进行一次更新,这样就大大的提升了性能。

而在我们前面实现的响应式系统中,会在每次修改后都会执行对应的收集的所有依赖函数。这里其实就要借助调度器来实现相应的功能。

那么什么是调度器呢,其实这里我们要实现的调度器就是要帮助我们来控制依赖的执行的一个工具,他的实现其实非常简单:我们只需要在依赖函数上配置一些options,在依赖执行的时候进行一些判断。如下实现

// 参数增加一个options配置对象 options = { scheduler: (effectFn) => {} }
function effect(fn, options = {}) {
  // 省略部分代码
  // 将 options 挂载到 effectFn 上
  effectFn.options = options
  // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
  effectFn.deps = []
  // 执行副作用函数
  effectFn()
}

function trigger(target, key) {
  // 省略部分代码
  effectsToRun.forEach(effectFn => {
    // 判断有没有配置调度器 有的话直接将副作用函数传给调度器控制后续操作
     if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn)
    } else {
      effectFn()
    }
  })
  // effectsToRun.forEach(effectFn => effectFn())
}

可以看到,上面我们只增加了几行代码就实现了一个简单的调度器,接下来对于处理每次修改都执行依赖的问题相信很多人都会有一些思路。这里我们利用事件循环对于异步事件的执行顺序机制来简单的实现一下(对事件循环不清楚的可以看这篇文章mp.weixin.qq.com/s/m3a6vjp8-…):

// 原始数据
const data = { foo: 1 }
// 省略部分代码

// 定义一个工作队列
const jobQueue = new Set()
const p = Promise.resolve()

let isFlushing = false
function flushJob() {
  if (isFlushing) return
  isFlushing = true
  p.then(() => {
    jobQueue.forEach(job => job())
  }).finally(() => {
    isFlushing = false
  })
}
function render() {
  console.log(dataProxy.foo)
}
effect(render, {
  scheduler(fn) {
    jobQueue.add(fn)
    flushJob()
  }
})

dataProxy.foo++
dataProxy.foo++
// 1
// 3

可以看到上面实现并不难,我们定义一个set集合用来保存将要执行的依赖,利用Promise.resolve()将set依赖集合的执行放到微任务队列中,这样我们就可以在主线程的同步计算执行完以后再去执行依赖集合,可以看到foo两次计算操作最后只打印了一次结果

懒执行

上面的调度器帮助我们实现了依赖执行的可控性,有时候我们也希望调用effect函数时可以不要立即执行effectFn,而是将其返回由我们自己控制什么时候再去执行,这里其实也很容易实现,只需要再提供一个可配置项lazy:

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
    activeEffect = effectFn
    // 在调用副作用函数之前将当前副作用函数压栈
    effectStack.push(effectFn)
    const res = fn()
    // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并还原 activeEffect 为之前的值
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
    // 将fn的执行结果返回
    return res
  }
  // 将 options 挂在到 effectFn 上
  effectFn.options = options
  // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
  effectFn.deps = []
  // 执行副作用函数
  if (!options.lazy) {
    effectFn()
  }

  return effectFn
}

上面实现可见,我们可以在options中传一个配置项lazy,其值为布尔值,如果是true就返回effectFn,值得注意的是effectFn中我们应该将fn执行的值返回。

vue中的computed和watch就是基于这两者实现的,其实他们的实现也并不复杂,这里读者可以自己尝试进行实现,下一篇将会带着大家手写一下基础的computed和watch