следующийTick
во ВьюnextTickКогда дело доходит до асинхронного обновления DOM во Vue, это кажется очень интересным, и я специально узнал об этом. из них оnextTickИсходный код требует много знаний,nextTickЭто основная реализация Vue.nextTickПрежде, чтобы облегчить ваше понимание, позвольте мне кратко представить механизм работы JS.
JS-операционный механизм
Выполнение JS является однопоточным и основано на цикле событий. Цикл событий примерно разделен на следующие этапы:
(1) Все задачи синхронизации выполняются в основном потоке, образуя стек контекста выполнения.
(2) В дополнение к основному потоку существует еще «очередь задач» (task queue). Как только асинхронная задача имеет запущенный результат, событие помещается в «очередь задач».
(3) Как только все задачи синхронизации в «стеке выполнения» будут выполнены, система прочитает «очередь задач», чтобы увидеть, какие события в ней находятся. Затем соответствующие асинхронные задачи завершают состояние ожидания, входят в стек выполнения и начинают выполнение.
(4) Основной поток продолжает повторять третий шаг выше.
Процесс выполнения основного потока — это тик, а все асинхронные результаты планируются через «очередь задач». Очередь сообщений хранит задачи одну за другой. В спецификации указано, что задачи делятся на две категории, а именноmacro taskа такжеmicro task, и каждыйmacro taskПосле окончания все микро задания должны быть очищены.
оmacro taskа такжеmicro taskКонцепция , не будет здесь подробно рассматриваться, просто продемонстрируйте порядок их выполнения с помощью фрагмента кода:
for (macroTask of macroTaskQueue) {
// 1. Handle current MACRO-TASK
handleMacroTask();
// 2. Handle all MICRO-TASK
for (microTask of microTaskQueue) {
handleMicroTask(microTask);
}
}
В среде браузера общийmacro taskимеютsetTimeout,MessageChannel,postMessage,setImmediate;Общийmicro taskимеютMutationObseverа такжеPromise.then.
Реализация Vue (исходный код Vue 2.5+)
После исходного кода Vue 2.5+ реализация nextTick имеет отдельный JS-файл для его поддержки, его исходный код невелик, а всего это более 100 строк. Далее, давайте посмотрим на его реализацию, вsrc/core/util/next-tick.jsсередина:
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIOS, isNative } from './env'
const callbacks = []
let pending = false
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// Here we have async deferring wrappers using both microtasks and (macro) tasks.
// In < 2.4 we used microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using (macro) tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).
let microTimerFunc
let macroTimerFunc
let useMacroTask = false
// 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 */
if (typeof setImmediate !== 'undefined' && isNative(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 */
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = () => {
p.then(flushCallbacks)
// in problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
} else {
// fallback to macro
microTimerFunc = macroTimerFunc
}
/**
* Wrap a function so that if any code inside triggers state change,
* the changes are queued using a (macro) task instead of a microtask.
*/
export function withMacroTask (fn: Function): Function {
return fn._withTask || (fn._withTask = function () {
useMacroTask = true
const res = fn.apply(null, arguments)
useMacroTask = false
return res
})
}
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
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
next-tick.jsподтвержденныйmicroTimerFuncа такжеmacroTimerFunc2 переменные, которые соответствуютmicro taskфункция иmacro taskФункция. Для реализации задачи макроса сначала проверьте, поддерживается ли нативный setImmediate.Эта функция поддерживается только более высокими версиями IE и Edge.Если она не поддерживается, то проверьте, поддерживается ли нативный setImmediate.MessageChannel, если он не поддерживается, он будет понижен до setTimeout 0; и дляmicro taskреализация, проверьте, поддерживает ли браузер нативную поддержкуPromise, если не поддерживается, указать прямо наmacro taskреализация.
next-tick.js2 функции открыты для внешнего мира, давайте сначала посмотримnextTick, которая является функцией, которую мы использовали для выполнения nextTick(flushSchedulerQueue) в предыдущем разделе. Его логика также очень проста, поместите входящую функцию обратного вызова cb в массив обратных вызовов и, наконец, за один раз выполните macroTimerFunc или microTimerFunc в соответствии с условием useMacroTask, и все они выполнят flushCallbacks в следующем тике. простой, обходя обратные вызовы, а затем выполнить соответствующую функцию обратного вызова.
используется здесьcallbacksа не напрямуюnextTickПричина выполнения функции обратного вызова заключается в том, чтобы убедиться, что nextTick выполняется несколько раз в одном и том же тике, и несколько асинхронных задач не будут открыты, а эти асинхронные задачи сжаты в одну синхронную задачу, и выполнение будет завершено в следующем тике. .
nextTickВ конце функции также есть часть логики:
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
это когдаnextTickЕсли параметр cb не передан, предоставьте вызов Promise, например:
nextTick().then(() => {})
когда_resolveКогда функция выполняется, она переходит к логике then.
next-tick.jsтакже подвергаетсяwithMacroTaskФункция, которая представляет собой слой упаковки для функции, гарантирующий, что данные будут произвольно изменены во время выполнения функции, инициируя выполнение изменений.nextTickвынужден идтиmacroTimerFunc. Например, для некоторых событий взаимодействия с DOM, таких как обработка функции обратного вызова события, связанной с v-on, она будет принудительно запущена.macro task.
Vue.js предоставляет 2 способа вызова nextTick: один — это глобальный API Vue.nextTick, другой — метод vm.$nextTick для экземпляра, независимо от того, какой из них мы используем, последний метод — вызвать next-tick.js. Реализован метод nextTick.
Давайте посмотрим на исходный код до версии vue2.5.
/**
* Defer a task to execute it asynchronously.
*/
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]()
}
}
// the nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore if */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve()
var logError = err => { console.error(err) }
timerFunc = () => {
p.then(nextTickHandler).catch(logError)
// in problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
} else {
// fallback to setTimeout
/* istanbul ignore next */
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()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
_resolve = resolve
})
}
}
})()
Во-первых, чтобы понятьnextTickТри важные переменные, определенные в .
callbacks
Используется для хранения всех функций обратного вызова, которые необходимо выполнить.
pending
Используется, чтобы отметить, выполняется ли функция обратного вызова
timerFunc
Используется для запуска выполнения функции обратного вызова
Далее узнайте оnextTickHandler()функция.
function nextTickHandler () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
Эта функция используется для выполнения всех функций обратного вызова, сохраненного в обратных вызовах. Следующим шагом является назначение метода триггера на TimerFunc.
Сначала определите, поддерживаются ли обещания изначально, и если да, используйте обещания для запуска и выполнения функций обратного вызова;
В противном случае, если поддерживаетсяMutationObserver, создается экземпляр объекта-наблюдателя, и при изменении наблюдаемого текстового узла запускаются и выполняются все функции обратного вызова.
Если не поддерживается, используйте setTimeout, чтобы установить задержку на 0.
Ну наконец тоqueueNextTickфункция. Поскольку nextTick — немедленная функция, поэтомуqueueNextTickФункция — это возвращаемая функция, которая принимает параметры, переданные пользователем, и используется для хранения функции обратного вызова в обратных вызовах.
На приведенном выше рисунке показан весь процесс выполнения, ключ лежит в функции timeFunc(), которая играет роль отложенного выполнения. Из приведенного выше введения можно узнать, что существует три реализации timeFunc().
Promise
MutationObserver
setTimeout
Среди них хорошо известны Promise и setTimeout — асинхронная задача, которая будет вызывать определенные функции после синхронной задачи и асинхронной задачи обновления DOM. Далее основное внимание уделяется MutationObserver. MutationObserver — это новый API в HTML5, интерфейс для мониторинга изменений DOM. Он может отслеживать удаление дочерних узлов, изменение атрибутов, изменение текстового содержимого и т. д. в объекте DOM. Процесс вызова прост, но немного необычен: сначала нужно привязать к нему callback:
var mo = new MutationObserver(callback)
Скопируйте код, указавMutationObserverКонструктор прохода в обратном вызове можно получитьMutationObserverнапример, этот обратный вызов будет вызван вMutationObserverЗапускается, когда экземпляр ожидает изменения.
на этот раз ты просто даешьMutationObserverЭкземпляр привязан к обратному вызову.Еще не установлено, какой DOM отслеживать, следить за удалением узла или следить за изменением атрибута. Это можно сделать, вызвав его метод наблюдателя:
var domTarget = 你想要监听的dom节点
mo.observe(domTarget, {
characterData: true //说明监听文本内容的修改。
})
в следующем тикеMutationObserverфункционировать, как показано выше. После прослушивания обновления DOM вызывается функция обратного вызова.
На самом деле использоватьMutationObserverПричина в том, что nextTick хочет, чтобы асинхронный API выполнял асинхронный обратный вызов, который я хочу выполнить после выполнения текущего синхронного кода, включая Promise и setTimeout, основанные на этой причине. Углубленный также включает в себя такой контент, как микрозадачи.
Суммировать
@versions после 2.5 $nextTick использовал setImmediate > MessageChannel > setTimeout
@Pre-2.5 $nextTick использовал Promise > MutationObserver > setTimeout
через этот разделnextTickВ сочетании с анализом сеттера в предыдущем разделе мы узнали, что повторный рендеринг изменений данных в DOM — это асинхронный процесс, который происходит на следующем тике. Это то, что мы обычно делаем в процессе разработки, например, когда мы получаем данные из интерфейса сервера, данные изменяются, если некоторые из наших методов зависят от изменений DOM после модификации данных, мы должныnextTickпосле казни. Например, следующий псевдокод:
getData(res).then(()=>{
this.xxx = res.data
this.$nextTick(() => {
// 这里我们可以获取变化后的 DOM
})
})
Ссылаться на
Демистификация технологии Vue.js
Простое понимание nextTick в Vue