前言

Vue.$nextTick这个API相信很多人都用过,按照文档的解释,“在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM”。我们通常会在使用第三方库或者处理复杂条件下的渲染时机的时候用到它,它是如此的好用以至于碰到棘手的问题的时候,我们都会想到是不是这简单的一行命令就可以解决问题呢?有时它确实奏效了,有时又没有,但在完全理解它之前,我建议还是对此保持谨慎。

看下面一段代码,猜猜点击div元素之后控制台会打印什么?

new Vue({
el: '#app',
template: '<div @click="handleClick">{{ msg }}</div>',
data () {
return {
msg: 1.0
}
},
methods: {
handleClick () {
this.msg = Math.random() // A
console.log('c1')
Promise.resolve().then(() => { // B
console.log('c2')
})
this.$nextTick(() => { // C
console.log('c3')
})
}
}
})

事实上如果是vue2.6.10版本,打印结果是c1 - c3 - c2,而如果是vue2.5.10版本,则结果是c1 - c2 - c3

那么,是什么导致了版本之间的差异呢?

差异原因分析

Vue内部存在着一个nextTick的“任务池”,里面包含着每次调用nextTick(callback)收集到的回调函数callback,在一个“合适的时机”,Vue清空任务池,依次触发收集到的回调函数。

上文中我们修改了Model层的数据,触发了reander watcher,Vue将执行一次重新渲染,但是这次重新渲染就是放在一个nextTick中去执行的,也就是说Vue没有即时地去更新View层,而是采用回调的方式进行;后面我们又用this.$nextTick函数往“任务池”中新加入一个回调,现在“任务池”中有两件任务了。

下面结合源代码看看:

