Чтение исходного кода Vue — пакетное асинхронное обновление и принцип nextTick

внешний интерфейс исходный код браузер Vue.js

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

Целевая версия Vue:2.5.17-beta.0

Комментарии к исходному коду Vue:GitHub.com/Шерлок Эд9…

Отказ от ответственности: Синтаксис исходного кода в статье использует Flow, и исходный код сокращен по мере необходимости (чтобы не путать @_@), если вы хотите увидеть полную версию, пожалуйста, введите вышегитхаб-адрес, эта статья представляет собой серию статей, адрес статьи внизу ~

Заинтересованные студенты могут добавить группу WeChat в конце статьи для совместного обсуждения~

1. Асинхронное обновление

предыдущий постМы полагаемся на реактивный подход к принципу сбораdefineReactiveсерединаsetterОтправка обновления в аксессорdep.notify()метод, этот метод будет уведомлять один за другим вdepизsubsНаблюдатели, собранные в подписке на свои изменения, выполняют обновление. ПосмотриupdateРеализация метода:

// src/core/observer/watcher.js

/* Subscriber接口,当依赖发生改变的时候进行回调 */
update() {
  if (this.computed) {
    // 一个computed watcher有两种模式:activated lazy(默认)
    // 只有当它被至少一个订阅者依赖时才置activated,这通常是另一个计算属性或组件的render function
    if (this.dep.subs.length === 0) {       // 如果没人订阅这个计算属性的变化
      // lazy时,我们希望它只在必要时执行计算,所以我们只是简单地将观察者标记为dirty
      // 当计算属性被访问时,实际的计算在this.evaluate()中执行
      this.dirty = true
    } else {
      // activated模式下,我们希望主动执行计算,但只有当值确实发生变化时才通知我们的订阅者
      this.getAndInvoke(() => {
        this.dep.notify()     // 通知渲染watcher重新渲染,通知依赖自己的所有watcher执行update
      })
    }
  } else if (this.sync) {	  // 同步
    this.run()
  } else {
    queueWatcher(this)        // 异步推送到调度者观察者队列中,下一个tick时调用
  }
}

если неcomputed watcherтакже неsyncТекущий наблюдатель, который вызывает обновление, будет помещен в очередь планировщика и будет вызван на следующем такте, чтобы увидетьqueueWatcher:

// src/core/observer/scheduler.js

/* 将一个观察者对象push进观察者队列,在队列中已经存在相同的id则
 * 该watcher将被跳过,除非它是在队列正被flush时推送
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {     // 检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验
    has[id] = true
    queue.push(watcher)      // 如果没有正在flush,直接push到队列中
    if (!waiting) {          // 标记是否已传给nextTick
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

/* 重置调度者状态 */
function resetSchedulerState () {
  queue.length = 0
  has = {}
  waiting = false
}

используется здесьhasХэш-карта используется для проверки существования идентификатора текущего наблюдателя. Если он уже существует, он будет пропущен. Если он не существует, он будет перемещен вqueueПоставьте в очередь и отметьте хеш-таблицу для следующей проверки, чтобы предотвратить повторные добавления. Это процесс дедупликации, он более цивилизован, чем каждый раз ходить в очередь для проверки веса, и при рендеринге повторяться не будет.patchОдин и тот же наблюдатель изменяется, поэтому даже если данные, используемые в представлении, изменяются 100 раз синхронно, асинхронноpatchобновит только последнюю модификацию.

здесьwaitingметод используется для обозначенияflushSchedulerQueueбыл переданnextTickБит флага, если он был доставлен, он будет только помещен в очередь, а не доставленflushSchedulerQueueДатьnextTick,Подожди покаresetSchedulerStateПри сбросе состояния планировщикаwaitingбудет сброшенfalseразрешатьflushSchedulerQueueОбратный вызов, который передается следующему тику, короче говоря, он гарантируетсяflushSchedulerQueueОбратные вызовы можно передавать только один раз за тик. посмотреть, что передаетсяnextTickПерезвонитеflushSchedulerQueueЧто вы наделали:

// src/core/observer/scheduler.js

