предисловие
Эта статья является моим скромным мнением, если есть какие-либо ошибки, пожалуйста, укажите на аудиторию.
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>
)
}
})
модульный тест
Лучший способ быстро понять исходный код — прочитать соответствующие модульные тесты. Это может помочь нам быстро понять конкретное значение и использование каждой функции, каждой переменной и обработку некоторых крайних случаев.
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'])
})
второй одиночный тест
Это связано с новой функцией,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'])
})
третий одиночный тест
queueJobЭто позволит избежать многократной отправки одной и той же функции (задания) в очередь.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подтолкнет параметры кqueueВ очереди, после завершения выполнения текущей задачи макроса, очередь очищается. -
queuePostFlushCbПринимает функцию или массив функций в качестве параметра,queuePostFlushCbподтолкнет параметры к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
nextTickОчень просто, создайте микрозадачу. После завершения текущей задачи макроса выполните fn.
function nextTick(fn?: () => void): Promise<void> {
return fn ? p.then(fn) : p
}
queueJob
Будуjobдобавить вqueueв очереди. передачаqueueFlushНачать обработку очереди.
function queueJob(job: () => void) {
// 避免重复的job添加到队列中,实现了去重
if (!queue.includes(job)) {
queue.push(job)
queueFlush()
}
}
queuePostFlushCb
Будуcbдобавить вpostFlushCbsв очереди. передачаqueueFlushНачать обработку очереди.
function queuePostFlushCb(cb: Function | Function[]) {
// 注意这里,postFlushCbs队列暂时没有做去重的处理
if (!isArray(cb)) {
postFlushCbs.push(cb)
} else {
// 如果cb是数组,展开后。添加到postFlushCbs队列中。
postFlushCbs.push(...cb)
}
queueFlush()
}
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использовал бы карту, да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)
}
}
}
💭Зачем нужно использовать checkRecursiveUpdates для проверки количества обращений к job или cb?
В Vue3,watchОбратный вызов будет отправлен в зависимость после обновления.queueОчередь, внутриnextTickВыполнить потом. Рассмотрим следующий код.fooОбновление приведет кwatchПерезвоните(update), неоднократно нажимал наqueueВ очереди очередь никогда не может быть очищена, что явно неправильно. Поэтому нам нужно использоватьcheckRecursiveUpdatesПроверить уровень рекурсии и вовремя выкинуть ошибку.
const foo = ref(0)
const update = () => {
foo.value += 1
}
watch(foo, update, {
lazy: true
})
foo.value += 1