前言
本文是本人的一些拙见,如有错误请观众老爷们指出。
nextTick
??为什么需要nextTick?
我们为什么需要
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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> ) } }) |
单元测试
快速了解源码的最好办法是阅读对应的单元测试。可以帮助我们快速的了解,每一个函数,每一个变量的具体含义和用法,以及一些边界情况的处理。
第一个单测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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']) }) |
第二个单测
这里涉及到一个新的函数,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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']) }) |
第三个单测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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']) }) |
第四个单测
如果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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']) }) |
第五个单测
这里又出现了一个新的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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']) }) |
第六个单测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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']) }) |
第七个单测
如果
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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']) }) |
第八个单测
允许在
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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']) }) |
第九个单测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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']) }) |
第十个单测
允许在
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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']) }) |
第十一个单试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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 允许在清空队列的期间添加新的成员。
话不多说,我们接下来直接看源码。
源码解析
1 2 3 4 5 6 7 8 9 10 11 | // 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
1 2 3 | function nextTick(fn?: () => void): Promise<void> { return fn ? p.then(fn) : p } |
queueJob
将
1 2 3 4 5 6 7 | function queueJob(job: () => void) { // 避免重复的job添加到队列中,实现了去重 if (!queue.includes(job)) { queue.push(job) queueFlush() } } |
queuePostFlushCb
将
1 2 3 4 5 6 7 8 9 10 | function queuePostFlushCb(cb: Function | Function[]) { // 注意这里,postFlushCbs队列暂时没有做去重的处理 if (!isArray(cb)) { postFlushCbs.push(cb) } else { // 如果cb是数组,展开后。添加到postFlushCbs队列中。 postFlushCbs.push(...cb) } queueFlush() } |
queueFlush
1 2 3 4 5 6 7 8 9 10 11 12 | // isFlushing,isFlushPending作为开关 let isFlushing = false let isFlushPending = false queueFlush() { if (!isFlushing && !isFlushPending) { // 将isFlushPending置为true,避免queueJob和queuePostFlushCb重复调用flushJobs isFlushPending = true // 开启微任务,宏任务结束后,flushJobs处理队列 nextTick(flushJobs) } } |
在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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) } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // 使用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]() } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // 最大递归层数 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) } } } |
??为什么需要使用checkRecursiveUpdates,对job或者cb的调用次数做检查?
在Vue3中,
1 2 3 4 5 6 7 8 9 10 11 | const foo = ref(0) const update = () => { foo.value += 1 } watch(foo, update, { lazy: true }) foo.value += 1 |