/* nextTick的回调函数,在下一个tick时flush掉两个队列同时运行watchers */
function flushSchedulerQueue () {
  flushing = true
  let watcher, id

  queue.sort((a, b) => a.id - b.id)					// 排序

  for (index = 0; index < queue.length; index++) {	 // 不要将length进行缓存
    watcher = queue[index]
    if (watcher.before) {         // 如果watcher有before则执行
      watcher.before()
    }
    id = watcher.id
    has[id] = null                // 将has的标记删除
    watcher.run()                 // 执行watcher
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {  // 在dev环境下检查是否进入死循环
      circular[id] = (circular[id] || 0) + 1     // 比如user watcher订阅自己的情况
      if (circular[id] > MAX_UPDATE_COUNT) {     // 持续执行了一百次watch代表可能存在死循环
        warn()								  // 进入死循环的警告
        break
      }
    }
  }
  resetSchedulerState()           // 重置调度者状态
  callActivatedHooks()            // 使子组件状态都置成active同时调用activated钩子
  callUpdatedHooks()              // 调用updated钩子
}

существуетnextTickметод выполненflushSchedulerQueueметод, этот метод выполняется один за другимqueueнаблюдательrunметод. Мы видим, что сначала естьqueue.sort()Метод сортирует наблюдателей в очереди по идентификатору от меньшего к большему, что гарантирует, что:

  1. Порядок обновления компонентов — от родительского к дочернему, поскольку родительские компоненты всегда создаются раньше дочерних.
  2. Наблюдатели за пользователями компонента (наблюдатели за слушателями) запускаются перед наблюдателями за рендерингом, потому что наблюдатели за пользователями часто создаются раньше, чем наблюдатели за рендерингом.
  3. Если компонент уничтожается во время работы наблюдателя родительского компонента, его выполнение наблюдателя будет пропущено.

В цикле for, который выполняет очередь один за другим,index < queue.lengthДлина здесь не кэшируется, потому что во время выполнения существующих объектов-наблюдателей в очередь могут быть помещены дополнительные объекты-наблюдатели.

Затем модификация данных отражается от уровня модели к процессу просмотра:数据更改 -> setter -> Dep -> Watcher -> nextTick -> patch -> 更新视图

2. Принцип nextTick

2.1 Макрозадача/микрозадача

Вот посмотрите на метод, который содержит выполнение каждого наблюдателя, переданного в качестве обратного вызова.nextTickПозже,nextTickчто делает этот метод. Но сначала нужно понятьEventLoop,macro task,micro taskНесколько понятий, если вы не понимаете, можете сослаться на нихЦикл событий в JS и Node.jsВ этой статье приведена картинка, показывающая отношения выполнения двух последних в основном потоке:

Объясните, когда основной поток завершает выполнение задачи синхронизации:

  1. Сначала движок берет первую задачу из очереди макрозадач, после завершения выполнения берет все задачи из очереди микрозадач и выполняет их последовательно;
  2. Затем взять из очереди макрозадач следующую, а после завершения выполнения снова вынуть все очереди микрозадач;
  3. До бесконечности, пока две задачи не окажутся в очереди на выполнение.

Распространенные типы асинхронных задач в среде браузера в соответствии с приоритетом:

  • macro task: код синхронизации,setImmediate,MessageChannel,setTimeout/setInterval
  • micro task:Promise.then,MutationObserver

Некоторые статьи ставятmicro taskназываемые микрозадачами,macro taskЭто называется макрозадачей, потому что два слова пишутся очень похоже -. - , поэтому следующие комментарии в основном написаны на китайском~

Давайте посмотрим на исходный кодmicro taskа такжеmacro taskРеализация:macroTimerFunc,microTimerFunc

// src/core/util/next-tick.js

const callbacks = []     // 存放异步执行的回调
let pending = false      // 一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送

/* 挨个同步执行callbacks中回调 */
function flushCallbacks() {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

let microTimerFunc        // 微任务执行方法
let macroTimerFunc        // 宏任务执行方法
let useMacroTask = false  // 是否强制为宏任务,默认使用微任务

// 宏任务
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  MessageChannel.toString() === '[object MessageChannelConstructor]'  // PhantomJS
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

// 微任务
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = () => {
    p.then(flushCallbacks)
  }
} else {
  microTimerFunc = macroTimerFunc      // fallback to macro
}

