Vue
中$nextTick
方法将回调延迟到下次DOM
更新循环以后执行,也就是在下次DOM
更新循环结束以后执行延迟回调,在修改数据以后当即使用这个方法,可以获取更新后的DOM
。简单来讲就是当数据更新时,在DOM
中渲染完成后,执行回调函数。javascript
经过一个简单的例子来演示$nextTick
方法的做用,首先须要知道Vue
在更新DOM
时是异步执行的,也就是说在更新数据时其不会阻塞代码的执行,直到执行栈中代码执行结束以后,才开始执行异步任务队列的代码,因此在数据更新时,组件不会当即渲染,此时在获取到DOM
结构后取得的值依然是旧的值,而在$nextTick
方法中设定的回调函数会在组件渲染完成以后执行,取得DOM
结构后取得的值即是新的值。css
<!DOCTYPE html> <html> <head> <title>Vue</title> </head> <body> <div id="app"></div> </body> <script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { msg: 'Vue' }, template:` <div> <div ref="msgElement">{{msg}}</div> <button @click="updateMsg">updateMsg</button> </div> `, methods:{ updateMsg: function(){ this.msg = "Update"; console.log("DOM未更新:", this.$refs.msgElement.innerHTML) this.$nextTick(() => { console.log("DOM已更新:", this.$refs.msgElement.innerHTML) }) } }, }) </script> </html>
官方文档中说明,Vue
在更新DOM
时是异步执行的,只要侦听到数据变化,Vue
将开启一个队列,并缓冲在同一事件循环中发生的全部数据变动,若是同一个watcher
被屡次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免没必要要的计算和DOM
操做是很是重要的。而后,在下一个的事件循环tick
中,Vue
刷新队列并执行实际工做。Vue
在内部对异步队列尝试使用原生的Promise.then
、MutationObserver
和setImmediate
,若是执行环境不支持,则会采用 setTimeout(fn, 0)
代替。
Js
是单线程的,其引入了同步阻塞与异步非阻塞的执行模式,在Js
异步模式中维护了一个Event Loop
,Event Loop
是一个执行模型,在不一样的地方有不一样的实现,浏览器和NodeJS
基于不一样的技术实现了各自的Event Loop
。浏览器的Event Loop
是在HTML5
的规范中明肯定义,NodeJS
的Event Loop
是基于libuv
实现的。
在浏览器中的Event Loop
由执行栈Execution Stack
、后台线程Background Threads
、宏队列Macrotask Queue
、微队列Microtask Queue
组成。html
setTimeout
、setInterval
、XMLHttpRequest
等等的执行线程。setTimeout
、setInterval
、setImmediate(Node)
、requestAnimationFrame
、UI rendering
、I/O
等操做Promise
、process.nextTick(Node)
、Object.observe
、MutationObserver
等操做当Js
执行时,进行以下流程vue
// Step 1 console.log(1); // Step 2 setTimeout(() => { console.log(2); Promise.resolve().then(() => { console.log(3); }); }, 0); // Step 3 new Promise((resolve, reject) => { console.log(4); resolve(); }).then(() => { console.log(5); }) // Step 4 setTimeout(() => { console.log(6); }, 0); // Step 5 console.log(7); // Step N // ... // Result /* 1 4 7 5 2 3 6 */
// 执行栈 console // 微队列 [] // 宏队列 [] console.log(1); // 1
// 执行栈 setTimeout // 微队列 [] // 宏队列 [setTimeout1] setTimeout(() => { console.log(2); Promise.resolve().then(() => { console.log(3); }); }, 0);
// 执行栈 Promise // 微队列 [then1] // 宏队列 [setTimeout1] new Promise((resolve, reject) => { console.log(4); // 4 // Promise是个函数对象,此处是同步执行的 // 执行栈 Promise console resolve(); }).then(() => { console.log(5); })
// 执行栈 setTimeout // 微队列 [then1] // 宏队列 [setTimeout1 setTimeout2] setTimeout(() => { console.log(6); }, 0);
// 执行栈 console // 微队列 [then1] // 宏队列 [setTimeout1 setTimeout2] console.log(7); // 7
// 执行栈 then1 // 微队列 [] // 宏队列 [setTimeout1 setTimeout2] console.log(5); // 5
// 执行栈 setTimeout1 // 微队列 [then2] // 宏队列 [setTimeout2] console.log(2); // 2 Promise.resolve().then(() => { console.log(3); });
// 执行栈 then2 // 微队列 [] // 宏队列 [setTimeout2] console.log(3); // 3
// 执行栈 setTimeout2 // 微队列 [] // 宏队列 [] console.log(6); // 6
在了解异步任务的执行队列后,回到中$nextTick
方法,当用户数据更新时,Vue
将会维护一个缓冲队列,对于全部的更新数据将要进行的组件渲染与DOM
操做进行必定的策略处理后加入缓冲队列,而后便会在$nextTick
方法的执行队列中加入一个flushSchedulerQueue
方法(这个方法将会触发在缓冲队列的全部回调的执行),而后将$nextTick
方法的回调加入$nextTick
方法中维护的执行队列,在异步挂载的执行队列触发时就会首先会首先执行flushSchedulerQueue
方法来处理DOM
渲染的任务,而后再去执行$nextTick
方法构建的任务,这样就能够实如今$nextTick
方法中取得已渲染完成的DOM
结构。在测试的过程当中发现了一个颇有意思的现象,在上述例子中的加入两个按钮,在点击updateMsg
按钮的结果是3 2 1
,点击updateMsgTest
按钮的运行结果是2 3 1
。java
<!DOCTYPE html> <html> <head> <title>Vue</title> </head> <body> <div id="app"></div> </body> <script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { msg: 'Vue' }, template:` <div> <div ref="msgElement">{{msg}}</div> <button @click="updateMsg">updateMsg</button> <button @click="updateMsgTest">updateMsgTest</button> </div> `, methods:{ updateMsg: function(){ this.msg = "Update"; setTimeout(() => console.log(1)) Promise.resolve().then(() => console.log(2)) this.$nextTick(() => { console.log(3) }) }, updateMsgTest: function(){ setTimeout(() => console.log(1)) Promise.resolve().then(() => console.log(2)) this.$nextTick(() => { console.log(3) }) } }, }) </script> </html>
这里假设运行环境中Promise
对象是彻底支持的,那么使用setTimeout
是宏队列在最后执行这个是没有异议的,可是使用$nextTick
方法以及自行定义的Promise
实例是有执行顺序的问题的,虽然都是微队列任务,可是在Vue
中具体实现的缘由致使了执行顺序可能会有所不一样,首先直接看一下$nextTick
方法的源码,关键地方添加了注释,请注意这是Vue2.4.2
版本的源码,在后期$nextTick
方法可能有所变动。git
/** * Defer a task to execute it asynchronously. */ var nextTick = (function () { // 闭包 内部变量 var callbacks = []; // 执行队列 var pending = false; // 标识,用以判断在某个事件循环中是否为第一次加入,第一次加入的时候才触发异步执行的队列挂载 var timerFunc; // 以何种方法执行挂载异步执行队列,这里假设Promise是彻底支持的 function nextTickHandler () { // 异步挂载的执行任务,触发时就已经正式准备开始执行异步任务了 pending = false; // 标识置false var copies = callbacks.slice(0); // 建立副本 callbacks.length = 0; // 执行队列置空 for (var i = 0; i < copies.length; i++) { copies[i](); // 执行 } } // the nextTick behavior leverages the microtask queue, which can be accessed // via either native Promise.then or MutationObserver. // MutationObserver has wider support, however it is seriously bugged in // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It // completely stops working after triggering a few times... so, if native // Promise is available, we will use it: /* istanbul ignore if */ if (typeof Promise !== 'undefined' && isNative(Promise)) { var p = Promise.resolve(); var logError = function (err) { console.error(err); }; timerFunc = function () { p.then(nextTickHandler).catch(logError); // 挂载异步任务队列 // in problematic UIWebViews, Promise.then doesn't completely break, but // it can get stuck in a weird state where callbacks are pushed into the // microtask queue but the queue isn't being flushed, until the browser // needs to do some other work, e.g. handle a timer. Therefore we can // "force" the microtask queue to be flushed by adding an empty timer. if (isIOS) { setTimeout(noop); } }; } else if (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 IE11, iOS7, Android 4.4 var counter = 1; var observer = new MutationObserver(nextTickHandler); var textNode = document.createTextNode(String(counter)); observer.observe(textNode, { characterData: true }); timerFunc = function () { counter = (counter + 1) % 2; textNode.data = String(counter); }; } else { // fallback to setTimeout /* istanbul ignore next */ timerFunc = function () { setTimeout(nextTickHandler, 0); }; } return function queueNextTick (cb, ctx) { // nextTick方法真正导出的方法 var _resolve; callbacks.push(function () { // 添加到执行队列中 并加入异常处理 if (cb) { try { cb.call(ctx); } catch (e) { handleError(e, ctx, 'nextTick'); } } else if (_resolve) { _resolve(ctx); } }); //判断在当前事件循环中是否为第一次加入,如果第一次加入则置标识为true并执行timerFunc函数用以挂载执行队列到Promise // 这个标识在执行队列中的任务将要执行时便置为false并建立执行队列的副本去运行执行队列中的任务,参见nextTickHandler函数的实现 // 在当前事件循环中置标识true并挂载,而后再次调用nextTick方法时只是将任务加入到执行队列中,直到挂载的异步任务触发,便置标识为false而后执行任务,再次调用nextTick方法时就是一样的执行方式而后不断如此往复 if (!pending) { pending = true; timerFunc(); } if (!cb && typeof Promise !== 'undefined') { return new Promise(function (resolve, reject) { _resolve = resolve; }) } } })();
回到刚才提出的问题上,在更新DOM
操做时会先触发$nextTick
方法的回调,解决这个问题的关键在于谁先将异步任务挂载到Promise
对象上。
首先对有数据更新的updateMsg
按钮触发的方法进行debug
,断点设置在Vue.js
的715
行,版本为2.4.2
,在查看调用栈以及传入的参数时能够观察到第一次执行$nextTick
方法的实际上是因为数据更新而调用的nextTick(flushSchedulerQueue);
语句,也就是说在执行this.msg = "Update";
的时候就已经触发了第一次的$nextTick
方法,此时在$nextTick
方法中的任务队列会首先将flushSchedulerQueue
方法加入队列并挂载$nextTick
方法的执行队列到Promise
对象上,而后才是自行自定义的Promise.resolve().then(() => console.log(2))
语句的挂载,当执行微任务队列中的任务时,首先会执行第一个挂载到Promise
的任务,此时这个任务是运行执行队列,这个队列中有两个方法,首先会运行flushSchedulerQueue
方法去触发组件的DOM
渲染操做,而后再执行console.log(3)
,而后执行第二个微队列的任务也就是() => console.log(2)
,此时微任务队列清空,而后再去宏任务队列执行console.log(1)
。
接下来对于没有数据更新的updateMsgTest
按钮触发的方法进行debug
,断点设置在一样的位置,此时没有数据更新,那么第一次触发$nextTick
方法的是自行定义的回调函数,那么此时$nextTick
方法的执行队列才会被挂载到Promise
对象上,很显然在此以前自行定义的输出2
的Promise
回调已经被挂载,那么对于这个按钮绑定的方法的执行流程即是首先执行console.log(2)
,而后执行$nextTick
方法闭包的执行队列,此时执行队列中只有一个回调函数console.log(3)
,此时微任务队列清空,而后再去宏任务队列执行console.log(1)
。
简单来讲就是谁先挂载Promise
对象的问题,在调用$nextTick
方法时就会将其闭包内部维护的执行队列挂载到Promise
对象,在数据更新时Vue
内部首先就会执行$nextTick
方法,以后便将执行队列挂载到了Promise
对象上,其实在明白Js
的Event Loop
模型后,将数据更新也看作一个$nextTick
方法的调用,而且明白$nextTick
方法会一次性执行全部推入的回调,就能够明白其执行顺序的问题了,下面是一个关于$nextTick
方法的最小化的DEMO
。github
var nextTick = (function(){ var pending = false; const callback = []; var p = Promise.resolve(); var handler = function(){ pending = true; callback.forEach(fn => fn()); } var timerFunc = function(){ p.then(handler); } return function queueNextTick(fn){ callback.push(() => fn()); if(!pending){ pending = true; timerFunc(); } } })(); (function(){ nextTick(() => console.log("触发DOM渲染队列的方法")); // 注释 / 取消注释 来查看效果 setTimeout(() => console.log(1)) Promise.resolve().then(() => console.log(2)) nextTick(() => { console.log(3) }) })();
https://github.com/WindrunnerMax/EveryDay
https://www.jianshu.com/p/e7ce7613f630 https://cn.vuejs.org/v2/api/#vm-nextTick https://segmentfault.com/q/1010000021240464 https://juejin.im/post/5d391ad8f265da1b8d166175 https://juejin.im/post/5ab94ee251882577b45f05c7 https://juejin.im/post/5a45fdeb6fb9a044ff31c9a8