1. 首页

Vue3源码解析:nextTick

前言

本文是本人的一些拙见,如有错误请观众老爷们指出。

nextTick

💭为什么需要nextTick?

我们为什么需要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> ) } }) //Js中文网,一个神奇的网站

单元测试

快速了解源码的最好办法是阅读对应的单元测试。可以帮助我们快速的了解,每一个函数,每一个变量的具体含义和用法,以及一些边界情况的处理。

nextTick源码的文件目录位置:packages/runtime-core/src/scheduler.ts

nextTick单元测试文件的文件目录位置:packages/runtime-core/__tests__/scheduler.spec.ts

第一个单测

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']) }) //Js中文网,一个神奇的网站

第二个单测

这里涉及到一个新的函数,queueJob。目前还不清楚其内部的实现,不过我们可以从单测中可以看出来。queueJob接受一个函数作为参数,queueJob会将参数按顺序保存到一个队列中,当宏任务执行完成后,微任务开始执行时,依次执行队列中的函数。


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']) }) //Js中文网,一个神奇的网站

第三个单测

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']) }) //Js中文网,一个神奇的网站

JS中文网 – 前端进阶资源教程 www.javascriptC.com
一个致力于帮助开发者用代码改变世界为使命的平台,每天都可以在这里找到技术世界的头条内容

第四个单测

如果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']) }) //Js中文网,一个神奇的网站

第五个单测

这里又出现了一个新的函数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']) }) //Js中文网,一个神奇的网站

第六个单测

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']) }) //Js中文网,一个神奇的网站

第七个单测

如果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']) }) //Js中文网,一个神奇的网站

第八个单测

允许在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']) }) //Js中文网,一个神奇的网站

第九个单测

job1的执行顺序高于cb2queueJob的优先级高于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']) }) //Js中文网,一个神奇的网站

第十个单测

允许在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']) }) //Js中文网,一个神奇的网站

第十一个单试

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']) }) //Js中文网,一个神奇的网站

总结

  1. nextTick接受函数作为参数,同时nextTick会创建一个微任务。
  2. queueJob接受函数作为参数,queueJob会将参数push到queue队列中,在当前宏任务执行结束之后,清空队列。
  3. queuePostFlushCb接受函数或者又函数组成的数组作为参数,queuePostFlushCb会将将参数push到postFlushCbs队列中,在当前宏任务执行结束之后,清空队列。
  4. queueJob执行的优先级高于queuePostFlushCb
  5. queueJobqueuePostFlushCb允许在清空队列的期间添加新的成员。

话不多说,我们接下来直接看源码。

源码解析


// 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() //Js中文网,一个神奇的网站

nextTick

nextTick非常简单,创建一个微任务。在当前宏任务结束后,执行fn。


function nextTick(fn?: () => void): Promise<void> { return fn ? p.then(fn) : p } //Js中文网,一个神奇的网站

queueJob

job添加到queue队列中。调用queueFlush开始处理队列。


function queueJob(job: () => void) { // 避免重复的job添加到队列中,实现了去重 if (!queue.includes(job)) { queue.push(job) queueFlush() } } //Js中文网,一个神奇的网站

queuePostFlushCb

cb添加到postFlushCbs队列中。调用queueFlush开始处理队列。


function queuePostFlushCb(cb: Function | Function[]) { // 注意这里,postFlushCbs队列暂时没有做去重的处理 if (!isArray(cb)) { postFlushCbs.push(cb) } else { // 如果cb是数组,展开后。添加到postFlushCbs队列中。 postFlushCbs.push(...cb) } queueFlush() } //Js中文网,一个神奇的网站

queueFlush

queueFlush会调用nextTick开启一个微任务。在当前宏任务执行完成后,使用flushJobs处理队列queuepostFlushCbs


// isFlushing,isFlushPending作为开关 let isFlushing = false let isFlushPending = false queueFlush() { if (!isFlushing && !isFlushPending) { // 将isFlushPending置为true,避免queueJob和queuePostFlushCb重复调用flushJobs isFlushPending = true // 开启微任务,宏任务结束后,flushJobs处理队列 nextTick(flushJobs) } } //Js中文网,一个神奇的网站

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) } } //Js中文网,一个神奇的网站

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]() } } } //Js中文网,一个神奇的网站

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) } } } //Js中文网,一个神奇的网站

💭为什么需要使用checkRecursiveUpdates,对job或者cb的调用次数做检查?

在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 //Js中文网,一个神奇的网站

作者:GoWest
链接:https://juejin.im/post/6844904016112009223

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:Vue3源码解析:nextTick

链接:https://www.javascriptc.com/4136.html

« Flutter 实战:增删查改功能示例代码
我敢打赌!这是全网最全的 Git 分支开发规范手册»
Flutter 中文教程资源

相关推荐

QR code