// vue 2.6.10 部分源码
// 这个数组就是“任务池”
var callbacks = []
function nextTick (cb, ctx) {
var _resolve;
// nextTick就是往“任务池”中塞入了一个回调
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();
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
...
// $nextTick 用法
// 1. this.$nextTick(function() { // do something })
// 2. this.$nextTick().then(function(ctx) { // do something })
Vue.prototype.$nextTick = function (fn) {
return nextTick(fn, this)
};

通过源码我们得知在“合适的时机”清空任务池的方法叫做TimerFunc,这个方法在时机选择上根据Vue版本有一些不同:

在Vue2.6.10版本,timerFunc的策略是优先在各处均使用微任务,即以Promise和MutationObserver为主,兼容宏任务setImmediate(仅IE和Edge兼容)和setTimeout。

在Vue2.5.10版本,Vue在dom事件中timerFunc优先使用了宏任务setImmediate和MessageChannel

下面来分析为什么会出现文初的差异:

在Vue2.6.10版本,handleClick触发后执行到A行,数据发生了变化,根据前文的描述,nextTick函数执行,此时TimerFunc执行,pending变为true,TimerFunc采用的是Promise,即Promise.resolve().then(flushCallbacks /* 清空任务池的方法 */),注册了一个微任务;接着执行到B行,它也注册了打印c2的Promise微任务,当然根据微任务执行规则,它是要晚于前一个微任务执行的;最后执行到C行,nextTick函数执行,它往任务池中塞入一个回调,然后到此为止了,因为此时pending是true,所以TimerFunc不重复执行。此时,微任务队列应该是这个样子的:

微任务1(flushCallbacks): 模板重新渲染 + 打印c3
微任务2(打印c2)

然后宏任务结束后微任务依次执行,顺序打印c3 -- c2

在vue2.5.10版本,handleClick触发后执行到A行,数据发生了变化,nextTick函数执行,但TimerFunc采用的是宏任务(为了方便我们假定所有浏览器均支持setImmediate),即setImmediate(flushCallbacks /* 清空任务池的方法 */),注册了一个宏任务;接着执行到B行,它注册了打印c2的Promise微任务;最后执行到C行,nextTick函数执行,往任务池中塞入一个回调,然后到此为止了。此时任务队列应该是这个样子的:

--- 当前宏任务
***
微任务1(打印c2) --- setImmediate宏任务,跟上面的宏任务没关系
flushCallbacks: 模板重新渲染 + 打印c3

根据以上分析,打印结果是c2 -- c3

那么怎么消除这个版本差异呢?其实改动一点代码即可

消除版本差异的方法

我们再看看nextTick的源码,发现如果不提供回调作为参数,则其会返回一个Promise,这个Promise将在callback执行时才resolve。

我们利用这种用法改写上面的代码:

handleClick () {
this.msg = Math.random()
console.log('c1')
Promise.resolve().then(() => {
console.log('c2')
})
// 换成这种写法
this.$nextTick().then(() => {
console.log('c3')
})
}

则两个版本均打印c1 -- c3 -- c2, 来分析下原因:

在vue2.6.10版本,执行到C行时nextTick任务池中加入的回调并没有直接打印c3,而是在它返回的Promise发生resolve后再把打印c3(then函数)加入微任务队列,此时微任务队列为:

微任务1(flushCallbacks): 模板重新渲染 + _resolve
微任务2(打印c2)
微任务3(打印c3) // 在微任务1中的_resolve调用后才被加入微任务队列中

在vue2.5.10版本,分析与上面类似,任务队列为:

--- 当前宏任务
***
微任务1(打印c2) --- setImmediate宏任务,跟上面的宏任务没关系
flushCallbacks: 模板重新渲染 + _resolve
***
微任务1(打印c3) // 在_resolve调用后才被加入微任务队列中

代码像这样改动,就保证了vue两个版本回调执行顺序的同一性,但是从任务队列可见回调的执行时机大不相同,前者是在同一个微任务队列中依次执行,而后者采用的宏任务模式显得更加复杂。js是单线程的,两个宏任务的执行间隔取决于任务耗时、浏览器策略等;至于各种宏任务之间调用的优先级和区别,就是另外一个问题了。

最后简单谈谈为什么vue源码对任务队列的执行方案从宏任务迁移到了微任务,其实这两种方案均存在一些问题,具体问题可以查看源码的注释,找到对应的issue进行分析。不同于React的Fiber架构,Vue并未在任务调度方面做得太多,最近发布的vue3.0beta版本仍然采用的是微任务方案。

Vue中nextTick的时序问题的更多相关文章

  1. vue中nextTick

    vue中nextTick可以拿到更新后的DOM元素 如果在mounted下不能准确拿到DOM元素,可以使用nextTick 在Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue ...

  2. Vue中nextTick()解析

    最近,在开发的时候遇到一个问题,让我对vue中nextTick()的用法加深了了解- 下面是在组件中引用的一个拖拽的组件: <vue-draggable-resizable class=&quo ...

  3. Vue中$nextTick的理解

    Vue中$nextTick的理解 Vue中$nextTick方法将回调延迟到下次DOM更新循环之后执行,也就是在下次DOM更新循环结束之后执行延迟回调,在修改数据之后立即使用这个方法,能够获取更新后的 ...

  4. 通俗易懂了解Vue中nextTick的内部实现原理

    1. 前言 nextTick 是 Vue 中的一个核心功能,在 Vue 内部实现中也经常用到 nextTick.在介绍 nextTick 实现原理之前,我们有必要先了解一下这个东西到底是什么,为什么要 ...

  5. vue中nextTick的理解

    A. vue 中的 nextTick 是什么? 1.首先需要清楚,nextTick是一个函数:这个函数的作用,简单理解就是下一次渲染后才执行 nextTick 函数中的操作: 2.在下一次 DOM 更 ...

  6. vue中$nextTick详细讲解保证你一看就明白

    1.功能描述 今天我们要实现这个一个小功能: 页面渲染完成后展示一个div元素: 当点击这个div元素后: div元素消失: 出现一个input元素:并且input元素聚焦 想必大家我觉得简单,我们一 ...

  7. vue中$nextTick的使用

    转载 https://www.jb51.net/article/154823.htm  ,写的通俗易懂 在这里我有一个疑问,因为在vue中mounted里面执行后,dom节点是挂载上去了的,所以视图上 ...

  8. vue中nextTick的使用(转载)

    转载自:https://www.cnblogs.com/chaoyuehedy/p/8985425.html 简介 vue是非常流行的框架,他结合了angular和react的优点,从而形成了一个轻量 ...

  9. vue中$nextTick的用法

    简介 vue是非常流行的框架,他结合了angular和react的优点,从而形成了一个轻量级的易上手的具有双向数据绑定特性的mvvm框架.本人比较喜欢用之.在我们用vue时,我们经常用到一个方法是th ...

  10. 对vue中nextTick()的理解及使用场景说明

    异步更新队列: 首先我们要对vue的数据更新有一定理解: vue是依靠数据驱动视图更新的,该更新的过程是异步的. 即:当侦听到你的数据发生变化时, Vue将开启一个队列(该队列被Vue官方称为异步更新 ...

随机推荐

  1. HTML中javascript的<script>标签使用方法详解

    只要一提到把JavaScript放到网页中,就不得不涉及Web的核心语言--HTML.在当初开发JavaScript的时候,Netscape要解决的一个重要问题就是如何做到让JavaScript既能与 ...

  2. C++ 11 数字转字符串新功能

    // 头文件 <string>string to_string (int val);string to_string (long val);string to_string (long l ...

  3. c++游戏编程(2)多文件编程与命名空间

    文章目录 前言 1 多文件编程 1.1 头文件 1.1.1 头文件的组成 1.1.2 头文件的储存 1.2 多文件编程 2 命名空间 总结 引用文章 前言 这是我的第二篇博客 上篇文章写了很多c++开 ...

  4. drush use dev.mentor.com | expecting statement

    在多站点的环境中, 不清楚在哪个目录下运行drush cc all, 这时可以运行 drush use dev.mentor.com然后还发现一个很搞笑的事情, 在一个文件的末尾一直现实红色报错符号, ...

  5. 【Python】容器:列表(list)/字典(dict)/元组(tuple)/集合(set)

    三.Python容器:列表(list)/字典(dict)/元组(tuple)/集合(set) 1.列表(list) 1.1 什么是列表 是一个'大容器',可以存储N多个元素简单来说就是其他语言中的数组 ...

  6. vw与百分比%的区别

    单位, vw:只和设备宽度有关系 %:有继承关系

  7. 2020ICPC沈阳I - Rise of Shadows

    剩余系 Problem - I - Codeforces 题意 给定 \(H,M,A\) \(2<=H,M<=10^9,\;0<=A<=\frac {H*M}2\) 假设一个钟 ...

  8. 781. 森林中的兔子 (Medium)

    问题描述 781. 森林中的兔子 (Medium) 森林中有未知数量的兔子.提问其中若干只兔子 "还有多少只兔子与你(指被提问的兔子)颜色相同?" ,将答案收集到一个整数数组 an ...

  9. Counting Triangles

  10. vue项目前台带表格的页面,让表格根据屏幕大小自适应高度,小屏幕时不出现多个滚动条

    参见馆藏库房系统, 右侧整体结构一般如下 <el-container class="ml10 mr10 br7 bgw"> <el-main> // el- ...