理解Vue.nextTick使用及源码分析(微任务的妙用)
异步更新机制使用微任务或宏任务,基于事件循环运行,在 Vue 中对性能起着至关重要的作用,它对重复冗余的 watcher 进行过滤。而 nextTick 根据不同的环境,使用优先级最高的异步任务。这样做的好处是等待所有的状态同步更新完毕后,再一次性渲染页面。用户创建的 nextTick 运行页面更新之后,因此能够获取更新后的DOM。
理解Vue.nextTick使用及源码分析
什么是Vue.nextTick()?
官方文档解释为:在下次DOM更新循环结束之后
执行的延迟回调
。在修改数据之后立即使用该方法,获取更新后的DOM。
我们也可以简单的理解为:当页面中的数据发生改变了,就会把该任务放到一个异步队列
中,只有在当前任务空闲时才会进行DOM渲染,当DOM渲染完成以后
,该函数会自动执行。
Vue.nextTick()方法的应用场景有哪些?
更改数据后,进行节点DOM操作。
<!DOCTYPE html>
<html>
<head>
<title>vue.nextTick()方法的使用</title>
<meta charset="utf-8">
<script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
<div id="app">
<template>
<div ref="list">{{name}}</div>
</template>
</div>
<script type="text/javascript">
new Vue({
el: '#app',
data: {
name: 'kongzhi111'
},
mounted() {
this.updateData();
},
methods: {
updateData() {
this.name = 'kongzhi222';
// 此时data数据变了,但是还没有更新dom节点
console.log(this.$refs.list.textContent); // 打印 kongzhi111
this.$nextTick(() => {
// 数据改变,dom节点更新后,才会触发
console.log(this.$refs.list.textContent); // 打印 kongzhi222
});
}
}
})
</script>
</body>
</html>
理解DOM更新:在VUE中,当我们修改了data中的某一个值后,并不会立刻去渲染html页面,而是将vue更改的数据放到watcher的一个异步队列中,只有在当前任务空闲时才会执行watcher中的队列任务
在created生命周期中进行DOM操作
在Vue生命周期中,created触发时,实例已完成
以下的配置:数据观测 (data observer)
,property 和方法的运算
,watch/event 事件回调
。然而,挂载阶段还没开始,$el property
目前尚不可用
(通过$refs
获取dom元素也获取不到).
<!DOCTYPE html>
<html>
<head>
<title>vue.nextTick()方法的使用</title>
<meta charset="utf-8">
<script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
<div id="app">
<template>
<div ref="list">{{name}}</div>
</template>
</div>
<script type="text/javascript">
new Vue({
el: '#app',
data: {
name: 'kongzhi111'
},
created() {
console.log(this.$refs.list); // 打印undefined
this.$nextTick(() => {
console.log(this.$refs.list); // 打印出 "<div>kongzhi111</div>"
});
},
methods: {
}
})
</script>
</body>
</html>
Vue.nextTick的调用方式如下
Vue.nextTick([callback, context]):
该方法是全局方法,该方法可接收2个参数,分别为回调函数 和 执行回调函数的上下文环境。
vm.$nextTick([callback]):
该方法是实列方法,执行时自动绑定this到当前的实列上。
vm.$nextTick 与 setTimeout 的区别是什么?
在区别他们俩之前,我们先来看一个简单的demo如下:
<!DOCTYPE html>
<html>
<head>
<title>vue.nextTick()方法的使用</title>
<meta charset="utf-8">
<script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
<div id="app">
<template>
<div ref="list">{{name}}</div>
</template>
</div>
<script type="text/javascript">
new Vue({
el: '#app',
data: {
name: 'kongzhi111'
},
created() {
console.log(this.$refs.list); // 打印undefined
setTimeout(() => {
console.log(this.$refs.list); // 打印出 "<div>kongzhi111</div>"
}, 0);
}
})
</script>
</body>
</html>
如上代码,我们不使用 nextTick
, 我们使用setTimeout
延迟也一样可以通过$refs获取页面中的HTML元素的,那么他们俩之间到底有什么区别呢?
vue源码中nextTick
在 src/core/util/next-tick.js
。在vue中使用了三种情况来延迟调用
该函数
- 首先我们会判断我们的设备是否支持
Promise对象
,如果支持的话,会使用Promise.then
来做延迟调用函数。 - 如果设备不支持Promise对象,再判断是否支持
MutationObserver
对象,如果支持该对象,就使用MutationObserver
来做延迟, - 最后如果上面两种都不支持的话,我们会使用
setTimeout(() => {}, 0)
; setTimeout 来做延迟操作。
比较 nextTick 与 setTimeout 的区别,实际上是比较 promise 或 MutationObserver 对象 与 setTimeout的区别的了
在比较promise与setTimeout之前,我们先来看如下demo。
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8">
</head>
<body>
<script type="text/javascript">
console.log(1);
setTimeout(function(){
console.log(2);
}, 0);
new Promise(function(resolve) {
console.log(3);
for (var i = 0; i < 100; i++) {
i === 99 && resolve();
}
console.log(4);
}).then(function() {
console.log(5);
});
console.log(6);
</script>
</body>
</html>
如上代码输出的结果是:1, 3, 4, 6, 5, 2;
首先打印1,这个我们能理解的,其实为什么打印3,在promise内部也属于同步的,只有在then内是异步的,因此打印 1, 3, 4 , 然后执行then函数是异步的,因此打印6. 那么结果为什么是 1, 3, 4, 6, 5, 2 呢? 为什么不是 1, 3, 4, 6, 2, 5呢?
我们都知道 Promise.then
和 setTimeout
都是异步的,那么在事件队列中Promise.then的事件应该是在setTimeout的后面的,那么为什么Promise.then比setTimeout函数先执行呢?
理解Event Loop 的概念
这一次,彻底弄懂 JavaScript 执行机制(Event Loop)
我们都明白,javascript是单线程的,所有的任务都会在主线程中执行的,当主线程中的任务都执行完成之后,系统会 “依次” 读取任务队列里面的事件,因此对应的异步任务进入主线程,开始执行。
但是异步任务队列
又分为: macrotasks(宏任务)
和 microtasks(微任务)
。 他们两者分别有如下API:
-
macrotasks(宏任务): setTimeout、setInterval、setImmediate、I/O、UI rendering 等。
-
microtasks(微任务): Promise、process.nextTick、MutationObserver 等。
如上我们的promise的then方法的函数会被推入到 microtasks(微任务) 队列中,而setTimeout函数会被推入到 macrotasks(宏任务) 任务队列中,在每一次事件循环中 macrotasks(宏任务) 只会提取一个执行,而 microtasks(微任务) 会一直提取,直到 microtasks(微任务)队列为空为止。
有了上面 macrotasks(宏任务) 和 microtasks(微任务) 概念后,我们再来理解上面的代码,上面所有的代码都写在script标签中,那么读取script标签
中的所有代码,它就是第一个宏任务
,因此我们就开始执行第一个宏任务。因此首先打印 1, 然后代码往下读取,我们遇到setTimeout, 它就是第二个宏任务,会将它推入到 macrotasks(宏任务) 事件队列里面排队。
下面我们继续往下读取,
遇到Promise对象,在Promise内部执行
是同步
的,因此会打印3, 4。 然后继续遇到Promise.then 回调函数
,他是一个 microtasks(微任务)
的,因此将他 推入到 microtasks(微任务) 事件队列中,最后代码执行 console.log(6); 因此打印6. 第一个macrotasks(宏任务)执行完成后,然后我们会依次循环执行 microtasks(微任务), 直到最后一个为止,因此我们就执行 promise.then() 异步回调中的代码,因此打印5,那么此时此刻第一个 macrotasks(宏任务) 执行完毕,会执行下一个 macrotasks(宏任务)任务。因此就执行到 setTimeout函数了,最后就打印2。到此,所有的任务都执行完毕。因此我们最后的结果为:1, 3, 4, 6, 5, 2;
我们可以继续多添加几个setTimeout函数和多加几个Promise对象来验证下,如下代码:
<script type="text/javascript">
console.log(1);
setTimeout(function(){
console.log(2);
}, 10);
new Promise(function(resolve) {
console.log(3);
for (var i = 0; i < 10000; i++) {
i === 9999 && resolve();
}
console.log(4);
}).then(function() {
console.log(5);
});
setTimeout(function(){
console.log(7);
},1);
new Promise(function(resolve) {
console.log(8);
resolve();
}).then(function(){
console.log(9);
});
console.log(6);
</script>
如上打印的结果为: 1, 3, 4, 8, 6, 5, 9, 7, 2;
首先打印1,promise内部是同步代码,因此打印 3, 4, 然后就是第二个promise内部代码,因此打印8,再打印外面的代码,就是6。因此主线程执行完成后,打印的结果分别为:1, 3, 4, 8, 6。
然后再执行 promise.then() 回调的 microtasks(微任务)。因此打印 5, 9。因此microtasks(微任务)执行完成后,就执行第二个宏任务setTimeout,由于第一个setTimeout是10毫秒后执行,第二个setTimeout是1毫秒后执行,因此1毫秒的优先级大于10毫秒的优先级,因此最后分别打印 7, 2 了。因此打印的结果是: 1, 3, 4, 8, 6, 5, 9, 7, 2;
总结: 如上我们也看到 microtasks(微任务)
包括 Promise
和 MutationObserver
, 因此 我们可以知道在Vue中的nextTick 的执行速度上快于setTimeout
。
我们从如下demo也可以得到验证:
<!DOCTYPE html>
<html>
<head>
<title>vue.nextTick()方法的使用</title>
<meta charset="utf-8">
<script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
<div id="app">
<template>
<div ref="list">{{name}}</div>
</template>
</div>
<script type="text/javascript">
new Vue({
el: '#app',
data: {
name: 'kongzhi111'
},
created() {
console.log(this.$refs.list); // 打印undefined
setTimeout(() => {
console.log(this.$refs.list); // 打印出 "<div>kongzhi111</div>"
}, 0);
this.$nextTick(function(){
console.log('nextTick比setTimeout先执行');
});
}
})
</script>
</body>
</html>
如上代码,先打印的是 undefiend, 其次是打印 “nextTick比setTimeout先执行” 信息, 最后打印出 “<div>kongzhi111” 信息。
理解 MutationObserver
在Vue中的nextTick
的源码中,使用了3种情况
来做延迟操作,首先会判断我们的设备是否支持Promsie对象
,如果支持Promise对象,就使用Promise.then()
异步函数来延迟,如果不支持,我们会继续判断我们的设备是否支持 MutationObserver
, 如果支持,我们就使用 MutationObserver 来监听。最后如果上面两种都不支持的话,我们会使用 setTimeout
来处理,那么我们现在要理解的是 MutationObserver 是什么?
MutationObserver是什么?
MutationObserver–MDN
MutationObserver 中文含义可以理解为 “变动观察器”。它是监听DOM变动
的接口,DOM发生任何变动,MutationObserver会得到通知。在Vue中是通过该属性来监听DOM更新完毕的。
它和事件类似,但有所不同,事件是同步的,当DOM发生变动时,事件会立刻处理,但是MutationObserver
则是异步(微任务)
的,它不会立即处理,而是等页面上所有的DOM完成后,会执行一次,如果页面上要操作100次DOM的话,如果是事件的话会监听100次DOM,但是我们的 MutationObserver 只会执行一次,它是等待所有的DOM操作完成后,再执行。
它的特点是:
- 等待所有脚本任务完成后,才会执行,即采用异步方式。
- DOM的变动记录会封装成一个数组进行处理。
- 还可以观测发生在DOM的所有类型变动,也可以观测某一类变动。
当然 MutationObserver 也是有浏览器兼容的,我们可以使用如下代码来检测浏览器是否支持该属性,如下代码:
var MutationObserver = window.MutationObserver || window.WebkitMutationObserver || window.MozMutationObserver;
// 监测浏览器是否支持
var observeMutationSupport = !!MutationObserver;
MutationObserver 构造函数
首先我们要使用 MutationObserver 构造函数的话,我们先要实列化 MutationObserver 构造函数,同时我们要指定该实列的回调函数,如下代码:
var observer = new MutationObserver(callback);
观察器callback回调函数会在每次DOM发生变动后调用,它接收2个参数,第一个是变动的数组,第二个是观察器的实列。
MutationObserver 实列的方法
observe() 该方法是要观察DOM节点的变动的。该方法接收2个参数,第一个参数是要观察的DOM元素,第二个是要观察的变动类型。
调用方式为:observer.observe(dom, options);
options 类型有如下:
- childList: 子节点的变动。
- attributes: 属性的变动。
- characterData: 节点内容或节点文本的变动。
- subtree: 所有后代节点的变动。
需要观察哪一种变动类型,需要在options对象
中指定为true即可; 但是如果设置subtree的变动,必须同时指定childList, attributes, 和 characterData 中的一种或多种。
监听childList的变动
<!DOCTYPE html>
<html>
<head>
<title>MutationObserver</title>
<meta charset="utf-8">
</head>
<body>
<div id="app">
<ul>
<li>kongzhi111</li>
</ul>
</div>
<script type="text/javascript">
var MutationObserver = window.MutationObserver || window.WebkitMutationObserver || window.MozMutationObserver;
var list = document.querySelector('ul');
var Observer = new MutationObserver(function(mutations, instance) {
console.log(mutations); // 打印mutations 如下图对应的
console.log(instance); // 打印instance 如下图对于的
mutations.forEach(function(mutation){
console.log(mutation); // 打印mutation
});
});
Observer.observe(list, {
childList: true, // 子节点的变动
subtree: true // 所有后代节点的变动
});
var li = document.createElement('li');
var textNode = document.createTextNode('kongzhi');
li.appendChild(textNode);
list.appendChild(li);
</script>
</body>
</html>
如上代码,我们使用了 observe() 方法来观察list节点的变化,只要list节点的子节点或后代的节点有任何变化都会触发 MutationObserver 构造函数的回调函数。因此就会打印该构造函数里面的数据
打印如下图所示:
监听characterData的变动
<!DOCTYPE html>
<html>
<head>
<title>MutationObserver</title>
<meta charset="utf-8">
</head>
<body>
<div id="app">
<ul>
<li>kongzhi111</li>
</ul>
</div>
<script type="text/javascript">
var MutationObserver = window.MutationObserver || window.WebkitMutationObserver || window.MozMutationObserver;
var list = document.querySelector('ul');
var Observer = new MutationObserver(function(mutations, instance) {
mutations.forEach(function(mutation){
console.log(mutation);
});
});
Observer.observe(list, {
childList: true, // 子节点的变动
characterData: true, // 节点内容或节点文本变动
subtree: true // 所有后代节点的变动
});
// 改变节点中的子节点中的数据
list.childNodes[0].data = "kongzhi222";
</script>
</body>
</html>
打印如下
监听属性的变动
<!DOCTYPE html>
<html>
<head>
<title>MutationObserver</title>
<meta charset="utf-8">
</head>
<body>
<div id="app">
<ul>
<li>kongzhi111</li>
</ul>
</div>
<script type="text/javascript">
var MutationObserver = window.MutationObserver || window.WebkitMutationObserver || window.MozMutationObserver;
var list = document.querySelector('ul');
var Observer = new MutationObserver(function(mutations, instance) {
mutations.forEach(function(mutation){
console.log(mutation);
});
});
Observer.observe(list, {
attributes: true
});
// 设置节点的属性,会触发回调函数
list.setAttribute('data-value', 'tugenhua111');
// 重新设置属性,会触发回调函数
list.setAttribute('data-value', 'tugenhua222');
// 删除属性,也会触发回调函数
list.removeAttribute('data-value');
</script>
</body>
</html>
如上就是MutationObserver
的基本使用,它能监听子节点的变动、属性的变动、节点内容或节点文本的变动及所有后代节点的变动。 下面我们来看下我们的nextTick.js
中的源码是如何实现的。
nextTick源码分析
vue源码在 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
// 变量 callbacks = []; 该变量的作用是: 用来存储所有需要执行的回调函数
const callbacks = []
// let pending = false; 该变量的作用是表示状态,判断是否有等待执行的回调函数(在微任务队列里)
let pending = false
// flushCallbacks() 函数,该函数的作用是用来执行callbacks里面存储的所有回调函数。
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// 如果代码中 timerFunc 函数被推送到任务队列中去则不需要重复推送。
let timerFunc;
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) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
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)
}
}
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) {
// 同一个事件循环(tick) 这里面只会执行一次
pending = true
// timerFunc会把数组回调函数的执行放到异步里,同步代码都执行完,才会执行,执行时pending状态会改为false
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
如上代码,我们从上往下看,首先定义变量 callbacks
= []; 该变量的作用是: 用来存储所有需要执行的回调函数
。
let pending = false
; 该变量的作用是表示状态,判断是否有等待执行的回调函数(在微任务队列里)
。
如果代码中 timerFunc
函数被推送到异步任务队列(微任务队列)中去则不重复
推送。
flushCallbacks()
函数,该函数的作用是用来执行callbacks
里面存储的所有回调函数
。如下代码:
function flushCallbacks () {
/*
设置 pending 为 false, 说明该 函数已经被推入到任务队列或主线程中。需要等待当前
栈执行完毕后再执行后面代码。
*/
pending = false;
// 拷贝一个callbacks函数数组的副本
// slice() 返回一个新的数组,不改变原来的数组
const copies = callbacks.slice(0)
// 把函数数组清空
callbacks.length = 0
// 循环该函数数组,依次执行。
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
timerFunc
: 保存需要被执行的函数。
继续看接下来的代码,我们上面讲解过,在Vue
中使用了几种情况
来延迟调用
该函数。
promise.then 延迟调用
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
}
// isNative 判断 Ctor是不是浏览器内置函数
/* istanbul ignore next */
function isNative (Ctor) {
return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}
// Promise.toString() 打印出 "function Promise() { [native code] }"
如果设备(或叫浏览器)支持Promise
, 就使用 Promise.then
的方式来延迟
函数的调用。Promise.then()回调
函数属于异步事件的微任务
,通过事件循环实现延迟执行
MutationObserver 监听
else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
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
}
首先判断设备是否支持 MutationObserver
对象, 如果支持的话,会创建一个MutationObserver构造函数
, 并且把flushCallbacks函数
当做callback
的回调, 然后创建一个文本节点, 之后会使用MutationObserver
对象的observe
来监听
该文本节点, 如果文本节点的内容有任何变动的话,就会触发 flushCallbacks
回调函数。触发timerFunc
函数, 会导致文本节点的数据发生改变,进而触发MutationObserver
构造函数。
MutationObserver
跟Promise.then回调
一样,也是微任务
setImmediate 监听
else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Techinically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
}
如果上面的 Promise
和 MutationObserver
都不支持的话, 我们继续会判断设备是否支持 setImmediate, 我们上面分析过, 他属于 macrotasks(宏任务)
的。该任务会在一个宏任务里执行回调队列。
使用setTimeout 做降级处理
如果我们上面三种情况, 设备都不支持的话, 我们会使用 setTimeout
来做降级处理, 实现延迟效果。如下基本代码:
else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
现在我们的源码继续往下看, 会看到我们的nextTick
函数被export
了,如下基本代码:
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()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
如上代码, nextTick
函数接收2个参数
,cb
是一个回调函数, ctx
是一个上下文。
首先会把cb
存入callbacks函数数组
里面去, 在函数内部会判断cb是否是一个函数,如果是一个函数,就调用执行该函数,当然它会在callbacks函数数组遍历的时候才会被执行
。
其次 如果cb不是一个函数的话, 那么会判断是否有_resolve值, 有该值就使用Promise.then() 这样的方式来调用。比如: this.$nextTick().then(cb)
这样的使用方式。因此在下面的if语句内会判断赋值给_resolve:
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
使用Promise
返回了一个fulfilled
的Promise
。赋值给_resolve
; 然后在callbacks.push
中会执行如下:
_resolve(ctx);
全局方法Vue.nextTick
在/src/core/global-api/index.js
中声明,是对函数nextTick
的引用,所以使用时可以显式指定执行上下文。代码初始化如下:
Vue.nextTick = nextTick;
我们可以使用如下的一个简单的demo来简化上面的代码。如下demo:
<script type="text/javascript">
var callbacks = [];
var pending = false;
function timerFunc() {
const copies = callbacks.slice(0)
callbacks.length = 0
for (var i = 0; i < copies.length; i++) {
copies[i]()
}
}
function nextTick(cb, ctx) {
var _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()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
// 调用方式如下:
nextTick(function() {
console.log('nextTick'); // nextTick
});
</script>
如上我们已经知道了 nextTick
是Vue
中的一个全局函数
, 在Vue里面会有一个Watcher
, 它用于观察数据的变化
, 然后更新DOM
, 但是在Vue
中并不是每次数据改变都会触发更新DOM的, 而是将这些操作都缓存
到一个队列中, 在一个事件循环结束后, 会刷新队列, 会统一执行DOM的更新
操作。
Watcher对象
在Vue
中使用的是Object.defineProperty
来监听每个对象属性数据变化的, 当监听到数据发生变化
的时候, 我们需要把该消息通知到所有的订阅者
, 也就是Dep
, 那么Dep
则会调用它管理的所有的Watch对象
,然后会调用Watch对象
中的update
方法, 我们可以看下源码中的update的实现。源码在 vue/src/core/observer/watcher.js
中如下代码:
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
// 同步执行渲染视图
this.run()
} else {
// 异步推送到观察者队列中
queueWatcher(this) // this 为当前的实例 watcher
}
}
如上代码我们可以看到, 在Vue
中默认异步执行DOM更新
。当异步执行update的时候,它默认调用 queueWatcher
函数。
我们下面再来看下该 queueWatcher
函数代码如下: (源码在: vue/src/core/observer/scheduler.js
) 中。
const queue = []
let has = {}
let waiting = false
let flushing = false
let index = 0
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
// flushing来表示队列的更新状态。
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
如上源码, 首先获取id = watcher.id
; 然后判断该id是否存在 if (has[id] == null) {}
, 如果已经存在则直接跳过(watcher去重
),不存在则执行if语句内部代码, 并且标记哈希表has[id] = true
; 用于下次检验。
flushing
来表示队列的更新状态
,如果 flushing
为false的话, 则把该watcher对象
push到队列
中,
waiting
的作用是防止 nextTick 重复
执行
if (!flushing) {
queue.push(watcher)
}
如果watcher
已经更新过了, 就把这个watche
r再放到当前执行的下一位
, 当前的watcher
处理完成后, 立即会处理这个最新的。如下代码:
else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
接着如下代码:
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
waiting
为false
, 等待下一个tick时, 会执行刷新队列。 如果不是正式环境的话, 会直接 调用该函数 flushSchedulerQueue
; (源码在: vue/src/core/observer/scheduler.js
) 中。否则的话, 把该函数放入 nextTick
函数延迟处理。
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
}
// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
}
flushSchedulerQueue
内将刚刚加入 queue 的 watcher 逐个 run 更新
。resetSchedulerState
重置状态,等待下一轮的异步更新。
function resetSchedulerState () {
index = queue.length = activatedChildren.length = 0
has = {}
if (process.env.NODE_ENV !== 'production') {
circular = {}
}
waiting = flushing = false
}
要注意此时 flushSchedulerQueue
还未执行,它只是作为回调传入而已。因为用户
可能也会调用 nextTick
方法。这种情况下,callbacks
里的内容为["flushSchedulerQueue", "用户的nextTick回调"]
,当所有同步任务执行完成
,才开始执行 callbacks
里面的回调。
由此可见,最先执行
的是页面更新的逻辑
,其次再到用户的 nextTick 回调
执行。这也是为什么我们能在 nextTick 中获取到更新后DOM的原因
。
总结
异步更新机制
使用微任务或宏任务
,基于事件循环
运行,在 Vue 中对性能起着至关重要的作用,它对重复冗余的 watcher 进行过滤
。
而 nextTick 根据不同的环境,使用优先级最高的异步任务。这样做的好处是等待所有的状态同步更新完毕后,再一次性
渲染页面。
用户创建的 nextTick 运行页面更新之后,因此能够获取更新后的DOM。
谢谢你阅读到了最后
期待你关注、收藏、评论、点赞
更多推荐
所有评论(0)