Анализ исходного кода Vue3: nextTick

Vue.js
Анализ исходного кода 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>
    )
  }
})

модульный тест

Лучший способ быстро понять исходный код — прочитать соответствующие модульные тесты. Это может помочь нам быстро понять конкретное значение и использование каждой функции, каждой переменной и обработку некоторых крайних случаев.

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'])
})

Суммировать

  1. nextTickпринимает функцию в качестве параметра, аnextTickБудет создана микрозадача.
  2. queueJobпринимает функцию в качестве параметра,queueJobподтолкнет параметры кqueueВ очереди, после завершения выполнения текущей задачи макроса, очередь очищается.
  3. queuePostFlushCbПринимает функцию или массив функций в качестве параметра,queuePostFlushCbподтолкнет параметры кpostFlushCbsВ очереди, после завершения выполнения текущей задачи макроса, очередь очищается.
  4. queueJobВыполнение имеет более высокий приоритет, чемqueuePostFlushCb
  5. 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