Механизм Vue nextTick

внешний интерфейс Vue.js Promise

задний план

Давайте сначала посмотрим на часть кода выполнения Vue:

export default {
  data () {
    return {
      msg: 0
    }
  },
  mounted () {
    this.msg = 1
    this.msg = 2
    this.msg = 3
  },
  watch: {
    msg () {
      console.log(this.msg)
    }
  }
}

Мы предполагаем, что выполнение этого скрипта будет печатать в последовательности: 1, 2, 3. Но на самом деле он выведет только один раз: 3. Почему такая ситуация? Давайте узнаем.

queueWatcher

мы определяемwatchмониторmsg, который на самом деле будет вызываться Vue следующим образомvm.$watch(keyOrFn, handler, options).$watchкогда мы инициализируем, дляvmФункция для привязки для созданияWatcherобъект. тогда посмотримWatcherкак бороться сhandlerиз:

this.deep = this.user = this.lazy = this.sync = false
...
  update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
...

Первоначальная настройкаthis.deep = this.user = this.lazy = this.sync = false, то есть когда триггерupdateПри обновлении будет выполнятьсяqueueWatcherметод:

const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
...
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

здесьnextTick(flushSchedulerQueue)серединаflushSchedulerQueueФункция на самом делеwatcherОбновление вида:

function flushSchedulerQueue () {
  flushing = true
  let watcher, id
  ...
 for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    id = watcher.id
    has[id] = null
    watcher.run()
    ...
  }
}

Кроме того, оwaitingпеременная, это очень важный флаг, он гарантируетflushSchedulerQueueОбратные вызовы разрешено размещать толькоcallbacksоднажды. Далее посмотримnextTickфункция, говоряnexTickПеред этим вам нужноEvent Loop,microTask,macroTaskПри определенном понимании Vue nextTick в основном использует эти базовые принципы. Если вы еще этого не знаете, вы можете обратиться к этой моей статьеВведение в циклы событийЧто ж, давайте посмотрим на его реализацию:

export const nextTick = (function () {
  const callbacks = []
  let pending = false
  let timerFunc

  function nextTickHandler () {
    pending = false
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for (let i = 0; i < copies.length; i++) {
      copies[i]()
    }
  }

  // An asynchronous deferring mechanism.
  // In pre 2.4, we used to use microtasks (Promise/MutationObserver)
  // but microtasks actually has too high a priority and fires in between
  // supposedly sequential events (e.g. #4521, #6690) or even between
  // bubbling of the same event (#6566). Technically setImmediate should be
  // the ideal choice, but it's not available everywhere; and the only polyfill
  // that consistently queues the callback after all DOM events triggered in the
  // same loop is by using MessageChannel.
  /* istanbul ignore if */
  if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    timerFunc = () => {
      setImmediate(nextTickHandler)
    }
  } else if (typeof MessageChannel !== 'undefined' && (
    isNative(MessageChannel) ||
    // PhantomJS
    MessageChannel.toString() === '[object MessageChannelConstructor]'
  )) {
    const channel = new MessageChannel()
    const port = channel.port2
    channel.port1.onmessage = nextTickHandler
    timerFunc = () => {
      port.postMessage(1)
    }
  } else
  /* istanbul ignore next */
  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    // use microtask in non-DOM environments, e.g. Weex
    const p = Promise.resolve()
    timerFunc = () => {
      p.then(nextTickHandler)
    }
  } else {
    // fallback to setTimeout
    timerFunc = () => {
      setTimeout(nextTickHandler, 0)
    }
  }

  return function queueNextTick (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
      timerFunc()
    }
    // $flow-disable-line
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        _resolve = resolve
      })
    }
  }
})()

Первые проходы VuecallbackМассив для имитации очереди событий, событий в очереди событий, черезnextTickHandlerметод для выполнения вызова, а что делать, выполняетсяtimerFuncрешать. Давайте взглянемtimeFuncОпределение:

  if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    timerFunc = () => {
      setImmediate(nextTickHandler)
    }
  } else if (typeof MessageChannel !== 'undefined' && (
    isNative(MessageChannel) ||
    // PhantomJS
    MessageChannel.toString() === '[object MessageChannelConstructor]'
  )) {
    const channel = new MessageChannel()
    const port = channel.port2
    channel.port1.onmessage = nextTickHandler
    timerFunc = () => {
      port.postMessage(1)
    }
  } else
  /* istanbul ignore next */
  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    // use microtask in non-DOM environments, e.g. Weex
    const p = Promise.resolve()
    timerFunc = () => {
      p.then(nextTickHandler)
    }
  } else {
    // fallback to setTimeout
    timerFunc = () => {
      setTimeout(nextTickHandler, 0)
    }
  }

