手写Vue2源码(六)—— 异步更新及nextTick

743 阅读3分钟

前言

通过手写Vue2源码,更深入了解Vue; 在项目开发过程,一步一步实现Vue核心功能,我会将不同功能放到不同分支,方便查阅; 另外我会编写一些开发文档,阐述编码细节及实现思路; 源码地址:手写Vue2源码

为何需要进行异步更新

上一篇我们介绍了利用Vue观察者模式实现数据驱动视图,视图的更新通过调用的watcher.update()方法实现;但是更新过程存在优化空间。试想一下,如果短时间内某一数据修改了很多次,是否有必要每次修改都进行视图更新,这必然会造成性能浪费。

改写watcher

watcher要怎么优化:

  1. update()更新需要异步执行
  2. 单一事件循环中,同一watcher只更新一次,即保证watcher的唯一性
// src/observer/watcher
import { queueWatcher } from "./scheduler";
export default class Watcher {
  // 更新当前watcher相关的视图
  // Vue中的更新是异步的
  update() {
    // 每次watcher进行更新的时候,可以让他们先缓存起来,之后再一起调用
    // 异步队列机制
    queueWatcher(this);
  }

  // 真正更新视图的方法
  run() {
    this.getter.call(this.vm)
  }
}

queueWatcher实现队列机制

watcher.update()中调用queueWatcher(watcher)方法,queueWatcher主要做三件事:

  1. 创建一个任务数组,存放不同的watcher
  2. 将watcher放入任务数组中,鉴别watcher的唯一性(相同watcher不重复push)
  3. 调用nextTick(flushSchedulerQueue),将任务数组清空(调用每个watcher的run()
// src/observer/scheduler.js
import { nextTick } from "../util/next-tick";

let queue = [];
let has = {}; // 维护存放了哪些watcher

/**
 * queueWatcher逻辑:
 * 1. 对watcher去重(有相同watcher的情况下,不重复push)
 * 2. 防抖:一段时间内只执行一次的更新(遍历所有watcher,执行watcher.run())
 */
export function queueWatcher(watcher) {
  const id = watcher.id;

  // watcher去重,即相同watcher只push一次
  if (!has[id]) {
    //  同步代码执行 把全部的watcher都放到队列里面去
    queue.push(watcher);
    has[id] = true;

    // 开启一次异步更新操作,批处理(防抖)
    // 进行异步调用
    nextTick(flushSchedulerQueue);
  }
}

function flushSchedulerQueue() {
  for (let index = 0; index < queue.length; index++) {
    // 调用watcher的run方法,执行真正的更新操作
    queue[index].run();
  }

  // 执行完之后清空队列
  queue = [];
  has = {};
}

nextTick实现异步队列

nextTick()主要做两件事:

  1. 创建一个任务队列callbacks
  2. 将所有调用nextTick(cb)的回调函数cb放入任务队列callbacks中,在微任务中去清空这个队列(微任务会在同步任务执行完之后执行)
// src/util/next-tick.js
const callbacks = [];
function flushCallbacks() {
  callbacks.forEach((cb) => cb());
  waiting = false;
}
let waiting = false;
/**
 * 流程:
 * 1. watcher更新流程:
 *       ——> watcher.update()
 *       ——> queueWatcher(watcher)
 *       ——> 对watcher去重,并将watcher放到一个数组中;最后执行 nextTick(flushSchedulerQueue)(flushSchedulerQueue的作用是遍历watcher数组,调用watcher.run())
 *       ——> 将 flushSchedulerQueue 放入一个 回调函数数组callbacks 中;定义一个微任务:flushCallbacks(callbacks);
 * 2. vm.$nextTick(cb):
 *       ——> 直接会执行Vue原型上的$nextTick()方法,即nextTick(cb)方法
 *       ——> 将cb 放入 上述的回调函数数组 callbacks 中,紧接着上述的flushSchedulerQueue,在微任务中一并执行
 *       ——> 由于在flushSchedulerQueue中会执行 watcher.run() 创建真实DOM,所以可以在$nextTick()回调中获取到最新DOM节点
 * 
 * 总结:
 * 1. callbacks 中包含 flushSchedulerQueue,以及$nextTick()的回调
 * 2. dep.subs中每个watcher执行update时,最后都会执行nextick,
 * 3. 执行nextick是否会创建微任务,取决于上一个微任务是否完成
 * 4. 一轮事件循环中,flushCallbacks只会执行一次
 */
export function nextTick(cb) {
  callbacks.push(cb);

  if (!waiting) {
    // 异步执行callBacks
    Promise.resolve().then(flushCallbacks);
    waiting = true;
  }
}

$nextTick 实现

$nextTick是定义在vue原型上的,它的具体实现就是上面的nextTick方法; 在原型上挂载$nextTick

// src/render.js
import { nextTick } from "./util/next-tick";
export function renderMixin(Vue) {
  // 挂载在原型的nextTick方法 可供用户手动调用
  Vue.prototype.$nextTick = nextTick;
}

总结

为何能在$nextTick中获取到选然后的DOM?

watcher的更新视图操作和$nextTick(cb)的回调函数都会放到一个callbacks异步任务队列中,待同步任务执行完成后一并执行;watcher更新视图会创建新的DOM,所以在$nextTick(cb)中可以获取到新的DOM

为何在UI还没完全渲染完成,就能拿到DOM?

$nextTick()回调中获取的是内存中的DOM,不关心UI有没有渲染完成

系列文章