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()
Метод сортирует наблюдателей в очереди по идентификатору от меньшего к большему, что гарантирует, что:
- Порядок обновления компонентов — от родительского к дочернему, поскольку родительские компоненты всегда создаются раньше дочерних.
- Наблюдатели за пользователями компонента (наблюдатели за слушателями) запускаются перед наблюдателями за рендерингом, потому что наблюдатели за пользователями часто создаются раньше, чем наблюдатели за рендерингом.
- Если компонент уничтожается во время работы наблюдателя родительского компонента, его выполнение наблюдателя будет пропущено.
В цикле for, который выполняет очередь один за другим,index < queue.length
Длина здесь не кэшируется, потому что во время выполнения существующих объектов-наблюдателей в очередь могут быть помещены дополнительные объекты-наблюдатели.
Затем модификация данных отражается от уровня модели к процессу просмотра:数据更改 -> setter -> Dep -> Watcher -> nextTick -> patch -> 更新视图
2. Принцип nextTick
2.1 Макрозадача/микрозадача
Вот посмотрите на метод, который содержит выполнение каждого наблюдателя, переданного в качестве обратного вызова.nextTick
Позже,nextTick
что делает этот метод. Но сначала нужно понятьEventLoop
,macro task
,micro task
Несколько понятий, если вы не понимаете, можете сослаться на нихЦикл событий в JS и Node.jsВ этой статье приведена картинка, показывающая отношения выполнения двух последних в основном потоке:
Объясните, когда основной поток завершает выполнение задачи синхронизации:
- Сначала движок берет первую задачу из очереди макрозадач, после завершения выполнения берет все задачи из очереди микрозадач и выполняет их последовательно;
- Затем взять из очереди макрозадач следующую, а после завершения выполнения снова вынуть все очереди микрозадач;
- До бесконечности, пока две задачи не окажутся в очереди на выполнение.
Распространенные типы асинхронных задач в среде браузера в соответствии с приоритетом:
-
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
Выполнена плавная деградация под другую совместимость браузера, илиПонижение стратегии:
-
macroTimerFunc
:setImmediate -> MessageChannel -> setTimeout
. Сначала проверьте, поддерживается ли он изначальноsetImmediate
, этот метод изначально реализован только в браузерах IE и Edge, а затем определяет, поддерживается ли онMessageChannel, если правильноMessageChannel
Если вы не знаете, вы можете обратиться кэта статья, если он еще не поддерживается, используйте его в последнюю очередьsetTimeout
; Зачем использовать приоритетsetImmediate
а такжеMessageChannel
вместо прямого использованияsetTimeout
Ну, это из-за правил HTML5Минимальная задержка выполнения setTimeout составляет 4 мс., а вложенный тайм-аут равен 10 мс.Для того, чтобы обратный вызов выполнялся как можно быстрее, первые два без ограничения минимальной задержки явно лучше, чемsetTimeout
. -
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
Здесь он разделен на три части, давайте посмотрим на него вместе;
- первый
nextTick
положить входящийcb
для функции обратного вызоваtry-catch
После обёртывания поместите его в анонимную функцию и запушите в массив callbacks, это делается для предотвращения единичногоcb
Если ошибка выполнения не приведет к зависанию всего потока JS, каждыйcb
Вся обертка предназначена для того, чтобы эти функции обратного вызова не влияли друг на друга, если выполнение было неправильным.Например, предыдущая все еще может быть выполнена после выдачи ошибки. - затем проверьте
pending
Статус, это то же самое, что и введенное ранееqueueWatcher
серединаwaiting
это значение, это бит флага, сначала этоfalse
входmacroTimerFunc
,microTimerFunc
метод установлен наtrue
, поэтому следующий вызовnextTick
не войдетmacroTimerFunc
,microTimerFunc
метод, следующий из двухmacro/micro tick
когдаflushCallbacks
Асинхронно выполнять задачи, собранные в очереди обратных вызовов, иflushCallbacks
В начале выполнения методаpending
задаватьfalse
, поэтому следующий вызовnextTick
пора начинать новый раундmacroTimerFunc
,microTimerFunc
, который формирует vueevent loop
. - Наконец, проверьте, передано ли
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改喽
Почему такой результат, объясните:
-
Синхронно:Когда имя в данных изменено, имя имени будет активировано в это время.
setter
серединаdep.notify
Уведомить наблюдателя рендеринга, который зависит от этих данных, чтобы перейтиupdate
,update
положитflushSchedulerQueue
функция переданаnextTick
, наблюдатель рендеринга находится вflushSchedulerQueue
время выполнения функцииwatcher.run
еще разdiff -> patch
Этот набор повторных рендеровre-render
Видите, в этом процессе будет повторная сборка зависимостей, этот процесс асинхронный, поэтому, когда мы напрямую изменяем имя, а затем печатаем, асинхронные изменения не вносятся.patch
в представление, поэтому элемент DOM в представлении по-прежнему остается исходным содержимым. -
Перед сеттером:Почему исходный контент печатается перед сеттером?
nextTick
При вызове поместите обратные вызовы в массив обратных вызовов один за другим, а затем выполните их позже.for
Цикл выполняется один за другим, поэтому он похож на концепцию очереди, первый вошел, первый вышел; после изменения имени триггер для заполнения наблюдателя рендерингаschedulerQueue
поставить в очередь и поставить его функцию выполненияflushSchedulerQueue
Перейти кnextTick
, в это время уже есть в очереди обратных вызововsetter前函数
потому что этоcb
вsetter前函数
После этого он помещается в очередь обратных вызовов, затем метод «первым поступил — первым обслужен» сначала выполняет обратные вызовы в обратных вызовах.setter前函数
, наблюдатель рендеринга в это время не выполняетсяwatcher.run
, поэтому напечатанный элемент DOM по-прежнему остается исходным содержимым. -
После сеттера:После выполнения сеттера
flushSchedulerQueue
, то наблюдатель рендеринга изменилpatch
к представлению, поэтому получение DOM в это время является измененным содержимым. -
Обещание пути:эквивалентно
Promise.then
Выполните эту функцию таким образом, чтобы DOM изменился. - Метод setTimeout:Наконец, задача выполнения задачи макроса, при которой DOM изменился.
Обратите внимание, что при выполненииsetter前函数
Перед этой асинхронной задачей выполнялся синхронный код, асинхронная задача не выполнялась, все$nextTick
Функция также выполняется, и все обратные вызовы помещаются в очередь обратных вызовов для ожидания выполнения, поэтому вsetter前函数
При выполнении очередь обратных вызовов выглядит так: [setter前函数
,flushSchedulerQueue
,setter后函数
,Promise方式函数
], это очередь микрозадач, после завершения выполнения выполняется макрозадачаsetTimeout
Так что распечатайте результаты выше.
Кроме того, если очередь задач макросов браузераsetImmediate
,MessageChannel
,setTimeout/setInterval
Различные типы задач будут выполняться одна за другой в порядке добавления в цикл событий в указанном выше порядке, поэтому, если браузер поддерживаетMessageChannel
,nextTick
казненmacroTimerFunc
, то если в очереди макрозадач есть обаnextTick
Добавленные задачи и задачи, добавленные пользователемsetTimeout
Типы задач будут выполняться в первую очередьnextTick
задачи, потому чтоMessageChannel
соотношение приоритетовsetTimeout
высота,setImmediate
То же самое справедливо.
Эта статья представляет собой серию статей, и более поздние части будут обновлены позже, чтобы добиться прогресса вместе ~
Большинство сообщений в Интернете имеют разную глубину и даже некоторые несоответствия. Следующие статьи являются кратким изложением процесса обучения. Если вы найдете какие-либо ошибки, пожалуйста, оставьте сообщение, чтобы указать ~
Ссылаться на:
PS: Всех приглашаю обратить внимание на мой публичный аккаунт [Front End Afternoon Tea], давайте работать вместе~
Кроме того, вы можете присоединиться к группе WeChat «Front-end Afternoon Tea Exchange Group», нажмите и удерживайте, чтобы определить QR-код ниже, чтобы добавить меня в друзья, обратите вниманиеДобавить группу, я заберу тебя в группу~