本文是本人的一些拙見,若有錯誤請觀衆老爺們指出。vue
咱們爲何須要nextTick
?考慮以下的場景。若是每一次foo
的變化,都會同步的觸發watch
的更新。那麼,若是watch
中包含了大量耗時的操做,則會形成嚴重的性能問題。因此在Vue的源碼中,watch
的更新發生在nextTick
以後。數組
const Demo = createComponent({
setup() {
const foo = ref(0)
const bar = ref(0)
const change = () => {
for (let i = 0; i < 100; i++) {
foo.value += 1
}
}
watch(foo, () => {
bar.value += 1
}, {
lazy: true
})
return { foo, bar, change }
},
render() {
const { foo, bar, change } = this
return (
<div>
<p>foo: {foo}</p>
<p>bar: {bar}</p>
{/* 點擊按鈕,bar實際上只會更新一次 */}
<button onClick={change}>change</button>
</div>
)
}
})
複製代碼
快速瞭解源碼的最好辦法是閱讀對應的單元測試。能夠幫助咱們快速的瞭解,每個函數,每個變量的具體含義和用法,以及一些邊界狀況的處理。markdown
nextTick
源碼的文件目錄位置:packages/runtime-core/src/scheduler.ts
async
nextTick
單元測試文件的文件目錄位置:packages/runtime-core/__tests__/scheduler.spec.ts
ide
nextTick
會建立一個微任務。當宏任務job2
執行完成後,清空微任務隊列,執行job1
。此時calls
數組的長度等於2。函數
it('nextTick', async () => { const calls: string[] = [] const dummyThen = Promise.resolve().then() const job1 = () => { calls.push('job1') } const job2 = () => { calls.push('job2') } nextTick(job1) job2() expect(calls.length).toBe(1) // 等待微任務隊列被清空 await dummyThen expect(calls.length).toBe(2) expect(calls).toMatchObject(['job2', 'job1']) }) 複製代碼
這裏涉及到一個新的函數,queueJob
。目前還不清楚其內部的實現,不過咱們能夠從單測中能夠看出來。queueJob
接受一個函數做爲參數,queueJob
會將參數按順序保存到一個隊列中,當宏任務執行完成後,微任務開始執行時,依次執行隊列中的函數。post
it('basic usage', async () => { const calls: string[] = [] const job1 = () => { calls.push('job1') } const job2 = () => { calls.push('job2') } queueJob(job1) queueJob(job2) expect(calls).toEqual([]) await nextTick() // 按照順序執行 expect(calls).toEqual(['job1', 'job2']) }) 複製代碼
queueJob
會避免同一個函數(job),屢次push到隊列之中。queueJob
包含了去重的處理。性能
it('should dedupe queued jobs', async () => { const calls: string[] = [] const job1 = () => { calls.push('job1') } const job2 = () => { calls.push('job2') } queueJob(job1) queueJob(job2) queueJob(job1) queueJob(job2) expect(calls).toEqual([]) await nextTick() expect(calls).toEqual(['job1', 'job2']) }) 複製代碼
若是queueJob(job2)
的調用發生在job1
的內部。job2
將會在job1
以後同一時間執行。不會等到下一次執行微任務時。單元測試
it('queueJob while flushing', async () => { const calls: string[] = [] const job1 = () => { calls.push('job1') queueJob(job2) } const job2 = () => { calls.push('job2') } queueJob(job1) await nextTick() // job2會在同一個微任務隊列執行期間被執行 expect(calls).toEqual(['job1', 'job2']) }) 複製代碼
這裏又出現了一個新的函數queuePostFlushCb
。目前依然尚不清楚其內部的實現,不過咱們能夠從單測中能夠看出來,queuePostFlushCb
接受函數做爲參數,或者由函數組成的數組做爲參數。測試
queuePostFlushCb
, 會將參數依次保存到一個隊列中,當宏任務執行完成後,清空微任務隊列時,會依次執行隊列中的每個函數。
it('basic usage', async () => { const calls: string[] = [] const cb1 = () => { calls.push('cb1') } const cb2 = () => { calls.push('cb2') } const cb3 = () => { calls.push('cb3') } queuePostFlushCb([cb1, cb2]) queuePostFlushCb(cb3) expect(calls).toEqual([]) await nextTick() // 按照添加隊列的順序,依次執行函數 expect(calls).toEqual(['cb1', 'cb2', 'cb3']) }) 複製代碼
queuePostFlushCb
不會將相同的函數,重複添加到隊列之中。
it('should dedupe queued postFlushCb', async () => { const calls: string[] = [] const cb1 = () => { calls.push('cb1') } const cb2 = () => { calls.push('cb2') } const cb3 = () => { calls.push('cb3') } queuePostFlushCb([cb1, cb2]) queuePostFlushCb(cb3) queuePostFlushCb([cb1, cb3]) queuePostFlushCb(cb2) expect(calls).toEqual([]) await nextTick() expect(calls).toEqual(['cb1', 'cb2', 'cb3']) }) 複製代碼
若是queuePostFlushCb(cb2)
的調用發生在cb1
的內部。cb2
將會在cb1
以後同一時間執行。不會等到下一次執行微任務時。
it('queuePostFlushCb while flushing', async () => { const calls: string[] = [] const cb1 = () => { calls.push('cb1') queuePostFlushCb(cb2) } const cb2 = () => { calls.push('cb2') } queuePostFlushCb(cb1) await nextTick() expect(calls).toEqual(['cb1', 'cb2']) }) 複製代碼
容許在queuePostFlushCb
中嵌套queueJob
it('queueJob inside postFlushCb', async () => { const calls: string[] = [] const job1 = () => { calls.push('job1') } const cb1 = () => { calls.push('cb1') queueJob(job1) } queuePostFlushCb(cb1) await nextTick() expect(calls).toEqual(['cb1', 'job1']) }) 複製代碼
job1
的執行順序高於cb2
。queueJob
的優先級高於queuePostFlushCb
。
it('queueJob & postFlushCb inside postFlushCb', async () => { const calls: string[] = [] const job1 = () => { calls.push('job1') } const cb1 = () => { calls.push('cb1') queuePostFlushCb(cb2) queueJob(job1) } const cb2 = () => { calls.push('cb2') } queuePostFlushCb(cb1) await nextTick() expect(calls).toEqual(['cb1', 'job1', 'cb2']) }) 複製代碼
容許在queueJob
中嵌套queuePostFlushCb
it('postFlushCb inside queueJob', async () => { const calls: string[] = [] const job1 = () => { calls.push('job1') queuePostFlushCb(cb1) } const cb1 = () => { calls.push('cb1') } queueJob(job1) await nextTick() expect(calls).toEqual(['job1', 'cb1']) }) 複製代碼
job2
將在cb1
以前執行。queueJob
優先級高於postFlushCb
。
it('queueJob & postFlushCb inside queueJob', async () => { const calls: string[] = [] const job1 = () => { calls.push('job1') queuePostFlushCb(cb1) queueJob(job2) } const job2 = () => { calls.push('job2') } const cb1 = () => { calls.push('cb1') } queueJob(job1) await nextTick() expect(calls).toEqual(['job1', 'job2', 'cb1']) }) 複製代碼
nextTick
接受函數做爲參數,同時nextTick
會建立一個微任務。queueJob
接受函數做爲參數,queueJob
會將參數push到queue
隊列中,在當前宏任務執行結束以後,清空隊列。queuePostFlushCb
接受函數或者又函數組成的數組做爲參數,queuePostFlushCb
會將將參數push到postFlushCbs
隊列中,在當前宏任務執行結束以後,清空隊列。queueJob
執行的優先級高於queuePostFlushCb
queueJob
和queuePostFlushCb
容許在清空隊列的期間添加新的成員。話很少說,咱們接下來直接看源碼。
// ErrorCodes 內部錯誤的類型枚舉 // callWithErrorHandling 包含了錯誤處理函數執行器 import { ErrorCodes, callWithErrorHandling } from './errorHandling' import { isArray } from '@vue/shared' // job隊列,queueJob函數會將參數添加到queue數組中 const queue: Function[] = [] // cb隊列,queuePostFlushCb函數會將參數添加到postFlushCbs數組中 const postFlushCbs: Function[] = [] // Promise對象狀態爲resolve const p = Promise.resolve() 複製代碼
nextTick
很是簡單,建立一個微任務。在當前宏任務結束後,執行fn。
function nextTick(fn?: () => void): Promise<void> {
return fn ? p.then(fn) : p
}
複製代碼
將job
添加到queue
隊列中。調用queueFlush
開始處理隊列。
function queueJob(job: () => void) {
// 避免重複的job添加到隊列中,實現了去重
if (!queue.includes(job)) {
queue.push(job)
queueFlush()
}
}
複製代碼
將cb
添加到postFlushCbs
隊列中。調用queueFlush
開始處理隊列。
function queuePostFlushCb(cb: Function | Function[]) { // 注意這裏,postFlushCbs隊列暫時沒有作去重的處理 if (!isArray(cb)) { postFlushCbs.push(cb) } else { // 若是cb是數組,展開後。添加到postFlushCbs隊列中。 postFlushCbs.push(...cb) } queueFlush() } 複製代碼
queueFlush
會調用nextTick
開啓一個微任務。在當前宏任務執行完成後,使用flushJobs
處理隊列queue
和postFlushCbs
。
// isFlushing,isFlushPending做爲開關 let isFlushing = false let isFlushPending = false queueFlush() { if (!isFlushing && !isFlushPending) { // 將isFlushPending置爲true,避免queueJob和queuePostFlushCb重複調用flushJobs isFlushPending = true // 開啓微任務,宏任務結束後,flushJobs處理隊列 nextTick(flushJobs) } } 複製代碼
在flushJobs
中,會優先處理queue
隊列,而後纔是postFlushCbs
隊列
function flushJobs(seen?: CountMap) { isFlushPending = false isFlushing = true let job if (__DEV__) { seen = seen || new Map() } // 1. 清空queue隊列 while ((job = queue.shift())) { if (__DEV__) { // 若是是開發環境,檢查job的調用次數是否超過最大遞歸次數 checkRecursiveUpdates(seen!, job) } // 使用callWithErrorHandling執行器,執行queue隊列中的job // 若是job拋出錯誤,callWithErrorHandling執行器會對錯誤進行捕獲 callWithErrorHandling(job, null, ErrorCodes.SCHEDULER) } // 2. 調用flushPostFlushCbs,處理postFlushCbs隊列 flushPostFlushCbs(seen) isFlushing = false // 若是沒有queue,postFlushCbs隊列沒有被清空 // 遞歸調用flushJobs清空隊列 if (queue.length || postFlushCbs.length) { flushJobs(seen) } } 複製代碼
flushPostFlushCbs
會對postFlushCbs
隊列進行去重。並清空postFlushCbs
隊列。
// 使用Set,對postFlushCbs隊列進行去重 const dedupe = (cbs: Function[]): Function[] => [...new Set(cbs)] function flushPostFlushCbs(seen?: CountMap) { if (postFlushCbs.length) { // postFlushCbs隊列去重 const cbs = dedupe(postFlushCbs) postFlushCbs.length = 0 if (__DEV__) { seen = seen || new Map() } // 清空postFlushCbs隊列 for (let i = 0; i < cbs.length; i++) { if (__DEV__) { // 若是是開發環境,檢查cb的調用次數是否超過最大遞歸次數 checkRecursiveUpdates(seen!, cbs[i]) } // 執行cb cbs[i]() } } } 複製代碼
checkRecursiveUpdates
會是用Map,對job
或者cb
的調用次數進行記錄,若是同一個job
或者cb
的調用次數超過了100次,則認爲超過了最大遞歸次數,並拋出錯誤。
// 最大遞歸層數 const RECURSION_LIMIT = 100 type CountMap = Map<Function, number> function checkRecursiveUpdates(seen: CountMap, fn: Function) { if (!seen.has(fn)) { seen.set(fn, 1) } else { const count = seen.get(fn)! // 若是調用次數超過了100次,拋出錯誤 if (count > RECURSION_LIMIT) { throw new Error( 'Maximum recursive updates exceeded. ' + "You may have code that is mutating state in your component's " + 'render function or updated hook or watcher source function.' ) } else { // 調用次數加一 seen.set(fn, count + 1) } } } 複製代碼
在Vue3中,watch
的callback會在依賴更新後,被push到queue
隊列中,在nextTick
以後執行。考慮以下的代碼。foo
的更新會致使watch
的callback(update
),反覆被push到queue
隊列中,隊列永遠沒法被清空,這種狀況顯然是錯誤的。因此咱們須要使用checkRecursiveUpdates
檢查遞歸的層數,及時的拋出錯誤。
const foo = ref(0) const update = () => { foo.value += 1 } watch(foo, update, { lazy: true }) foo.value += 1 複製代碼