Vue系列---理解Vue.nextTick使用及源码分析(五)
阅读目录
- 一. 什么是 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');</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">//</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)"> 重新设置属性,会触发回调函数</span>
list.setAttribute('data-value', 'tugenhua222');
</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">//</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)"> 删除属性,也会触发回调函数</span>
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<span style="color: rgba(0, 0, 255, 1)">if</span> (process.env.NODE_ENV !== 'production' && !<span style="color: rgba(0, 0, 0, 1)">config.async) { flushSchedulerQueue() </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> } 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 = trueif (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
waiting 为 false, 等待下一个 tick 时, 会执行刷新队列。 如果不是正式环境的话, 会直接 调用该函数 flushSchedulerQueue; (源码在: vue/src/core/observer/scheduler.js) 中。否则的话, 把该函数放入 nextTick 函数延迟处理。