Vue 2.x / nextTick

483 阅读4分钟

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

前言

JS事件循环和异步执行机制在 Vue 中的一个应用场景 —— $nextTick

源码总览

看看 next-tick.js 中主要做了什么事情:

  1. 定义了三个变量,一个函数 callbacks:Array pending:Boolean flushCallbacks:Function timerFunc:undefined
  2. 进行一堆 if..else if.. 判断 (是给 timerFunc 降级赋值的过程)
  3. 抛出一个函数 nextTick
// next-tick.js 

let callbacks = [];

let pending = false;

// flushCallbacks ---------------------------------
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}


// timerFunc ---------------------------------
let timerFunc;

// 1.优先考虑Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]')) {
// 2.降级到MutationObserver
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 3.降级到setImmediate(只有IE支持)
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
// 4.以上都不支持,用setTimeout兜底
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}


// nextTick ---------------------------------
export function nextTick (cb) {
    // 把传进来的回调函数放到callbacks队列里
    callbacks.push(cb);

    // pending代表一个等待状态 等这个tick执行
    if (!pending) {
        pending = true
        timerFunc()
    }
    
    // 如果没传递回调 提供一个Promise化的调用
    if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
          _resolve = resolve
        })
    }
}

逐个瞅瞅

callbacks

存储任务队列。在 nextTick 中收集,在 flushCallbacks 中依次执行

pending

pending 代表一个等待状态

flushCallbacks 函数

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0) //Q1:为什么要拷贝?
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

循环遍历,按照队列数据结构 “先进先出” 的原则,逐一执行所有 callback 。

Q1:执行 callbacks 里的任务,为什么要拷贝出来遍历,而不是直接遍历? 如果某一个 callback 执行的时候,又一次调用了 nextTick,进而更新了 callbacks,那这个时候的执行就不是我们所期望的了。所以需要拷贝出来,并清空队列,这时再更新 callbacks 也不影响我们的循环和执行,符合预期。

timerFunc 函数

timerFunc 是在那堆 if..else if.. 中赋值的,主要是做了环境的判断和兼容处理。优先使用 Promise 异步任务,按环境逐步做降级赋值: Promise -> MutationObserver -> setImmediate -> setTimeout

Promise 分支

  • 判断环境是否支持 Promise 并且 Promise 是否为原生
  • 使用 Promise 异步调用 flushCallbacks 函数
  • 当执行环境是 iPhone 等,使用 setTimeout 异步调用 noop ,iOS 中在一些异常的 webview 中,promise 结束后任务队列并没有刷新所以强制执行 setTimeout 刷新任务队列
  • IE 不支持 Promise,其他现代浏览器最新版本已完全支持
  • 了解更多:Promise

MutationObserver 分支

  • 对非IE浏览器和是否可以使用 HTML5 新特性 MutationObserver 进行判断
  • 实例一个 MutationObserver 对象,这个对象主要是对浏览器 DOM 变化进行监听,当实例化 MutationObserver 对象并且执行对象 observe,设置 DOM 节点发生改变时自动触发回调
  • timerFunc 赋值为一个改变 DOM 节点的方法,当 DOM 节点发生改变,触发 flushCallbacks(这里其实就是想用利用 MutationObserver 的特性进行异步操作)
  • 了解更多:MutationObserver

setImmediate 分支

  • 判断 setImmediate 是否存在,目前只有最新版本的 IE 和Node.js 0.10+ 实现了该方法
  • 如果存在,传入 flushCallbacks 执行 setImmediate
  • 了解更多:window.setImmediate

setTimeout 分支

  • 当以上所有分支异步 api 都不支持的时候,使用 macro task (宏任务)的 setTimeout 执行 flushCallbacks

nextTick 函数

  1. 声明一个局部变量 _resolve
  2. 把所有回调函数压进 callbacks 中,以栈的形式的存储所有 callback
  3. 当 pending 为 false 时,执行 timerFunc 函数
  4. 当没有 callback 的时候,返回一个 Promise 的调用方式,可以用 .then 接收

简单的说,nextTick 就是先收集所有需要异步执行的任务(放到 callbacks中),然后在下一个 tick时执行 callbacks 的任务。

为什么 :为了更好的性能,将更新 DOM 操作存放在异步更新队列中,在下一个 tick 统一进行更新 DOM 操作。 如果我们每更新一次数据,Vue 就需要去更新一次 DOM 操作的话,得有多卡顿? 所以 Vue 中就采用了异步更新队列这种方式来进行优化,也就是依赖上边我们分析的 nextTick 所做的最核心的事情。

参考链接

从 Event Loop 角度解读 Vue NextTick 源码
Vue - The Good Parts: nextTick
Vue源码——nextTick实现原理