读过源码,相信大家都知道 Vue 异步更新的大概流程:依赖收集结束之后,当响应式数据发生变化 -> 触发 setter 执行 dep.notify -> 让 dep 通知 自己收集的所有 watcher 执行 update 方法 -> watch.update 调用 queueWatcher 将自己放到 watcher 队列 -> 接下来调用 nextTick 方法将刷新 watcher 队列的方法放到 callbacks 数组 -> 然后将刷新 callbacks 数组的方法放到浏览器的异步任务队列 -> 待将来执行时最终触发 watcher.run 方法,执行 watcher.get 方法。

实现

接下来会完整实现 Vue 的异步更新队列,让你彻底理解 Vue 的异步更新过程都发生了什么。

Watcher

/src/watcher.js

// 用来标记 watcher
let uid = 0

**
 * @param {*} cb 回调函数,负责更新 DOM 的回调函数
 * @param {*} options watcher 的配置项
 */
export default function Watcher(cb, options = {}, vm = null) {
  // 标识 watcher
  this.uid = uid++
  // ...
}

复制代码

watcher.update

/src/watcher.js

/**
 * 响应式数据更新时,dep 通知 watcher 执行 update 方法,
 * 让 update 方法执行 this._cb 函数更新 DOM
 */
Watcher.prototype.update = function () {
  if (this.options.lazy) { // 懒执行,比如 computed 计算属性
    // 将 dirty 置为 true,当页面重新渲染获取计算属性时就可以执行 evalute 方法获取最新的值了
    this.dirty = true
  } else {
    // 将 watcher 放入异步 watcher 队列
    queueWatcher(this)
  }
}

复制代码

watcher.run

/src/watcher.js

/**
 * 由刷新 watcher 队列的函数调用,负责执行 watcher.get 方法
 */
Watcher.prototype.run = function () {
  this.get()
}

复制代码

异步更新队列

/src/asyncUpdateQueue.js

/**
 * 异步更新队列
 */

// 存储本次更新的所有 watcher
const queue = []

// 标识现在是否正在刷新 watcher 队列
let flushing = false
// 标识,保证 callbacks 数组中只会有一个刷新 watcher 队列的函数
let waiting = false
// 存放刷新 watcher 队列的函数,或者用户调用 Vue.nextTick 方法传递的回调函数
const callbacks = []
// 标识浏览器当前任务队列中是否存在刷新 callbacks 数组的函数
let pending = false

复制代码

queueWatcher

/src/asyncUpdateQueue.js

/**
 * 将 watcher 放入队列
 * @param {*} watcher 待会儿需要被执行的 watcher,包括渲染 watcher、用户 watcher、computed
 */
export function queueWatcher(watcher) {
  if (!queue.includes(watcher)) { // 防止重复入队
    if (!flushing) { // 现在没有在刷新 watcher 队列
      queue.push(watcher)
    } else { // 正在刷新 watcher 队列,比如用户 watcher 的回调函数中更改了某个响应式数据
      // 标记当前 watcher 在 for 中是否已经完成入队操作
      let flag = false
      // 这时的 watcher 队列时有序的(uid 由小到大),需要保证当前 watcher 插入进去后仍然有序
      for (let i = queue.length - 1; i >= 0; i--) {
        if (queue[i].uid < watcher.uid) { // 找到了刚好比当前 watcher.uid 小的那个 watcher 的位置
          // 将当前 watcher 插入到该位置的后面
          queue.splice(i + 1, 0, watcher)
          flag = true
          break;
        }
      }
      if (!flag) { // 说明上面的 for 循环在队列中没找到比当前 watcher.uid 小的 watcher
        // 将当前 watcher 插入到队首 
        queue.unshift(watcher)
      }
    }
    if (!waiting) { // 表示当前 callbacks 数组中还没有刷新 watcher 队列的函数
      // 保证 callbacks 数组中只会有一个刷新 watcher 队列的函数
      // 因为如果有多个,没有任何意义,第二个执行的时候 watcher 队列已经为空了
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

复制代码

flushSchedulerQueue

/src/asyncUpdateQueue.js

/**
 * 负责刷新 watcher 队列的函数,由 flushCallbacks 函数调用
 */
function flushSchedulerQueue() {
  // 表示正在刷新 watcher 队列
  flushing = true
  // 给 watcher 队列排序,根据 uid 由小到大排序
  queue.sort((a, b) => a.uid - b.uid)
  // 遍历队列,依次执行其中每个 watcher 的 run 方法
  while (queue.length) {
    // 取出队首的 watcher
    const watcher = queue.shift()
    // 执行 run 方法
    watcher.run()
  }
  // 到这里 watcher 队列刷新完毕
  flushing = waiting = false
}

复制代码

nextTick

/src/asyncUpdateQueue.js

/**
 * 将刷新 watcher 队列的函数或者用户调用 Vue.nextTick 方法传递的回调函数放入 callbacks 数组
 * 如果当前的浏览器任务队列中没有刷新 callbacks 的函数,则将 flushCallbacks 函数放入任务队列
 */
function nextTick(cb) {
  callbacks.push(cb)
  if (!pending) { // 表明浏览器当前任务队列中没有刷新 callbacks 数组的函数
    // 将 flushCallbacks 函数放入浏览器的微任务队列
    Promise.resolve().then(flushCallbacks)
  }
}

复制代码

flushCallbacks

/src/asyncUpdateQueue.js

/**
 * 负责刷新 callbacks 数组的函数,执行 callbacks 数组中的所有函数
 */
function flushCallbacks() {
  // 表示浏览器任务队列中的 flushCallbacks 函数已经被拿到执行栈执行了
  // 新的 flushCallbacks 函数可以进入浏览器的任务队列了
  pending = false
  while(callbacks.length) {
    // 拿出最头上的回调函数
    const cb = callbacks.shift()
    // 执行回调函数
    cb()
  }
}

复制代码

总结

到这里 精通 Vue 系列 就要结束了,现在我们再回头看下整个系列:从 Vue 源码解读 开始到现在的 手写 Vue,总共 20 篇文章。如果你是从头到尾跟下来的,相信我们最初定的目标早已实现,这会儿你是否可以在自己的简历上写上:精通 Vue 源码原理。