vue2.x 中的数据异步更新和 nextTick 方法解析

前言

众所周知,vue 中的更新时异步的,比如 this.msg = xxx,你看起来他是立马更新了,其实并没有。它会异步执行,接下来就来看看怎么实现的吧。

先上图

image.png

首先从数据改动开始说起

  • 调用this.msg = xxx 数据发生变更
  • 在数据初始化阶段已经收集了依赖的watcher 到 dep 中, 执行 dep.notify 通知watcehr变更
  • notify 方法遍历调用 所有以来的watcherupdate 方法,把当前watcher 实例放入 queueWatcher 函数中执行,接下来就是异步更新的关键了,看代码

queueWatcher 函数代码 在 src\core\observer\scheduler.js

主要作用:把当前 watcher 实例添加到一个 queue 中

export function queueWatcher (watcher: Watcher) {
  // 拿到 watcher 的唯一标识
  const id = watcher.id
  // 无论有多少数据更新,相同的 watcher 只被压入一次
  // 我理解这就是为什么在一次操作中,多次更改了变量的值,但是只进行了一次页面更新的原因,
  // 同一变量 依赖它的 watcher 是一定的,所以已经存在了就不再放进watcher 队列中了,也不会走后面的逻辑
  if (has[id] == null) {
    // 缓存当前的watcher的标识,用于判断是否重复
    has[id] = true
    // 如果当前不是刷新状态,直接入队
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      // 此处能走到这儿,说明 flushSchedulerQueue 函数被执行了 watcher队列已经正在开始被更新了, 
      // 并且 在执行某个watcher.run方法的时候又触发的数据响应式更新,重新触发了 queueWatcher
      // 因为在执行的时候回有一个给 watcher 排序的操作,所以,当 watcher 正在更新时已经是排好顺序了的,此时需要插入到特定的位置,保持 watcher 队列依然是保持顺序的
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    // waiting 表示当前的 flushSchedulerQueue 还没有被执行,因为还没有重置状态, waiting 仍然 为 true
    // 所以 waiting 的意义就是 表明是否执行了flushSchedulerQueue,
    if (!waiting) {
      waiting = true
      // 直接同步刷新队列
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        // 同步执行
        flushSchedulerQueue()
        return
      }
      // 把更新队列函数放到异步队列中
      nextTick(flushSchedulerQueue)
    }
  }
}
复制代码

flushSchedulerQueue 代码在相同目录下

// 主要作用: 遍历执行每一个 watcher 的 run 方法,进而实现数据和视图的更新,并在执行完所有的 方法之后,重置状态,表示正在刷新队列的 flushing, 表示 watcher 是否存在的 has,表示是否需要执行 nexttick 的 waiting

function flushSchedulerQueue () {
    // 当方法被执行时,设置为正在刷新状态,以示可以继续执行 nextTick 方法
    flushing = true
    // 把队列中的 watcher 排个序,
      /**
       * 排序的作用:(此句照搬照抄而来)
       * 1. 保证父组件的watcher比子组件的watcher先更新,因为父组件总是先被创建,子组件后被创建
       * 2. 组件用户的watcher在其渲染watcher之前执行。
       * 3. 如果一个组件在其父组件执行期间被销毁了,会跳过该子组件。
       */
    queue.sort((a, b) => a.id - b.id)
    // 中间略去若干代码
    ...
    // 遍历  queue 中存的 所有的 watcher,执行 run 方法更新
    for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        watcher.run()
    }
    // 因为 queue 是在一个闭包中,所以当遍历执行完毕了,就把 队列清空
    queue.length = 0;
    // has 是判断 当前 watcher 是否重复,作为是否把 watcher 放进 queue 的依据
    // 此时已经执行完了 queue 中的所有 watcher了,之前已经执行过的watcher 如果发生了变更,可以重新加入了
    has = {}
    // waiting 是判断是否 执行 nextTick 的标识,当前的刷新队列已经执行完毕了,说以,可以设置为 false 了,执行下一轮的的添加异步事件队列的方法
    // flushing 是判断是否当前异步事件正在执行的标志,当前更新完毕,作为判断 watcher 入队的形式
    waiting = flushing = false
}
复制代码

