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

JavaScript Vue.js

предисловие

nextTick — это основная функция Vue, а nextTick часто используется во внутренней реализации Vue. Однако многие новички не понимают ни принципа nextTick, ни даже функции nextTick.

Итак, давайте посмотрим, что такое nextTick.

функция nextTick

Взгляните на описание в официальной документации:

Выполняет отложенный обратный вызов после завершения следующего цикла обновления DOM. Используйте этот метод сразу после изменения данных, чтобы получить обновленную модель DOM.

Взгляните еще раз на официальный пример:

// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
  // DOM 更新了
})

// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
Vue.nextTick()
  .then(function () {
    // DOM 更新了
  })

Новое с версии 2.1.0: возвращает обещание, если обратный вызов не предоставлен и в среде, поддерживающей обещание. Обратите внимание, что Vue не поставляется с полифиллом для промисов, поэтому, если ваш целевой браузер изначально не поддерживает промисы (IE: что вы, ребята, делаете), вам придется предоставить свой собственный полифилл.

Можно видеть, что основная функция nextTick — позволить функции обратного вызова воздействовать на обновление dom после изменения данных. Многие люди недоумевают, когда видят это.Почему мне нужно выполнять функцию обратного вызова после обновления DOM?После того, как я изменяю данные, разве DOM не обновляется автоматически?

Это связано с циклом событий в JS, существует бесчисленное множество онлайн-руководств, поэтому я не буду повторять их здесь. Прежде чем продолжить чтение этой статьи, рекомендуется разобраться в цикле событий.

В качестве практического примера:

У нас есть таблица с пейджером, который требует выбора первого элемента при каждом перелистывании страницы. В обычных обстоятельствах нам нужно щелкнуть по пейджеру, получить данные из фона, обновить данные таблицы и манипулировать API таблицы, чтобы выбрать первый элемент.

Однако вы обнаружите, что данные таблицы обновляются, но первый элемент не выбран. Потому что при выборе первого элемента, хотя данные и обновляются, DOM не обновляется. На этом этапе вы можете использовать nextTick для управления выбором первого элемента в таблице после обновления DOM.

Итак, что делает nextTick для выполнения функции обратного вызова после обновления DOM?

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

Исходный код nextTick находится в src/core/util/next-tick.js, всего строк 118. Он очень короткий и мощный и очень подходит для студентов, которые впервые читают исходный код. .

Исходный код nextTick в основном разделен на две части:

1. Проверка способностей

2. Очены обратного вызова обнаружения по-разному в зависимости от возможности

Тестирование способностей

Эта часть на самом деле очень проста. Как мы все знаем, цикл событий делится на макро-задачи и микро-задачи. Независимо от того, выполняются ли макро-задачи или микро-задачи, он войдет в следующий тик после завершения и будет выполняться между двумя тиками. рендеринг.

Однако макрозадачи занимают больше времени, чем микрозадачи, поэтому, если браузер поддерживает это, сначала используются микрозадачи. Если браузер не поддерживает микрозадачи, используйте макрозадачи, однако эффективность различных макрозадач также различна, и необходимо использовать разные макрозадачи в зависимости от поддержки браузера.

nextTick следует этой идее при обнаружении способностей.

// Determine (macro) task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
// 如果浏览器不支持Promise,使用宏任务来执行nextTick回调函数队列
// 能力检测,测试浏览器是否支持原生的setImmediate(setImmediate只在IE中有效)
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // 如果支持,宏任务( macro task)使用setImmediate
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
  // 同上
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  /* istanbul ignore next */
  // 都不支持的情况下,使用setTimeout
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

Во-первых, проверьте, поддерживает ли браузер setImmediate. Если нет, используйте MessageChannel. Если он не поддерживает, вы можете использовать только setTimeout, который наименее эффективен, но наиболее совместим.

После этого проверьте, поддерживает ли браузер Promise, если поддерживает, используйте Promise для выполнения очереди callback-функций, ведь скорость микрозадач выше, чем у макрозадач. Если это не поддерживается, вы можете использовать задачу макроса только для выполнения очереди функции обратного вызова.

Выполнить очередь функций обратного вызова

Код, выполняющий очередь функций обратного вызова, находится только в начале и в конце.

// 回调函数队列
const callbacks = []
// 异步锁
let pending = false

// 执行回调函数
function flushCallbacks () {
  // 重置异步锁
  pending = false
  // 防止出现nextTick中包含nextTick时出现问题,在执行回调函数队列前,提前复制备份,清空回调函数队列
  const copies = callbacks.slice(0)
  callbacks.length = 0
  // 执行回调函数队列
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

...

// 我们调用的nextTick函数
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 将回调函数推入回调队列
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // 如果异步锁未锁上,锁上异步锁,调用异步函数,准备等同步函数执行完后,就开始执行回调函数队列
  if (!pending) {
    pending = true
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }
  // $flow-disable-line
  // 2.1.0新增,如果没有提供回调,并且支持Promise,返回一个Promise
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

Общий процесс состоит в том, чтобы получить функцию обратного вызова и поместить функцию обратного вызова в очередь функций обратного вызова.

В то же время, когда получена первая функция обратного вызова, выполняется соответствующий асинхронный метод в обнаружении возможностей (в асинхронном методе вызывается очередь функций обратного вызова).

Как я могу гарантировать, что асинхронный метод будет выполняться только при получении первой функции обратного вызова?

В исходном коде nextTick используется концепция асинхронной блокировки, то есть при получении первой функции обратного вызова сначала закрывается блокировка и выполняется асинхронный метод. В этот момент браузер находится в ситуации, когда асинхронный код выполняется после ожидания выполнения синхронного кода.

Проведем аналогию: это эквивалентно группе пассажиров, садящихся в автобус. Когда первый пассажир садится в автобус, автомобиль заводится и готов к поездке. После того, как все пассажиры садятся в автобус, автомобиль может быть официально за рулем.

Конечно, есть непонятный момент при выполнении функции flushCallbacks, а именно: зачем нужно резервировать очередь функции обратного вызова? Очередь функции обратного вызова резервной копии также выполняется?

Потому что будет такая ситуация: nextTick применяет nextTick. Если flushCallbacks не выполняет специальную обработку и выполняет функцию обратного вызова напрямую, это приведет к тому, что функция обратного вызова в nextTick войдет в очередь обратных вызовов. Это эквивалентно тому, что пассажир следующего автобуса садится в предыдущий автобус.

Реализовать простой nextTick

Сказав это, давайте реализуем простой nextTick:

let callbacks = []
let pending = false

function nextTick (cb) {
    callbacks.push(cb)

    if (!pending) {
        pending = true
        setTimeout(flushCallback, 0)
    }
}

function flushCallback () {
    pending = false
    let copies = callbacks.slice()
    callbacks.length = 0
    copies.forEach(copy => {
        copy()
    })
}

Видно, что в упрощенной версии nextTick callback-функция принимается через nextTick, а callback-функция выполняется асинхронно через setTimeout. Таким образом, функция обратного вызова может быть выполнена в следующем тике, то есть функция обратного вызова выполняется после повторного рендеринга пользовательского интерфейса.