浅析 vue 2.6 中的 nextTick 方法.
事件循环
JS 的 事件循环 和 任务队列 其实是理解 nextTick 概念的关键. 这个网上其实有很多优质的文章做了详细介绍, 我就简单过过了.
以下内容适用于浏览器端 JS,Node.JS 的事件循环机制并不相同.
规范中规定 task 分为两大类: task(macrotask) 和 microtask.
通常认为是 task 的任务源:
- setTimeout / setInterval
- setImmediate
- MessageChannel
- I/O
- UI rendering
通常认为是 microtask 的任务源:
- Promise
- process.nextTick
- MutationObserver
- Object.observe(已废弃)
简单概况:(这里是官方规范)
首先开始执行 script 脚本, 直到执行上下文栈为空时, 开始清空 microtask 队列里的任务, 队列嘛, 先入先出, 出一个执行一个, 清空完毕, 走事件循环.
事件循环: 不断地去取 task 队列的中的一个任务推入栈中执行, 并在当次循环里依次清空 microtask 队列里的任务, 清空之后, 可能会触发页面更新渲染(由浏览器决定).
之后重复 事件循环 步骤.
nextTick
Vue 中数据的变化到 DOM 的更新渲染是一个异步过程.
此方法便用于在 DOM 更新循环结束之后执行延迟回调.
使用方法很简单:
- // 修改数据
- vm.msg = 'Hello';
- // DOM 还没有更新
- Vue.nextTick(function() {
- // DOM 更新了
- });
- // 作为一个 Promise 使用
- Vue.nextTick().then(function() {
- // DOM 更新了
- });
源码 去除注释, 其实也只有不到一百来行, 整体还是很容易理解的.
这里划成 3 个部分介绍.
模块变量
介绍 引入的模块 和 定义的变量.
- // noop 空函数, 可用作函数占位符
- import { noop } from 'shared/util';
- // Vue 内部的错误处理函数
- import { handleError } from './error';
- // 判断是 IE/iOS / 内置函数
- import { isIE, isIOS, isNative } from './env';
- // 使用 MicroTask 的标识符
- export let isUsingMicroTask = false;
- // 以数组形式存储执行的函数
- const callbacks = [];
- // nextTick 执行状态
- 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]();
- }
- }
异步延迟函数
接下来是核心的 异步延迟函数. 这里不同的 Vue 版本采用的策略其实并不相同.
2.6 版本优先使用 microtask 作为异步延迟包装器.
2.5 版本则是 macrotask 结合 microtask. 然而, 在重绘之前状态改变时会有小问题(如 #6813 https://github.com/vuejs/vue/issues/6813 ). 此外, 在事件处理程序中使用 macrotask 会导致一些无法规避的奇怪行为(如#7109 https://github.com/vuejs/vue/issues/7109 ,#7153 https://github.com/vuejs/vue/issues/7153 ,#7546 https://github.com/vuejs/vue/issues/7546 ,#7834 https://github.com/vuejs/vue/issues/7834 ,#8109 https://github.com/vuejs/vue/issues/8109 ).
所以 2.6 版本现在又改用 microtask 了, 为什么是又呢.. 因为 2.4 版本及之前也是用的 microtask...
microtask 在某些情况下也是会有问题的, 因为 microtask 优先级比较高, 事件会在顺序事件 (如#4521 https://github.com/vuejs/vue/issues/4521 ,#6690 https://github.com/vuejs/vue/issues/6690 有变通方法) 之间甚至在同一事件的冒泡过程中触发(#6566 https://github.com/vuejs/vue/issues/6566 ).
- // 核心的异步延迟函数, 用于异步延迟调用 flushCallbacks 函数
- let timerFunc;
- // timerFunc 优先使用原生 Promise
- // 原本 MutationObserver 支持更广, 但在 iOS>= 9.3.3 的 UIwebView 中, 触摸事件处理程序中触发会产生严重错误
- if (typeof Promise !== 'undefined' && isNative(Promise)) {
- const p = Promise.resolve();
- timerFunc = () => {
- p.then(flushCallbacks);
- // iOS 的 UIWebView,Promise.then 回调被推入 microtask 队列但是队列可能不会如期执行.
- // 因此, 添加一个空计时器 "强制" 执行 microtask 队列.
- if (isIOS) setTimeout(noop);
- };
- isUsingMicroTask = true;
- // 当原生 Promise 不可用时, timerFunc 使用原生 MutationObserver
- // 如 PhantomJS,iOS7,Android 4.4
- // issue #6466 MutationObserver 在 IE11 并不可靠, 所以这里排除了 IE
- } else if (
- !isIE &&
- typeof MutationObserver !== 'undefined' &&
- (isNative(MutationObserver) ||
- // PhantomJS 和 iOS 7.x
- 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;
- // 如果原生 setImmediate 可用, timerFunc 使用原生 setImmediate
- } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
- timerFunc = () => {
- setImmediate(flushCallbacks);
- };
- } else {
- // 最后的倔强, timerFunc 使用 setTimeout
- timerFunc = () => {
- setTimeout(flushCallbacks, 0);
- };
- }
一句话总结优先级: microtask 优先.
Promise> MutationObserver> setImmediate> setTimeout
nextTick 函数
nextTick 函数. 接受两个参数:
cb 回调函数: 是要延迟执行的函数;
ctx: 指定 cb 回调函数 的 this 指向;
Vue 实例方法 $nextTick 做了进一步封装, 把 ctx 设置为当前 Vue 实例.
- export function nextTick(cb?: Function, ctx?: Object) {
- let _resolve;
- // cb 回调函数会经统一处理压入 callbacks 数组
- callbacks.push(() => {
- if (cb) {
- // 给 cb 回调函数执行加上了 try-catch 错误处理
- try {
- cb.call(ctx);
- } catch (e) {
- handleError(e, ctx, 'nextTick');
- }
- } else if (_resolve) {
- _resolve(ctx);
- }
- });
- // 执行异步延迟函数 timerFunc
- if (!pending) {
- pending = true;
- timerFunc();
- }
- // 当 nextTick 没有传入函数参数的时候, 返回一个 Promise 化的调用
- if (!cb && typeof Promise !== 'undefined') {
- return new Promise(resolve => {
- _resolve = resolve;
- });
- }
- }
小结
整体看过来, 感觉还是比较好理解的吧~ 2.6 版本相比之前简化了一点.
小结一下, 每次调用 Vue.nextTick(cb) 会做些什么: cb 函数经处理压入 callbacks 数组, 执行 timerFunc 函数, 延迟调用 flushCallbacks 函数, 遍历执行 callbacks 数组中的所有函数.
延迟调用优先级如下: Promise> MutationObserver> setImmediate> setTimeout
版本差异
其实 Vue 2.4,2.5,2.6 版本的 nextTick 策略都略不一样.
整体 2.6 和 2.4 的比较相似.(仔细瞅了瞅, 基本就是一样的, 2.6 的 timerFunc 多了个 setImmediate 判断)
2.5 版本其实也差不多... 源码写法有些不一样, 整体优先级是: Promise> setImmediate> MessageChannel> setTimeout, 如果更新是在 v-on 事件处理程序中触发的, nextTick 会采用 macrotask. 感兴趣可自行去末尾的参考地址进行查阅.
参考:
API-vue.js https://cn.vuejs.org/v2/api/#Vue-nextTick
事件循环规范
Vue nextTick v2.6.7 源码
Vue nextTick v2.5.22 源码
Vue nextTick v2.4.4 源码
从 event loop 规范探究 JavaScript 异步及浏览器更新渲染时机
来源: https://juejin.im/post/5c7674b5e51d4506304edb99