Vue中nextTick的时序问题
前言
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的时序问题的更多相关文章
- vue中nextTick
vue中nextTick可以拿到更新后的DOM元素 如果在mounted下不能准确拿到DOM元素,可以使用nextTick 在Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue ...
- Vue中nextTick()解析
最近,在开发的时候遇到一个问题,让我对vue中nextTick()的用法加深了了解- 下面是在组件中引用的一个拖拽的组件: <vue-draggable-resizable class=&quo ...
- Vue中$nextTick的理解
Vue中$nextTick的理解 Vue中$nextTick方法将回调延迟到下次DOM更新循环之后执行,也就是在下次DOM更新循环结束之后执行延迟回调,在修改数据之后立即使用这个方法,能够获取更新后的 ...
- 通俗易懂了解Vue中nextTick的内部实现原理
1. 前言 nextTick 是 Vue 中的一个核心功能,在 Vue 内部实现中也经常用到 nextTick.在介绍 nextTick 实现原理之前,我们有必要先了解一下这个东西到底是什么,为什么要 ...
- vue中nextTick的理解
A. vue 中的 nextTick 是什么? 1.首先需要清楚,nextTick是一个函数:这个函数的作用,简单理解就是下一次渲染后才执行 nextTick 函数中的操作: 2.在下一次 DOM 更 ...
- vue中$nextTick详细讲解保证你一看就明白
1.功能描述 今天我们要实现这个一个小功能: 页面渲染完成后展示一个div元素: 当点击这个div元素后: div元素消失: 出现一个input元素:并且input元素聚焦 想必大家我觉得简单,我们一 ...
- vue中$nextTick的使用
转载 https://www.jb51.net/article/154823.htm ,写的通俗易懂 在这里我有一个疑问,因为在vue中mounted里面执行后,dom节点是挂载上去了的,所以视图上 ...
- vue中nextTick的使用(转载)
转载自:https://www.cnblogs.com/chaoyuehedy/p/8985425.html 简介 vue是非常流行的框架,他结合了angular和react的优点,从而形成了一个轻量 ...
- vue中$nextTick的用法
简介 vue是非常流行的框架,他结合了angular和react的优点,从而形成了一个轻量级的易上手的具有双向数据绑定特性的mvvm框架.本人比较喜欢用之.在我们用vue时,我们经常用到一个方法是th ...
- 对vue中nextTick()的理解及使用场景说明
异步更新队列: 首先我们要对vue的数据更新有一定理解: vue是依靠数据驱动视图更新的,该更新的过程是异步的. 即:当侦听到你的数据发生变化时, Vue将开启一个队列(该队列被Vue官方称为异步更新 ...
随机推荐
- 摄像头的MIPI接口、DVP接口和CSI接口
电脑摄像头接口是USB接口,智能手机的摄像头接口是MIPI接口,还有一部分的摄像头(比如说某些支持DVP接口的硬件)是DVP接口. USB是串行通用串行总线(Universal Serial Bus) ...
- python 操作 ES 一、基础操作
示例代码环境 python:3.8 es:7.8.0环境安装 pip install elasticsearch==7.8.0 from elasticsearch import Elasticsea ...
- c语言动态库与静态库
// show.h #ifndef __SHOW_H_ #define __SHOW_H_ #include <stdio.h> #include "math.h" v ...
- python中items()和iteritems()的区别
items()函数,将一个字典以dict_items的形式返回,因为字典是无序的,所以返回的列表也是无序的: 1 a ={'a':1,'b':2,'c':3,'d':4} 2 print(a.item ...
- -bash: nslookup: 未找到命令;centos7 安装nslookup
一.安装服务 [root@localhost ~]# yum -y install bind-utils 二.查看 [root@localhost ~]# nslookup
- EBS 常用sql
1)查看请求挂在哪个状态下 SELECT fcpv.concurrent_program_name FROM fnd_request_groups frg, --请求组 fnd_request_gro ...
- 从零搭建hadoop集群之zookeeper集群安装
1. 从官方渠道获取对应的zookeeper的安装包 http://archive.apache.org/dist/zookeeper/ zookeeper-3.4.10.tar.g 2. 上传zoo ...
- Redis后端面试题
目录 简要说一下什么是Redis? 为什么要⽤Redis(缓存)? 为什么要⽤Redis⽽不⽤map/guava做缓存? Redis与Memcached的区别 Redis的应⽤场景 redis为什么那 ...
- fastapi四:uvicorn.run支持的参数
`app:指定应用app,'脚本名:FastAPI实例对象'.FastAPI实例对象 host: 字符串,允许被访问的形式 locahost.127.0.0.1.当前IP.0.0.0.0,默认为127 ...
- Div的几种选择器
Div 是一个html标签,一个块级元素(单独显示一行),单独使用没有意义,需要结合CSS来使用,主要用于页面的布局. div选择器: 1.元素选择器: 1 <style> 2 div{ ...