задний план
Давайте сначала посмотрим на часть кода выполнения 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.
-
потому что первый триггер
this.msg = 'end'
, который запускаетwatcher
изupdate
, тем самым помещая обратный вызов операции обновления в очередь событий vue. -
this.$nextTick
Также введите новую функцию обратного вызова для отправки очереди событий, все они проходятsetImmediate
-->MessageChannel
-->Promise
-->setTimeout
определятьtimeFunc
. а такжеPromise.resolve().then
Это микрозадача, поэтому сначала будет напечатано обещание. -
в поддержку
MessageChannel
а такжеsetImmediate
случае их порядок выполнения предшествуетsetTimeout
(В IE11/Edge задержка setImmediate может быть в пределах 1 мс, а setTimeout имеет минимальную задержку 4 мс, поэтому setImmediate выполняет функцию обратного вызова раньше, чем setTimeout(0). Во-вторых, поскольку массиву обратного вызова отдается приоритет в очереди событий. ), он напечатает 2, затем напечатает 3 -
но не поддерживает
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