Vue nextTick实现原理、源码解析

1,727 阅读5分钟

为什么Vue有一个API叫nextTick?

Vue.nextTick 的原理和用途

用法:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。 Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。

具体来说,异步执行的运行机制如下。

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。

Vue 在修改数据后,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。

用途——应用场景:需要在视图更新之后,基于新的视图进行操作。 可以看这篇:Vue.nextTick 的原理和用途

vue中nextTick的源码分析

vue中nextTick源码(vue\src\core\util\next-tick.js)

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false

const callbacks = []
let pending = false

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

let timerFunc  // timerFunc函数是重点!
// task的执行优先级
// Promise -> MutationObserver -> setImmediate -> setTimeout

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]'
)) {
  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)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

// nextTick 主函数
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

nextTick函数

nextTick主函数

var callbacks = [];
var pending = false;
function nextTick(cb, ctx) {  //cb?: Function, ctx?: Object
    var _resolve;
    callbacks.push(function() { 
        if (cb) {
            try {
                cb.call(ctx);
            } catch (e) {
                handleError(e, ctx, 'nextTick');
            }
        } else if (_resolve) {
            _resolve(ctx);
        }
    });
    if (!pending) {  
        pending = true;
        timerFunc();  
    }
    if (!cb && typeof Promise !== 'undefined') {  
        return new Promise(function(resolve) {
            _resolve = resolve;
        })
    }
}

nextTick函数中通过参数cb传入的函数,包装后push到callbacks数组中。变量pending是用来保证执行一个事件循环中只执行一次timerFunc()函数。最后if (!cb && typeof Promise !== 'undefined'),判断参数cb不存在(即没有参数再传入了),且浏览器支持Promise,就会return一个Promise实例对象。例如 nextTick().then(() => {}),当 _resolve 函数执行,就会执行 then 的逻辑中。

通过参数cb传入的函数是如何包装的?

当参数cb有值,在try语句中执行cb.call(ctx),参数ctx是传入函数的参数。如果执行失败错误会被catch捕获。如果参数cb没有值。执行_resolve(ctx),因为cb不存在时,会return一个Promise实例对象,那么执行_resolve(ctx),就会执行then的逻辑。

  • timerFunc()函数 来看一下timerFunc()函数,先只看用 Promise 创建一个异步执行的 ptimerFunc函数。其中timerFunc 函数就是用各种异步执行的方法调用 flushCallbacks 函数。
let timerFunc;
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {   
    p.then(flushCallbacks)  //异步执行方法调用flushCallbacks
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} 
  • flushCallbacks函数。
function flushCallbacks () {    
  pending = false 
  const copies = callbacks.slice(0) 
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

执行pending=false使下个事件循环中nextTick函数中能调用timerFunc函数。执行const copies = callbacks.slice(0)是把要异步执行的函数集合 callbacks克隆到常量copies,然后把callbacks清空。再遍历copies执行每一项函数。

总结一下nextTick函数的逻辑

定义了一个callbacks数组来模拟事件队列,通过参数cb传入的函数经过一个函数包装,在这个包装过程中会执行传入的函数,处理执行失败的情况,以及参数cb不存在的情景,然后添加到callbacks数组中。再调用timerFunc函数,该函数就是用各种异步执行的方法调用flushCallbacks 函数,在flushCallbacks 函数中拷贝callbacks中的每个函数,并执行。定义了一个变量 pending来保证一个事件循环中只调用一次 timerFunc 函数。

那么其中的关键还是怎么定义 timerFunc 函数。因为在各浏览器下对创建异步执行函数的方法各不相同,要做兼容处理,下面来介绍一下各种方法。

Promise 创建异步执行函数

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {   
  //timerFunc函数就是用各种异步执行的方法调用flushCallbacks函数。
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} 

首先通过isNative方法判断浏览器是否支持Promise,其中 typeof Promise 支持的话为 function,故该条件满足,直接返回一个resolved状态的 Promise 对象。在 timerFunc 函数中执行 p.then(flushCallbacks),会直接执行 flushCallbacks 函数,在其中会遍历去执行每个 nextTick 传入的函数,因 Promise 是个微任务 (micro task)类型,故这些函数就变成异步执行了。执行 if (isIOS) { setTimeout(noop)} 来在 IOS 浏览器下添加空的计时器强制刷新微任务队列。

isNative方法先判断浏览器是否支持Promise,另一个条件,当 Ctor 是函数类型时,执行 /native code/.test(Ctor.toString()),检测函数 toString 之后的字符串中是否带有 native code 片段,那为什么要这么监测。这是因为这里的 toString 是 Function 的一个实例方法,如果是浏览器内置函数调用实例方法 toString 返回的结果是function Promise() { [native code] }

function isNative(Ctor) {
    return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}

调用timerFunc函数时,通过 p.then(flushCallbacks) 会直接执行 flushCallbacks 函数,在其中会遍历去执行每个 nextTick 传入的函数,因 Promise 是个微任务(micro task)类型,故这些函数就变成异步执行了。

MutationObserver 创建异步执行函数

if (!isIE && typeof MutationObserver !== 'undefined' &&
    (isNative(MutationObserver) ||
    MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
    var counter = 1;
    var observer = new MutationObserver(flushCallbacks);
    var textNode = document.createTextNode(String(counter));
    observer.observe(textNode, {
        characterData: true
    });
    timerFunc = function() {
        counter = (counter + 1) % 2;
        textNode.data = String(counter);
    };
    isUsingMicroTask = true;
}

创建并返回一个新的 MutationObserver,并且把 flushCallbacks 作为回到函数传入,它会在指定的 DOM 发生变化时被调用,在其中会遍历去执行每个 nextTick 传入的函数,因为MutationObserver 是个微任务 (micro task)类型,故这些函数就变成异步执行了。

setImmediate 创建异步执行函数

if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} 

setImmediate 只兼容 IE10 以上浏览器,其他浏览器均不兼容。其是个宏任务 (macro task),消耗的资源比较小

setTimeout 创建异步执行函数

timerFunc = () => {
    setTimeout(flushCallbacks, 0);
  }

兼容 IE10 以下的浏览器,创建异步任务,其是个宏任务 (macro task),消耗资源较大。

参考文献1:你真的理解$nextTick么

参考文献2:Vue源码——nextTick实现原理