阅读目录
- 一. 什么是Vue.nextTick()?
-
二. Vue.nextTick()方法的应用场景有哪些?
- 三. Vue.nextTick的调用方式如下:
- 四:vm.$nextTick 与 setTimeout 的区别是什么?
- 五:理解 MutationObserver
- 六:nextTick源码分析
一. 什么是Vue.nextTick()?
官方文档解释为:在下次DOM更新循环结束之后执行的延迟回调。在修改数据之后立即使用该方法,获取更新后的DOM。
我们也可以简单的理解为:当页面中的数据发生改变了,就会把该任务放到一个异步队列中,只有在当前任务空闲时才会进行DOM渲染,当DOM渲染完成以后,该函数就会自动执行。
2.1 更改数据后,进行节点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'; console.log(this.$refs.list.textContent); // 打印 kongzhi111 this.$nextTick(() => { console.log('-------'); console.log(this.$refs.list.textContent); // 打印 kongzhi222 }); } } }) </script> </body> </html>
如上代码,页面初始化时候,页面显示的是 "kongzhi111"; 当页面中的所有的DOM更新完成后,我在mounted()生命周期中调用 updateData()方法,然后在该方法内部修改 this.name 这个数据,再打印 this.$refs.list.textContent, 可以看到打印的数据 还是 'kongzhi111'; 为什么会是这样呢?那是因为修改name数据后,我们的DOM还没有被渲染完成,所以我们这个时候获取的值还是之前的值,但是我们放在nextTick函数里面的时候,代码会在DOM更新完成后 会自动执行 nextTick()函数,因此这个时候我们再去使用 this.$refs.list.textContent 获取该值的时候,就可以获取到最新值了。
理解DOM更新:在VUE中,当我们修改了data中的某一个值后,并不会立刻去渲染html页面,而是将vue更改的数据放到watcher的一个异步队列中,只有在当前任务空闲时才会执行watcher中的队列任务,因此这就会有一个延迟时间,因此我们把代码放到nextTick函数后就可以获取到该 html 页面的最新值了。
2.2 在created生命周期中进行DOM操作。
在Vue生命周期中,只有在mounted生命周期中我们的HTML才渲染完成,因此在该生命周期中,我们就可以获取到页面中的html DOM节点,但是如果我们在 created生命周期中是访问不到DOM节点的。
在该生命周期中我们想要获取DOM节点的话,我们需要使用 this.$nextTick() 函数。
比如如下代码进行演示:
<!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>
如上代码,在created生命周期内,我们打印 this.$refs.list 值为undefined,那是因为在created生命周期内页面的html没有被渲染完成,因此打印出为undefined; 但是我们把它放入 this.$nextTick函数内即可 打印出值出来,这也印证了 nextTick 是在下次DOM更新循环结束之后执行的延迟回调。因此只有DOM渲染完成后才会自动执行的延迟回调函数。
Vue的特点之一就是能实现响应式,但数据更新时,DOM不会立即更新,而是放入一个异步队列中,因此如果在我们的业务场景中,需要在DOM更新之后执行一段代码时,这个时候我们可以使用 this.$nextTick() 函数来实现。
三. Vue.nextTick的调用方式如下:
Vue.nextTick([callback, context]) 和 vm.$nextTick([callback]);
Vue.nextTick([callback, context]); 该方法是全局方法,该方法可接收2个参数,分别为回调函数 和 执行回调函数的上下文环境。
vm.$nextTick([callback]): 该方法是实列方法,执行时自动绑定this到当前的实列上。
四:vm.$nextTick 与 setTimeout 的区别是什么?
<!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延迟也一样可以获取页面中的HTML元素的,那么他们俩之间到底有什么区别呢?
通过看vue源码我们知道,nextTick 源码在 src/core/util/next-tick.js 里面。在vue中使用了三种情况来延迟调用该函数,首先我们会判断我们的设备是否支持Promise对象,如果支持的话,会使用 Promise.then 来做延迟调用函数。如果设备不支持Promise对象,再判断是否支持 MutationObserver 对象,如果支持该对象,就使用MutationObserver来做延迟,最后如果上面两种都不支持的话,我们会使用 setTimeout(() => {}, 0); setTimeout 来做延迟操作。
在比较 nextTick 与 setTimeout 的区别,其实我们可以比较 promise 或 MutationObserver 对象 与 setTimeout的区别的了,因为nextTick会先判断设备是否支持promise及MutationObserver 对象的,只要我们弄懂 promise 和 setTimeout的区别,也就弄明白 nextTick 与 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是单线程的,所有的任务都会在主线程中执行的,当主线程中的任务都执行完成之后,系统会 "依次" 读取任务队列里面的事件,因此对应的异步任务进入主线程,开始执行。
但是异步任务队列又分为: macrotasks(宏任务) 和 microtasks(微任务)。 他们两者分别有如下API:
macrotasks(宏任务): setTimeout、setInterval、setImmediate、I/O、UI rendering 等。
microtasks(微任务): Promise、process.nextTick、MutationObserver 等。
如上我们的promise的then方法的函数会被推入到 microtasks(微任务) 队列中,而setTimeout函数会被推入到 macrotasks(宏任务) 任务队列中,在每一次事件循环中 macrotasks(宏任务) 只会提取一个执行,而 microtasks(微任务) 会一直提取,直到 microtasks(微任务)队列为空为止。
也就是说,如果某个 microtasks(微任务) 被推入到执行中,那么当主线程任务执行完成后,会循环调用该队列任务中的下一个任务来执行,直到该任务队列到最后一个任务为止。而事件循环每次只会入栈一个 macrotasks(宏任务), 主线程执行完成该任务后又会循环检查 microtasks(微任务) 队列是否还有未执行的,直到所有的执行完成后,再执行 macrotasks(宏任务)。 依次循环,直到所有的异步任务完成为止。
有了上面 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</div>" 信息。
五:理解 MutationObserver
在Vue中的nextTick的源码中,使用了3种情况来做延迟操作,首先会判断我们的设备是否支持Promsie对象,如果支持Promise对象,就使用Promise.then()异步函数来延迟,如果不支持,我们会继续判断我们的设备是否支持 MutationObserver, 如果支持,我们就使用 MutationObserver 来监听。最后如果上面两种都不支持的话,我们会使用 setTimeout 来处理,那么我们现在要理解的是 MutationObserver 是什么?
5.1 MutationObserver是什么?
MutationObserver 中文含义可以理解为 "变动观察器"。它是监听DOM变动的接口,DOM发生任何变动,MutationObserver会得到通知。在Vue中是通过该属性来监听DOM更新完毕的。
它和事件类似,但有所不同,事件是同步的,当DOM发生变动时,事件会立刻处理,但是 MutationObserver 则是异步的,它不会立即处理,而是等页面上所有的DOM完成后,会执行一次,如果页面上要操作100次DOM的话,如果是事件的话会监听100次DOM,但是我们的 MutationObserver 只会执行一次,它是等待所有的DOM操作完成后,再执行。
它的特点是:
1. 等待所有脚本任务完成后,才会执行,即采用异步方式。
2. DOM的变动记录会封装成一个数组进行处理。
3. 还可以观测发生在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 中的一种或多种。
1. 监听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 构造函数的回调函数。因此就会打印该构造函数里面的数据。
打印如下图所示:
2. 监听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>
打印如下效果:
3. 监听属性的变动
<!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源码分析
import { noop } from 'shared/util' import { handleError } from './error' import { isIE, isIOS, isNative } from './env' export let isUsingMicroTask = false const callbacks = [] 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]() } } 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) { pending = true 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函数数组的副本 const copies = callbacks.slice(0) // 把函数数组清空 callbacks.length = 0 // 循环该函数数组,依次执行。 for (let i = 0; i < copies.length; i++) { copies[i]() } }
timerFunc: 保存需要被执行的函数。
继续看接下来的代码,我们上面讲解过,在Vue中使用了几种情况来延迟调用该函数。
1. promise.then 延迟调用, 基本代码如下:
if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) if (isIOS) setTimeout(noop) } isUsingMicroTask = true }
如上代码的含义是: 如果我们的设备(或叫浏览器)支持Promise, 那么我们就使用 Promise.then的方式来延迟函数的调用。Promise.then会将函数延迟到调用栈的最末端,从而会做到延迟。
2. 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构造函数。
3. 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(宏任务)的。该任务会在一个宏任务里执行回调队列。
4. 使用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 是一个上下文。 首先会把它存入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('打印出来了'); // 会被执行打印 }); </script>
如上我们已经知道了 nextTick 是Vue中的一个全局函数, 在Vue里面会有一个Watcher, 它用于观察数据的变化, 然后更新DOM, 但是在Vue中并不是每次数据改变都会触发更新DOM的, 而是将这些操作都缓存到一个队列中, 在一个事件循环结束后, 会刷新队列, 会统一执行DOM的更新操作。
在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) } }
如上代码我们可以看到, 在Vue中它默认是使用异步执行DOM更新的。当异步执行update的时候,它默认会调用 queueWatcher 函数。
我们下面再来看下该 queueWatcher 函数代码如下: (源码在: vue/src/core/observer/scheduler.js) 中。
export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true 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) {} , 如果已经存在则直接跳过,不存在则执行if
语句内部代码, 并且标记哈希表has[id] = true; 用于下次检验。如果 flushing 为false的话, 则把该watcher对象push到队列中, 考虑到一些情况, 比如正在更新队列中
的watcher时, 又有事件塞入进来怎么处理? 因此这边加了一个flushing来表示队列的更新状态。
如果加入队列到更新状态时,又分为两种情况:
1. 这个watcher还没有处理, 就找到这个watcher在队列中的位置, 并且把新的放在后面, 比如如下代码:
if (!flushing) { queue.push(watcher) }
2. 如果watcher已经更新过了, 就把这个watcher再放到当前执行的下一位, 当前的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 函数延迟处理。