flushCallbacksЭтот метод предназначен для синхронного выполнения функций обратного вызова в обратных вызовах одна за другой. Функции обратного вызова в обратных вызовах вызываютсяnextTickОн добавляется, когда вы хотите, тогда как его использоватьmicro taskа такжеmacro taskвыполнитьflushCallbacksНу вот их реализацияmacroTimerFunc,microTimerFuncПары API с использованием макрозадач/микрозадач в браузереflushCallbacksМетод завернут в слой. Например, метод макро-задачиmacroTimerFunc=()=>{ setImmediate(flushCallbacks) }, чтобы при запуске выполнения задачи макросаmacroTimerFunc()Обратные вызовы, хранящиеся в массиве обратных вызовов, могут использоваться в следующем цикле задач макроса в браузере, и то же самое верно для микрозадач. Также можно видеть, чтоnextTickАсинхронная функция обратного вызова сжимается в синхронную задачу и выполняется за один тик вместо открытия нескольких асинхронных задач.

Обратите внимание, что здесь есть более сложное для понимания место, первый вызовnextTickкогдаpendingЕсли оно ложно, то задача макро- или микрозадачи в цикле событий браузера была отправлена. такmacroTimerFunc,microTimerFuncЭквивалентно заполнителю очереди задач, а позжеpendingЕсли это правда, он будет продолжать добавляться в очередь заполнителей, и цикл событий будет выполняться вместе, когда наступит очередь очереди задач. воплощать в жизньflushCallbacksВремяpendingУстановите false, чтобы разрешить следующий раунд выполненияnextTickВремя до заполнителя цикла событий.

можно увидеть вышеmacroTimerFuncа такжеmicroTimerFuncВыполнена плавная деградация под другую совместимость браузера, илиПонижение стратегии:

  1. macroTimerFunc:setImmediate -> MessageChannel -> setTimeout. Сначала проверьте, поддерживается ли он изначальноsetImmediate, этот метод изначально реализован только в браузерах IE и Edge, а затем определяет, поддерживается ли онMessageChannel, если правильноMessageChannelЕсли вы не знаете, вы можете обратиться кэта статья, если он еще не поддерживается, используйте его в последнюю очередьsetTimeout; Зачем использовать приоритетsetImmediateа такжеMessageChannelвместо прямого использованияsetTimeoutНу, это из-за правил HTML5Минимальная задержка выполнения setTimeout составляет 4 мс., а вложенный тайм-аут равен 10 мс.Для того, чтобы обратный вызов выполнялся как можно быстрее, первые два без ограничения минимальной задержки явно лучше, чемsetTimeout.
  2. microTimerFunc:Promise.then -> macroTimerFunc. Сначала проверьте, поддерживается ли онPromise, если поддерживаетсяPromise.thenзвонитьflushCallbacksметод, в противном случае вырождается вmacroTimerFunc; После vue2.5nextTickУбрана плавная деградация микрозадач по соображениям совместимости.MutationObserverПуть.

2.2 Реализация nextTick

Наконец, давайте посмотрим, что мы обычно используемnextTickКак именно реализован метод:

// src/core/util/next-tick.js

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()
    }
  }
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

/* 强制使用macrotask的方法 */
export function withMacroTask(fn: Function): Function {
  return fn._withTask || (fn._withTask = function() {
    useMacroTask = true
    const res = fn.apply(null, arguments)
    useMacroTask = false
    return res
  })
}