nextTick 方法 源码src\core\util\next-tick.js

export function nextTick(cb ? : Function, ctx ? : Object) {
  let _resolve
  // 把执行更新操作之后的回调函数添加到队列里
  // 用try catch包装一下传进来的函数,避免使用$nextTick时,传入的回调函数出错能够及时的捕获到
  // 只要执行了nextTick函数,就把回调函数添加到回调列表里
  // 这里的 cb 回调函数就是 flushSchedulerQueue 函数,里面执行了 queue 中存放的所有的 watcher.run 方法
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // 通过pending来判断是否需要向任务队列中添加任务
  // 如果上一个清空回调列表的当flushCallbacks函数还在任务队列中,就不往任务队列中添加
  // 第一次执行时,就默认就添加一个进任务队列,一旦添加进任务队列,就表明暂时不在需要往任务队列中添加flush函数
  // 当执行了上一个 flushCallbacks 函数的时候,pending修改为false,表明可以重新添加一个清空回调列表的flush函数到任务队列了
  if (!pending) {
    pending = true
    // 这里是调用清空 callbacks 数组中方法,并执行的函数,
    timerFunc()
  }
  // $flow-disable-line
  // 判断当前环境是否支持promise,如果支持的话,可以返回一个期约对象,
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
复制代码

timerFunc() 方法,主要是做一些降级操作,实现异步的关键

timerFunc = () => { 
    Promise.resolve().then(flushCallbacks)
}
// 如果当前环境不支持的话,会进行一定的降级操作,直到最后,用 宏任务settimeout来处理
复制代码

看看 flushCallbacks, 任务就是执行了所有的 callbacks函数

function flushCallbacks() {
  // 如果开始执行了 flushCallbacks 说明,当前的异步任务已经为空了,如果此时再 nextTick 方法会添加新的 任务进去了
  pending = false
  // 拷贝一份callbacks中的所有回调函数,用于执行
  const copies = callbacks.slice(0)
  // 随即删除所有callbacks
  callbacks.length = 0
  // 当微任务队列中的flushCallbacks添加到执行栈中了,就执行callbacks中的所有的函数
  // 也就是调用执行每一个 flushSchedulerQueue 函数,然后遍历执行每一个函数
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
复制代码

基本关键变量的作用

  1. waiting: 变量,作为是否执行 nextTick ,添加 flushSchedulerQueue 方法的关键,标志着 callbacks 中 是否有 flushSchedulerQueue 方法, 比如同一个变量的改变,可能会影响多个 watcher,因为执行 flushSchedulerQueue 是异步的,遍历dep.update 先把所有的 watcher 都放入到 queue 中,也才只执行了一次 nextTick,callbacks中也只有一个方法。虽然当第一次方如 watcher 时就会执行 nexttick 把 flushSchedulerQueue方法放入callbacks 中,看起来好像已经要执行了,但是因为 queue 是闭包变量,所以,后续的变量仍然可以添加queue 中,
  2. flushing:: 表示是否正在执行 flushSchedulerQueue 方法,如果是正在执行更新方法的话,对向已经排好序的 watcher 队列中添加新的 watcher,需要把新 watcher 插入到排好序的指定的位置,这也就是为什么遍历 watdher 那块儿会 直接使用 queue.length 的原因,这个长度会发生变化。
  3. pending:: pending 是决定是否把更新 callbacks 数组的方法放入异步队列的关键,保证了异步队列中只有一个清空callbacks 的任务, 也就解释了,连续手动执行多个 $nextTick 方法不会立即执行,也还是会把他们的回调 放入 callbacks 中,然后等到任务都执行完毕了,一下把所有的 回调函数都执行掉。

参考

猜你喜欢

转载自juejin.im/post/7017039600535207972