Как можно заметитьtimerFuncПриоритет определенияmacroTask --> microTask,В отсутствиеDomокружающая среда, использованиеmicroTask, такие как Weex

setImmediate, MessageChannel против setTimeout

Мы являемся приоритетным определениемsetImmediate,MessageChannelЗачем создавать macroTask с ними вместо setTimeout? HTML5 предусматривает, что минимальная задержка setTimeout составляет 4 мс, что означает, что асинхронный обратный вызов может быть запущен за 4 мс в идеальной среде. Vue использует так много функций для имитации асинхронных задач, и его единственная цель — сделать обратные вызовы асинхронными и вызываться как можно раньше. Задержка MessageChannel и setImmediate явно меньше, чем setTimeout.

Решать проблему

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

Сейчас такая ситуация, при монтировании значение test будет выполнено 1000 раз по циклу ++. Каждый раз, когда ++ срабатывает в соответствии с ответомsetter->Dep->Watcher->update->run. Если в это время представление не обновляется асинхронно, то ++ будет напрямую манипулировать DOM для обновления представления каждый раз, что очень сильно влияет на производительность. Итак, Vue реализуетqueueОчередь, которая будет выполняться равномерно в следующем такте (или на этапе микрозадачи текущего такта)queueсерединаWatcherбегать. При этом Watcher с одним и тем же id не будут добавляться в очередь повторно, поэтому 1000 запусков Watcher выполняться не будут. Окончательное представление обновления только напрямую изменит 0 DOM, соответствующий тесту, на 1000. Гарантируется, что действие по обновлению DOM операции представления вызывается на следующем тике (или этапе микрозадачи текущего тика) после выполнения текущего стека, что значительно оптимизирует производительность.

интересный вопрос

var vm = new Vue({
    el: '#example',
    data: {
        msg: 'begin',
    },
    mounted () {
      this.msg = 'end'
      console.log('1')
      setTimeout(() => { // macroTask
         console.log('3')
      }, 0)
      Promise.resolve().then(function () { //microTask
        console.log('promise!')
      })
      this.$nextTick(function () {
        console.log('2')
      })
  }
})

Порядок выполнения этого должен быть известен всем, чтобы напечатать: 1, обещание, 2, 3.

  1. потому что первый триггерthis.msg = 'end', который запускаетwatcherизupdate, тем самым помещая обратный вызов операции обновления в очередь событий vue.

  2. this.$nextTickТакже введите новую функцию обратного вызова для отправки очереди событий, все они проходятsetImmediate --> MessageChannel --> Promise --> setTimeoutопределятьtimeFunc. а такжеPromise.resolve().thenЭто микрозадача, поэтому сначала будет напечатано обещание.

  3. в поддержкуMessageChannelа такжеsetImmediateслучае их порядок выполнения предшествуетsetTimeout(В IE11/Edge задержка setImmediate может быть в пределах 1 мс, а setTimeout имеет минимальную задержку 4 мс, поэтому setImmediate выполняет функцию обратного вызова раньше, чем setTimeout(0). Во-вторых, поскольку массиву обратного вызова отдается приоритет в очереди событий. ), он напечатает 2, затем напечатает 3

  4. но не поддерживаетMessageChannelа такжеsetImmediate, это пройдетPromiseопределениеtimeFunc, а также старая версия до Vue 2.4 будет выполняться первойpromise. Эта ситуация приводит к следующему порядку: 1, 2, обещание, 3. Поскольку this.msg должен сначала вызвать функцию обновления dom, функция обновления dom будет получена обратным вызовом и сначала введена в очередь асинхронного времени, а затем она будет определена.Promise.resolve().then(function () { console.log('promise!')})Такая микрозадача, затем определите$nextTickОн будет получен обратным вызовом снова. Мы знаем, что очередь удовлетворяет принципу «первым пришел — первым обслужен», поэтому объект хранения обратного вызова выполняется первым.

постскриптум

Если вас интересует исходный код Vue, вы можете зайти сюда:

Более интересное объяснение исходного кода соглашения Vue

Справочная статья:

Обновление Vue.js и наступление на пит-ноты

[Исходный код Vue] Стратегия асинхронного обновления DOM в механизме Vue и nextTick