nextTickЗдесь он разделен на три части, давайте посмотрим на него вместе;

  1. первыйnextTickположить входящийcbдля функции обратного вызоваtry-catchПосле обёртывания поместите его в анонимную функцию и запушите в массив callbacks, это делается для предотвращения единичногоcbЕсли ошибка выполнения не приведет к зависанию всего потока JS, каждыйcbВся обертка предназначена для того, чтобы эти функции обратного вызова не влияли друг на друга, если выполнение было неправильным.Например, предыдущая все еще может быть выполнена после выдачи ошибки.
  2. затем проверьтеpendingСтатус, это то же самое, что и введенное ранееqueueWatcherсерединаwaitingэто значение, это бит флага, сначала этоfalseвходmacroTimerFunc,microTimerFuncметод установлен наtrue, поэтому следующий вызовnextTickне войдетmacroTimerFunc,microTimerFuncметод, следующий из двухmacro/micro tickкогдаflushCallbacksАсинхронно выполнять задачи, собранные в очереди обратных вызовов, иflushCallbacksВ начале выполнения методаpendingзадаватьfalse, поэтому следующий вызовnextTickпора начинать новый раундmacroTimerFunc,microTimerFunc, который формирует vueevent loop.
  3. Наконец, проверьте, передано лиcb,потому чтоnextTickТакже поддерживаются обещанные звонки:nextTick().then(() => {}), поэтому, если нет входящегоcbПросто верните экземпляр промиса и передайте разрешение в _resolve, чтобы при выполнении последнего он перешел к моменту, когда мы его вызываем, и передал его вthenв методе.

Исходный код Vuenext-tick.jsЕсть еще один важный раздел документа.Примечания, вот перевод:

В версиях до vue2.5 nextTick в основном основывался наmicro taskдостичь, но в некоторых случаяхmicro taskимеет слишком высокий приоритет и может находиться между последовательными событиями (например,№ 4521,№6690) или даже срабатывает между событиями во время всплытия для одного и того же события (№6566). Но если все изменить наmacro task, это также повлияет на производительность некоторых сцен с перерисовкой и анимацией, например проблема#6813. Решение, предоставляемое версиями после vue2.5, заключается в использовании значения по умолчанию.micro task, но принудительно применяется при необходимости (например, в обработчиках прикрепленных событий v-on)macro task.

Зачем использовать значение по умолчаниюmicro taskЧто ж, он использует функцию высокого приоритета, чтобы гарантировать, что все микрозадачи в очереди выполняются за один цикл.

обязательныйmacro taskМетод заключается в вызове функции обработчика обратного вызова по умолчанию при привязке событий DOM.withMacroTaskспособ сделать слой упаковкиhandler = withMacroTask(handler), что гарантирует, что во время выполнения всей callback-функции при изменении состояния данных эти изменения будут переданы вmacro taskсередина. Вышеописанное реализовано вsrc/platforms/web/runtime/modules/events.jsизaddметод, вы можете сами взглянуть на конкретный код.

Пока я писал этот пост, кто-то задал вопрос.версии vue 2.4 и 2.5 имеют разные события @input, причина этой проблемы также в том, что события DOM в версиях до 2.5 используютmicro task, а затем используйтеmacro task, решение ссылкиНесколько методов, представленных в , вот метод, который будет использоваться в навесном крюке.addEventListenerДобавьте собственные методы событий для реализации, см.CodePen.

3. Пример

Сказав так много, давайте возьмем пример.CodePen

<div id="app">
  <span id='name' ref='name'>{{ name }}</span>
  <button @click='change'>change name</button>
  <div id='content'></div>
</div>
<script>
  new Vue({
    el: '#app',
    data() {
      return {
        name: 'SHERlocked93'
      }
    },
    methods: {
      change() {
        const $name = this.$refs.name
        this.$nextTick(() => console.log('setter前:' + $name.innerHTML))
        this.name = ' name改喽 '
        console.log('同步方式:' + this.$refs.name.innerHTML)
        setTimeout(() => this.console("setTimeout方式:" + this.$refs.name.innerHTML))
        this.$nextTick(() => console.log('setter后:' + $name.innerHTML))
        this.$nextTick().then(() => console.log('Promise方式:' + $name.innerHTML))
      }
    }
  })
</script>

Выполните следующее, чтобы увидеть результат:

同步方式:SHERlocked93 
setter前:SHERlocked93 
setter后:name改喽 
Promise方式:name改喽 
setTimeout方式:name改喽

