咱們都知道vue是數據驅動視圖,而vue中視圖更新是異步的。在業務開發中,有沒有經歷過當改變了數據,視圖卻沒有按照咱們的指望渲染?而須要將對應的操做放在nextTick中視圖才能按照預期的渲染,有的時候nextTick也不能生效,而須要利用setTimeout來解決?html
搞清楚這些問題,那麼就須要搞明白如下幾個問題:
一、vue中究竟是如何來實現異步更新視圖;
二、vue爲何要異步更新視圖;
三、nextTick的原理;
四、nextTick如何來解決數據改變視圖不更新的問題的;
五、nextTick的使用場景。vue
如下分享個人思考過程。react
vue中每一個組件實例都對應一個 watcher實例,它會在組件渲染的過程當中把「接觸」過的數據屬性記錄爲依賴。以後當依賴項的 setter 觸發時,會通知 watcher,從而使它關聯的組件從新渲染。
若是對vue視圖渲染的思想還不是很清楚,能夠參考這篇defineProperty實現視圖渲染用defineProty模擬的Vue的渲染視圖,來了解整個視圖渲染的思想。git
可是Vue的視圖渲染是異步的,異步的過程是數據改變不會當即更新視圖,當數據所有修改完,最後再統一進行視圖渲染。github
在渲染的過程當中,中間有一個對虛擬dom進行差別化的計算過程(diff算法),大量的修改帶來頻繁的虛擬dom差別化計算,從而致使渲染性能下降,異步渲染正是對視圖渲染性能的優化。算法
/** * Subscriber interface. * Will be called when a dependency changes. */ update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } }
/** * Push a watcher into the watcher queue. * Jobs with duplicate IDs will be skipped unless it's * pushed when the queue is being flushed. */ 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 nextTick(flushSchedulerQueue) } } }
當第一次依賴有變化就會調用nextTick方法,將更新視圖的回調設置成微任務或宏任務,而後後面依賴更新對應的watcher對象都只是被加入到隊列中,只有當nextTick回調執行以後,纔會遍歷調用隊列中的watcher對象中的更新方法更新視圖。chrome
這個nextTick和咱們在業務中調用的this.$nextTick()是同一個函數。express
if (!waiting) { waiting = true nextTick(flushSchedulerQueue) }
flushSchedulerQueue刷新隊列的函數,用於更新視圖segmentfault
function flushSchedulerQueue () { 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] id = watcher.id has[id] = null watcher.run() // in dev build, check and stop circular updates. if (process.env.NODE_ENV !== 'production' && has[id] != null) { circular[id] = (circular[id] || 0) + 1 if (circular[id] > MAX_UPDATE_COUNT) { warn( 'You may have an infinite update loop ' + ( watcher.user ? `in watcher with expression "${watcher.expression}"` : `in a component render function.` ), watcher.vm ) break } } }
那麼nextTick究竟是個什麼東西呢?數組
vue 2.5中nextTick的源碼以下(也能夠跳過源碼直接看後面的demo,來理解nextTick的用處):
/** * Defer a task to execute it asynchronously. */ export const nextTick = (function () { const callbacks = [] let pending = false let timerFunc function nextTickHandler () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } } // An asynchronous deferring mechanism. // In pre 2.4, we used to use microtasks (Promise/MutationObserver) // but microtasks actually has too high a priority and fires in between // supposedly sequential events (e.g. #4521, #6690) or even between // bubbling of the same event (#6566). Technically setImmediate should be // the ideal choice, but it's not available everywhere; and the only polyfill // that consistently queues the callback after all DOM events triggered in the // same loop is by using MessageChannel. /* istanbul ignore if */ if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = () => { setImmediate(nextTickHandler) } } else if (typeof MessageChannel !== 'undefined' && ( isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === '[object MessageChannelConstructor]' )) { const channel = new MessageChannel() const port = channel.port2 channel.port1.onmessage = nextTickHandler timerFunc = () => { port.postMessage(1) } } else /* istanbul ignore next */ if (typeof Promise !== 'undefined' && isNative(Promise)) { // use microtask in non-DOM environments, e.g. Weex const p = Promise.resolve() timerFunc = () => { p.then(nextTickHandler) } } else { // fallback to setTimeout timerFunc = () => { setTimeout(nextTickHandler, 0) } } return function queueNextTick (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, reject) => { _resolve = resolve }) } } })()
用下面這個demo來感覺依賴更新時和nextTick的關係以及nextTick的用處:
function isNative(Ctor) { return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) } const nextTick = (function () { let pending = false; let callbacks = [] let timerFunc function nextTickHandler() { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } } if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = () => { setImmediate(nextTickHandler) } } else if (typeof MessageChannel !== 'undefined' && ( isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === '[object MessageChannelConstructor]' )) { const channel = new MessageChannel() const port = channel.port2 channel.port1.onmessage = nextTickHandler timerFunc = () => { port.postMessage(1) } } else /* istanbul ignore next */ if (typeof Promise !== 'undefined' && isNative(Promise)) { // use microtask in non-DOM environments, e.g. Weex const p = Promise.resolve() timerFunc = () => { p.then(nextTickHandler) } } else { // fallback to setTimeout timerFunc = () => { setTimeout(nextTickHandler, 0) } } console.log('timerFunc:', timerFunc) return function queueNextTick(cb, ctx) { callbacks.push(() => { if (cb) { cb.call(ctx) } }) // console.log('callbacks:', callbacks) if (!pending) { pending = true console.log('pending...', true) timerFunc() } } })() // 模擬異步視圖更新 // 第一次先將對應新值添加到一個數組中,而後調用一次nextTick,將讀取數據的回調做爲nextTick的參數 // 後面的新值直接添加到數組中 console.time() let arr = [] arr.push(99999999) nextTick(() => { console.log('nextTick one:', arr, arr.length) }) function add(len) { for (let i = 0; i < len; i++) { arr.push(i) console.log('i:', i) } } add(4) // console.timeEnd() // add() // add() nextTick(() => { arr.push(888888) console.log('nextTick two:', arr, arr.length) }) add(8)的值以後 console.timeEnd()
在chrome運行結果以下:
能夠看到第二個nextTick中push的值最後渲染在add(8)的值以後,這也就是nextTick的做用了,nextTick的做用就是用來處理須要在數據更新(在vue中手動調用nextTick時對應的是dom更新完成後)完才執行的操做。
nextTick的原理:
首先nextTick會將外部傳進的函數回調存在內部數組中,nextTick內部有一個用來遍歷這個內部數組的函數nextTickHandler,而這個函數的執行是異步的,何時執行取決於這個函數是屬於什麼類型的異步任務:微任務or宏任務。
主線程執行完,就會去任務隊列中取任務到主線程中執行,任務隊列中包含了微任務和宏任務,首先會取微任務,微任務執行完就會取宏任務執行,依此循環。nextTickHandler設置成微任務或宏任務就能保證其老是在數據修改完或者dom更新完而後再執行。(js執行機制能夠看promise時序問題&js執行機制)
爲何vue中對設置函數nextTickHandler的異步任務類型會有以下幾種判斷?
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = () => { setImmediate(nextTickHandler) } } else if (typeof MessageChannel !== 'undefined' && ( isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === '[object MessageChannelConstructor]' )) { const channel = new MessageChannel() const port = channel.port2 channel.port1.onmessage = nextTickHandler timerFunc = () => { port.postMessage(1) } } else /* istanbul ignore next */ if (typeof Promise !== 'undefined' && isNative(Promise)) { // use microtask in non-DOM environments, e.g. Weex const p = Promise.resolve() timerFunc = () => { p.then(nextTickHandler) } } else { // fallback to setTimeout timerFunc = () => { setTimeout(nextTickHandler, 0) } }
瀏覽器環境中常見的異步任務種類,按照優先級:
macro task
:同步代碼、setImmediate
、MessageChannel
、setTimeout/setInterval
micro task
:Promise.then
、MutationObserver
而爲何最後才判斷使用setTimeout?
vue中目的就是要儘量的快地執行回調渲染視圖,而setTimeout有最小延遲限制:若是嵌套深度超過5級,setTimeout(回調,0)就會有4ms的延遲。
因此首先選用執行更快的setImmediate,可是setImmediate有兼容性問題,目前只支持Edge、Ie瀏覽器:
能夠用一樣執行比setTimeout更快的宏任務MessageChannel來代替setImmediate。MessageChannel兼容性以下:
當以上都不支持的時候,就使用new Promise().then(),將回調設置成微任務,Promise不支持才使用setTimeout。
總結:
nextTick就是利用了js機制執行任務的規則,將nextTick的回調函數設置成宏任務或微任務來達到在主線程的操做執行完,再執行的目的。
在vue中主要提供對依賴Dom更新完成後再作操做的狀況的支持
當改變數據,視圖沒有按預期渲染時;都應該考慮是不是由於本須要在dom執行完再執行,然而實際卻在dom沒有執行完就執行了代碼,若是是就考慮使用將邏輯放到nextTick中,有的時候業務操做複雜,有些操做可能須要更晚一些執行,放在nextTick中仍然沒有達到預期效果,這個時候能夠考慮使用setTimeout,將邏輯放到宏任務中。
基於以上分析,能夠列舉幾個nextTick經常使用到的使用場景:
// input 定位 scrollToInputBottom() { this.$nextTick(() => { this.$refs.accept_buddy_left.scrollTop = this.$refs.accept_buddy_left.scrollTop + 135 this.$refs.accept_buddy_ipt[ this.$refs.accept_buddy_ipt.length - 1 ].$refs.ipt.focus() }) },
// 監聽來自 url 的期數變化,跳到該期數 urlInfoTerm: { immediate: true, handler(val) { if (val !== 0) { this.$nextTick(function() { // 計算期數所在位置的高度 this.setCellsHeight() //設置滾動距離 this.spaceLenght = this.getColumnPositionIndex( this.list, ) setTimeout(() => { this.setScrollPosition(val) }, 800) }) } },