nextTick 是什么
$nextTick:根据官方文档的解释,它可以在 DOM 更新完毕之后执行一个回调函数,并返回一个 Promise(如果支持的话)
1// 修改数据 2vm.msg = "Hello"; 3 4// DOM 还没有更新 5Vue.nextTick(function() { 6 // DOM 更新了 7});
这块理解 EventLoop 的应该一看就懂,其实就是在下一次事件循环开始时开始更新 DOM,避免中间频繁的操作引起页面的重绘和回流。
这块引用官方文档:
可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。 这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的
Promise.then、MutationObserver和setImmediate,如果执行环境不支持,则会采用setTimeout(fn, 0)代替。
列如当设置vm.text = 'new value'时,该组件不会立即重新渲染,当刷新队列时,组件会在下一个事件循环‘tick’中更新,
1<div id="example">{{message}}</div> 2var vm = new Vue({ 3 el: '#example', 4 data: { 5 message: '123' 6 } 7}) 8vm.message = 'new message' // 更改数据 9vm.$el.textContent === 'new message' // false 10Vue.nextTick(function () { 11 vm.$el.textContent === 'new message' // true 12})
一般在设置了this.xx='xx'数据后,立即得到最新的 DOM 数据时,才会用到$nextTick,因为 DOM 的更新是异步进行的,所以获取需要用到这个方法。
更新流程(源码解析)
- 当数据被修改时,watcher 会侦听到变化,然后会将变化进行入队:
1/* 2 * Subscriber interface. 3 * Will be called when a dependency changes. 4 */ 5Watcher.prototype.update = function update() { 6 /* istanbul ignore else */ 7 if (this.lazy) { 8 this.dirty = true; 9 } else if (this.sync) { 10 this.run(); 11 } else { 12 queueWatcher(this); 13 } 14};
- 并使用 nextTick 方法添加一个 flushScheduleQueue 回调
1/** 2 * Push a watcher into the watcher queue. 3 * Jobs with duplicate IDs will be skipped unless it's 4 * pushed when the queue is being flushed. 5 */ 6function queueWatcher(watcher) { 7 var id = watcher.id; 8 if (has[id] == null) { 9 has[id] = true; 10 if (!flushing) { 11 queue.push(watcher); 12 } else { 13 // if already flushing, splice the watcher based on its id 14 // if already past its id, it will be run next immediately. 15 var i = queue.length - 1; 16 while (i > index && queue[i].id > watcher.id) { 17 i--; 18 } 19 queue.splice(i + 1, 0, watcher); 20 } 21 // queue the flush 22 if (!waiting) { 23 waiting = true; 24 25 if (!config.async) { 26 flushSchedulerQueue(); 27 return; 28 } 29 nextTick(flushSchedulerQueue); 30 } 31 } 32}
- flushScheduleQueue 加入到 callback 数组中,并且异步执行
1function nextTick(cb, ctx) { 2 var _resolve; 3 callbacks.push(function() { 4 if (cb) { 5 try { 6 cb.call(ctx); // !! cb 就是加入的回调 7 } catch (e) { 8 handleError(e, ctx, "nextTick"); 9 } 10 } else if (_resolve) { 11 _resolve(ctx); 12 } 13 }); 14 if (!pending) { 15 // 异步执行 操作 见timerFunc 16 pending = true; 17 timerFunc(); 18 } 19 // $flow-disable-line 20 if (!cb && typeof Promise !== "undefined") { 21 return new Promise(function(resolve) { 22 _resolve = resolve; 23 }); 24 } 25}
- timerFunc 操作就是异步执行了依次判断使用:Promise.then=>MutationObserver=>setImmediate=>setTimeout
1var timerFunc; 2 3if (typeof Promise !== "undefined" && isNative(Promise)) { 4 var p = Promise.resolve(); 5 timerFunc = function() { 6 p.then(flushCallbacks); 7 // 1. Promise.then 8 if (isIOS) { 9 setTimeout(noop); 10 } 11 }; 12 isUsingMicroTask = true; 13} else if ( 14 !isIE && 15 typeof MutationObserver !== "undefined" && 16 (isNative(MutationObserver) || 17 MutationObserver.toString() === "[object MutationObserverConstructor]") 18) { 19 // 2. MutationObserver 20 var counter = 1; 21 var observer = new MutationObserver(flushCallbacks); 22 var textNode = document.createTextNode(String(counter)); 23 observer.observe(textNode, { 24 characterData: true, 25 }); 26 timerFunc = function() { 27 counter = (counter + 1) % 2; 28 textNode.data = String(counter); 29 }; 30 isUsingMicroTask = true; 31} else if (typeof setImmediate !== "undefined" && isNative(setImmediate)) { 32 // 3. setImmediate 33 timerFunc = function() { 34 setImmediate(flushCallbacks); 35 }; 36} else { 37 //4. setTimeout 38 timerFunc = function() { 39 setTimeout(flushCallbacks, 0); 40 }; 41}
- flushCallbacks 遍历所有的 callbacks 并执行
1function flushCallbacks() { 2 pending = false; 3 var copies = callbacks.slice(0); 4 callbacks.length = 0; 5 for (var i = 0; i < copies.length; i++) { 6 copies[i](); 7 } 8}
- 其中就有前面加入的 flushScheduleQueue,利用 queue 中的 watcher 的 run 方法,更新组件
1for (index = 0; index < queue.length; index++) { 2 watcher = queue[index]; 3 watcher.run(); 4}
总结
以上就是 vue 的 nextTick 方法的实现原理了,总结一下就是:
-
Vue 用异步队列的方式来控制 DOM 更新和 nextTick 回调先后执行
-
microtask 因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕
-
因为兼容性问题,vue 不得不做了 microtask 向 macrotask 的降级方案