Почему такой результат, объясните:

  1. Синхронно:Когда имя в данных изменено, имя имени будет активировано в это время.setterсерединаdep.notifyУведомить наблюдателя рендеринга, который зависит от этих данных, чтобы перейтиupdate,updateположитflushSchedulerQueueфункция переданаnextTick, наблюдатель рендеринга находится вflushSchedulerQueueвремя выполнения функцииwatcher.runеще разdiff -> patchЭтот набор повторных рендеровre-renderВидите, в этом процессе будет повторная сборка зависимостей, этот процесс асинхронный, поэтому, когда мы напрямую изменяем имя, а затем печатаем, асинхронные изменения не вносятся.patchв представление, поэтому элемент DOM в представлении по-прежнему остается исходным содержимым.
  2. Перед сеттером:Почему исходный контент печатается перед сеттером?nextTickПри вызове поместите обратные вызовы в массив обратных вызовов один за другим, а затем выполните их позже.forЦикл выполняется один за другим, поэтому он похож на концепцию очереди, первый вошел, первый вышел; после изменения имени триггер для заполнения наблюдателя рендерингаschedulerQueueпоставить в очередь и поставить его функцию выполненияflushSchedulerQueueПерейти кnextTick, в это время уже есть в очереди обратных вызововsetter前函数потому что этоcbвsetter前函数После этого он помещается в очередь обратных вызовов, затем метод «первым поступил — первым обслужен» сначала выполняет обратные вызовы в обратных вызовах.setter前函数, наблюдатель рендеринга в это время не выполняетсяwatcher.run, поэтому напечатанный элемент DOM по-прежнему остается исходным содержимым.
  3. После сеттера:После выполнения сеттераflushSchedulerQueue, то наблюдатель рендеринга изменилpatchк представлению, поэтому получение DOM в это время является измененным содержимым.
  4. Обещание пути:эквивалентноPromise.thenВыполните эту функцию таким образом, чтобы DOM изменился.
  5. Метод setTimeout:Наконец, задача выполнения задачи макроса, при которой DOM изменился.

Обратите внимание, что при выполненииsetter前函数Перед этой асинхронной задачей выполнялся синхронный код, асинхронная задача не выполнялась, все$nextTickФункция также выполняется, и все обратные вызовы помещаются в очередь обратных вызовов для ожидания выполнения, поэтому вsetter前函数При выполнении очередь обратных вызовов выглядит так: [setter前函数,flushSchedulerQueue,setter后函数,Promise方式函数], это очередь микрозадач, после завершения выполнения выполняется макрозадачаsetTimeoutТак что распечатайте результаты выше.

Кроме того, если очередь задач макросов браузераsetImmediate,MessageChannel,setTimeout/setIntervalРазличные типы задач будут выполняться одна за другой в порядке добавления в цикл событий в указанном выше порядке, поэтому, если браузер поддерживаетMessageChannel,nextTickказненmacroTimerFunc, то если в очереди макрозадач есть обаnextTickДобавленные задачи и задачи, добавленные пользователемsetTimeoutТипы задач будут выполняться в первую очередьnextTickзадачи, потому чтоMessageChannelсоотношение приоритетовsetTimeoutвысота,setImmediateТо же самое справедливо.


Эта статья представляет собой серию статей, и более поздние части будут обновлены позже, чтобы добиться прогресса вместе ~

  1. Чтение исходного кода Vue — файловая структура и механизм работы
  2. Чтение исходного кода Vue — принцип сбора зависимостей
  3. Чтение исходного кода Vue — пакетное асинхронное обновление и принцип nextTick

Большинство сообщений в Интернете имеют разную глубину и даже некоторые несоответствия. Следующие статьи являются кратким изложением процесса обучения. Если вы найдете какие-либо ошибки, пожалуйста, оставьте сообщение, чтобы указать ~

Ссылаться на:

  1. Изучение исходного кода Vue2.1.7
  2. Раскрыта технология Vue.js
  3. Анализ внутреннего рабочего механизма Vue.js
  4. Документация Vue.js
  5. Запись: window.MessageChannel эти вещи
  6. MDN - MessageChannel
  7. Цикл событий в JS и Node.js
  8. Хуан И — примечания по обновлению Vue.js
  9. Механизм Vue nextTick

PS: Всех приглашаю обратить внимание на мой публичный аккаунт [Front End Afternoon Tea], давайте работать вместе~

Кроме того, вы можете присоединиться к группе WeChat «Front-end Afternoon Tea Exchange Group», нажмите и удерживайте, чтобы определить QR-код ниже, чтобы добавить меня в друзья, обратите вниманиеДобавить группу, я заберу тебя в группу~