nextTick
在 Vue 中是一個很出名的工具函數,咱們在實際運用的時候也常常會用到,那麼它實際上到底有什麼樣的做用,Vue 中又是如何設計的,咱們在平常中有什麼場景是能夠借鑑的。html
咱們以 Vue 最新的 v2.6.14 版原本分析,連接 github.com/vuejs/vue/b…前端
nextTick
是個什麼東西,參考 Vue 2 的官方 API 文檔:cn.vuejs.org/v2/api/#Vue…vue
能夠看出是執行一個回調函數,咱們這裏能夠成爲一個任務,那在 Vue 中文檔已經講明白了,在下次 DOM 更新循環結束後執行這個任務(回調),這樣你就能夠取到更新後的 DOM 了。react
先來看下 nextTick 所有的代碼,把flow相關去掉,加上咱們本身的註釋:git
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(異步)執行這些任務。github
那根據這個核心實現,在不考慮兼容性和異常的狀況下,咱們能夠實現一個極簡版本的 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行,可是功能很核心也很強大,咱們能夠像這樣使用:api
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 依次執行他們。數組
那爲何須要 nextTick
呢,咱們不能直接執行這些任務嗎?在 Vue 中的話,官網也給到了你們答案,詳情 cn.vuejs.org/v2/guide/re…promise
若是簡化來理解的話就是:爲了更好的性能,將更新 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 庫 github.com/caolan/asyn… 簡直就是把異步玩到了極致,裏邊有不少很好的實現思路以及技巧,感興趣的也是能夠深刻了解的。
咱們的現實需求也同樣,例如,在小程序場景中,不能超出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 是一個很好的東西,至關有用,咱們須要深刻理解並使用它。這裏有一個比較有意思的一個點是 nextTick
的返回值處理,應用到了一個技巧:外部如何更新 Promise 的狀態,即你所看到的 _resolve
這個變量的做用。
Promise,一個各大廠基本都在考察的,Promise有哪些規範,包含哪些定義,哪些API,如何實現一個 Promise。
但願你去深刻學習和理解它,作到精通 Promise!
isNative
的處理,他是如何判斷的滴滴前端技術團隊的團隊號已經上線,咱們也同步了必定的招聘信息,咱們也會持續增長更多職位,有興趣的同窗能夠一塊兒聊聊。