從 4 個要點說透 Vue nextTick
前言
nextTick
在 Vue 中是一個很出名的工具函數,我們在實際運用的時候也經常會用到,那麼它實際上到底有什麼樣的作用,Vue 中又是如何設計的,我們在日常中有什麼場景是可以借鑑的。
我們以 Vue 最新的 v2.6.14 版本來分析,鏈接 https://github.com/vuejs/vue/blob/v2.6.14/src/core/util/next-tick.js
正文分析
What
nextTick
是個什麼東西,參考 Vue 2 的官方 API 文檔:https://cn.vuejs.org/v2/api/#Vue-nextTick
可以看出是執行一個回調函數,我們這裏可以成爲一個任務,那在 Vue 中文檔已經講明白了,在下次 DOM 更新循環結束後執行這個任務(回調),這樣你就可以取到更新後的 DOM 了。
How
先來看下 nextTick 全部的代碼,把 flow 相關去掉,加上我們自己的註釋:
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
// 是否使用的是 MicroTask,如 Promise MutationObserver
// 如果瀏覽器不支持 則會使用 MacroTask setImmediate setTimeout
// 相關進一步知識可以參考 瀏覽器 eventloop 相關文章
export let isUsingMicroTask = false
// 儲存所有的 callback 隊列,可以認爲是一個個任務
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]()
}
}
// 實現異步的函數,從名字上看下一個 tick,即一個 timer
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// 原生 Promise 異步
const p = Promise.resolve()
timerFunc = () => {
// 利用 promise.then 實現,一個 micro task 之後執行 flushCallbacks
p.then(flushCallbacks)
// 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)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// 降級使用 MutationObserver
// 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 = () => {
// 觸發textNode的改變,進而觸發MutationObserver的回調執行 flushCallbacks
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
// 直接利用 setImmediate
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
// 經典的 setTimeout
setTimeout(flushCallbacks, 0)
}
}
// 主實現
export function nextTick (cb, ctx) {
let _resolve
// 往 callbacks 隊列中添加一個一個任務
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// 如果不是在等待中,即上一輪的callbacks任務隊列已經執行完畢
// 那麼就進入等待狀態,重新進入新一輪的等待下一個timer然後執行新一輪存下來的callbacks任務隊列
if (!pending) {
pending = true
timerFunc()
}
// nextTick的另一種用法,nextTick().then()
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
我們可以看出來,代碼雖然不多,但是處理的情況還是很多的,也有很多兼容性的處理。如果我們來翻譯下,nextTick 最核心的實現就是:拿一個隊列存儲所有要執行的任務,在下一個 tick(異步)執行這些任務。
那根據這個核心實現,在不考慮兼容性和異常的情況下,我們可以實現一個極簡版本的 nextTick:
let pending = false
const tasks = []
const flushCallbacks = () => {
pending = false
tasks.forEach(task => task())
tasks.length = 0
}
const p = Promise.resolve()
const timerFunc = () => {
p.then(flushCallbacks)
}
function nextTick(task) {
tasks.push(task)
if (!pending) {
pending = true
timerFunc()
}
}
短短 20 行,但是功能很核心也很強大,我們可以像這樣使用:
const task1 = () => console.log('1')
const task2 = () => console.log('2')
console.log('before')
nextTick(task1)
nextTick(task2)
console.log('after')
// 運行的結果:before after 1 2
這個時候,相信你已經更進一步理解了 nextTick
:將需要異步執行的任務收集起來在下一個 tick 依次執行他們。
Why
那爲什麼需要 nextTick
呢,我們不能直接執行這些任務嗎?在 Vue 中的話,官網也給到了大家答案,詳情 https://cn.vuejs.org/v2/guide/reactivity.html#%E5%BC%82%E6%AD%A5%E6%9B%B4%E6%96%B0%E9%98%9F%E5%88%97。
如果簡化來理解的話就是:爲了更好的性能,將更新 DOM 操作存放在異步更新隊列中,在下一個 tick 統一進行更新 DOM 操作。
試想下,如果我們每更新一次數據,Vue 就需要去更新一次 DOM 操作的話,得有多卡頓,因爲日常我們處理邏輯一定是這樣的:
const data = {
title: 'hello',
desc: 'world'
}
this.msg = data.title
this.context = data.desc
這個還是一個局部場景,更別想說,我們的整個 Vue 應用的數據更新,DOM 更新了。
所以 Vue 中就採用了異步更新隊列這種方式來進行優化,也就是依賴上邊我們分析的 nextTick
所做的最核心的事情。
總結
在 nextTick
之中,我們可以從其中學到什麼或者我們可以進一步瞭解什麼呢?
隊列
看出這裏邊對於隊列的操作(當然,用數組模擬的,本質是一樣的):隊列裏添加任務,執行隊列裏的任務,清空隊列。
隊列是一個我們十分常用的數據結構,上邊所提到的 eventloop,你會發現和 nextTick 本質是一樣的,只是變得更復雜了,存在多個隊列的情況,需要處理。
異步
我們知道了部分 timerFunc
的實現,相對應的也就是我們需要知道,哪些 API 的操作是異步的,以及是哪種異步處理(MacroTask、MicroTask),他們之間有什麼差異和使用的影響,我們遇到異步場景的時候應該如何去選擇。
還有一個點,這裏用到了降級的方案 setTimeout
,傳的第二個參數是 0,那麼這個時候的效果是啥樣的; setTimeout 還可以有其他的什麼用法,到底可以有幾個參數,返回值是啥類型的,什麼時候需要我們手工去 clearTimeout。
相對應的延伸,就是大名鼎鼎的 eventloop 相關知識,也需要去區分瀏覽器環境和 Node.js 環境。
異步和隊列碰撞在一起,可以有很多火花。
我們有很多時候時候都需要處理異步任務,而對於這些任務的處理,最合適的數據結構就是隊列了,例如大名鼎鼎的 async 庫 https://github.com/caolan/async 簡直就是把異步玩到了極致,裏邊有很多很好的實現思路以及技巧,感興趣的也是可以深入瞭解的。
我們的現實需求也一樣,例如,在小程序場景中,不能超出 10 個的併發請求,超出的請求會被取消掉,所以我們需要對請求進行封裝一層,在 mpx 中是封裝爲了 mpx-fetch,而且我們還要求了高低優先級兩種請求,這種情況,就需要我們藉助於隊列來實現我們的需求。
數組循環
在 flushCallbacks
中,我們看到了一個技巧,正常我們自己的簡單實現中,是直接便利 callbacks 然後執行的,而 Vue 中則不是,他是複製了一份新的,然後循環執行的。
這麼做的原因,其實是考慮了一種特殊情況,如果某一個 callback 執行的時候,又一次調用了 nextTick,進而更新了 callbacks,那這個時候的執行就不是我們所期望的了。所以需要先拷貝一份原有的,即使在 cb 中更新了 callbacks 也不影響我們的循環和執行,符合預期。
這是一個很嚴謹的地方,我們在實際場景中,也要有這種思考和意識。
同時這個問題還可以有很多的延伸,針對於數組循環,正向循環和逆向循環有啥區別嗎,是不是都一樣;以及我們用 for 循環和用數組本身的 forEach 會有啥不一樣嗎;還有 for 循環的終止條件,我們寫 i < array.length
和 const len = array.length; i < len
有啥區別沒有?
Promise
Promise 是一個很好的東西,相當有用,我們需要深入理解並使用它。這裏有一個比較有意思的一個點是 nextTick
的返回值處理,應用到了一個技巧:外部如何更新 Promise 的狀態,即你所看到的 _resolve
這個變量的作用。
Promise,一個各大廠基本都在考察的,Promise 有哪些規範,包含哪些定義,哪些 API,如何實現一個 Promise。
希望你去深入學習和理解它,做到精通 Promise!
其他小 Tips
-
isNative
的處理,他是如何判斷的 -
pending,防重
-
錯誤如何處理
-
如何考慮兼容和降級
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/vXPfiOM3QevqMVqGlKevQA