前言

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. 【转载】VMWare 各版本下载地址【centos7安装gerrit】

    https://blog.csdn.net/weixin_44129085/article/details/110443135 centos7安装gerrit https://blog.csdn.ne ...

  2. (0403)位运算符+interface

    1)interface 2)位运算符

  3. drf Serializer基本使用

    drf序列化 在前后端不分离的项目中,可以使用Django自带的forms组件进行数据验证,也可以使用Django自带的序列化组件对模型表数据进行序列化. 那么在前后端分离的项目中,drf也提供了数据 ...

  4. 通过系统函数分配内存sbrk/sbrk

    #include <unistd.h> #include <stdio.h> int main(void) { printf("================brk ...

  5. AWS RedShift实战应用SQL大全及经验分享[持续更新]

    文章目录 前言 - 关于RedShift 一.数据维护篇 1.1 表结构操作 1.2 数据添加与查询 1.3 数据修改与删除 1.4 事物操作 二.SQL结构篇 2.1 使用with封装代码 2.2 ...

  6. 1 wine-stable + 2 brew install mono

    一. 通过wine官网找到安装方法 1  brew tap homebrew/cask-versions2  brew install --cask --no-quarantine (selected ...

  7. composer 操作

    composer list 显示所有命令 composer show 显示所有包信息 composer install 在 composer.json 配置中添加依赖库之后运行此命令安装 compos ...

  8. curl:(6) Could not resolve host: baidu.com; Unknown error

    问题描述 有段时间没操作CentOS了,然后启动Virtualbox中的CentOS之后,发现网络不通,ping baidu.com 出现错误 curl:(6) Could not resolve h ...

  9. python 迁移虚拟环境

    1.在源环境中获取包列表(新建文件夹whls) #cd 虚拟环境目录下的\scripts,cmd acitivate # 下载清单到requirements.txt,切换到whls目录 pip fre ...

  10. git入门123

    一.新手上路 最重要的4招: 1. 初始化本地仓库 git init 或者 git clone 远程仓库地址 2.添加改动文件 git add 改动的文件名或者目录 偷懒的话可以直接 git add ...