Статья впервые опубликована на:GitHub.com/US TB-Вуд, умри, о ты…
написать впереди
Некоторое время, чтобы написать пункты перед проектом, есть некоторые сомнения по поводу NEXTTICK. Изучив различную информацию, подведите итог стратегии асинхронного обновления Vue.js, а также использования и принципа NEXTTICK. Если есть место, чтобы обобщить неправильное место, добро пожаловать, чтобы указать!
Эта статья будет резюмирована из следующих 3 пунктов:
- Почему Vue.js обновляет представления асинхронно?
- Как JavaScript работает асинхронно?
- Когда использовать nextTick?
Давайте посмотрим на пример
<template>
<div>
<div ref="message">{{message}}</div>
<button @click="handleClick">点击</button>
</div>
</template>
export default {
data () {
return {
message: 'begin'
};
},
methods () {
handleClick () {
this.message = 'end';
console.log(this.$refs.message.innerText); //打印“begin”
}
}
}
Напечатанный результат — «начало». Мы четко назначаем сообщение «конец» в событии клика, но innerHTML реального узла DOM не получает ожидаемого «начала». Почему?
см. другой пример
<template>
<div>
<div>{{number}}</div>
<div @click="handleClick">click</div>
</div>
</template>
export default {
data () {
return {
number: 0
};
},
methods: {
handleClick () {
for(let i = 0; i < 10000; i++) {
this.number++;
}
}
}
}
После щелчка по событию клика число будет пройдено и увеличено 10 000 раз. В реактивной системе Vue.js вы можете взглянуть на мою предыдущую статьюПринцип адаптивной системы Vue.js. Мы знаем, что Vue.js пройдет через процессы «setter->Dep->Watcher->patch->View». .
Согласно предыдущему пониманию, каждый раз, когда число равно +1, будет срабатывать установщик числа для изменения реального DOM в соответствии с описанным выше процессом, а затем DOM будет обновляться 10 000 раз.Об этом интересно думать. ! Посмотрите описание на официальном сайте:Vue выполняет обновления DOM асинхронно. Как только наблюдается изменение данных, Vue открывает очередь и буферизует все изменения данных, которые происходят в том же цикле событий. Если один и тот же наблюдатель запускается несколько раз, он будет помещен в очередь только один раз. Эта дедупликация во время буферизации важна, чтобы избежать ненужных вычислений и манипуляций с DOM..
Как работает JavaScript
Чтобы облегчить понимание стратегии асинхронного обновления Vue.js и nextTick, сначала представим механизм работы следующего JS, обратитесь к учителю Руан Ифэн.Подробное объяснение механизма работы JavaScript: снова поговорим о цикле событий. Ключевая часть извлечения заключается в следующем: JS является однопоточным, а это означает, что одновременно может выполняться только одно действие. Он основан на опросе событий и может быть разделен на следующие этапы:
(1) Все задачи синхронизации выполняются в основном потоке, образуя стек контекста выполнения.
(2) В дополнение к основному потоку существует еще «очередь задач» (task queue). Как только асинхронная задача имеет запущенный результат, событие помещается в «очередь задач».
(3) Как только все задачи синхронизации в «стеке выполнения» будут выполнены, система прочитает «очередь задач», чтобы увидеть, какие события в ней находятся. Затем соответствующие асинхронные задачи завершают состояние ожидания, входят в стек выполнения и начинают выполнение.
(4) Основной поток продолжает повторять третий шаг выше.
На приведенном выше рисунке схематично показан основной поток и очередь задач. Пока основной поток пуст, он будет читать «очередь задач», как работает JavaScript. Этот процесс будет повторяться. Процесс выполнения основного потока — это тик. Все асинхронные результаты планируются через «очередь задач». Существует два основных типа очередей задач: "макрозадача" и "микрозадача". Эти два типа задач попадают в очередь задач. Общие макрозадачи включают setTimeout, MessageChannel, postMessage и setImmediate, а общие микрозадачи включают MutationObsever и Promise.then.
опрос событий
Когда Vue.js изменяет данные, он не будет изменять данные немедленно, а будет ждать обновления данных, опрашиваемых тем же событием, а затем единообразно обновлять представление.Знай почтиПример выше:
//改变数据
vm.message = 'changed'
//想要立即使用更新后的DOM。这样不行,因为设置message后DOM还没有更新
console.log(vm.$el.textContent) // 并不会得到'changed'
//这样可以,nextTick里面的代码会在DOM更新后执行
Vue.nextTick(function(){
console.log(vm.$el.textContent) //可以得到'changed'
})
Диаграмма:
Имитация следующего тика
Определение nextTick на официальном сайте:
Выполняет отложенный обратный вызов после завершения следующего цикла обновления DOM. Используйте этот метод сразу после изменения данных, чтобы получить обновленную модель DOM.
Следующее использует setTimeout для имитации nextTick. Во-первых, определите обратные вызовы для хранения nextTick. Прежде чем следующий тик обработает функцию обратного вызова, все cbs будут сохранены в этом массиве обратных вызовов. pending — это бит флага, который представляет состояние ожидания. Тогда setTimeout создаст в задаче событие flushCallbacks, а flushCallbacks будет выполнять все cbs в callbacks по очереди при выполнении.
// 存储nextTick
let callbacks = [];
let pending = false;
function nextTick (cb) {
callbacks.push(cb);
if (!pending) {
// 代表等待状态的标志位
pending = true;
setTimeout(flushCallbacks, 0);
}
}
function flushCallbacks () {
pending = false;
const copies = callbacks.slice(0);
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}
Реальный код намного сложнее, чем здесь.В исходном коде Vue.js nextTick определен в отдельном файле для обслуживания, в src/core/util/next-tick.js:
/* @flow */
/* globals MessageChannel */
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
})
}
}
После добавления комментариев:
/**
* Defer a task to execute it asynchronously.
*/
/*
延迟一个任务使其异步执行,在下一个tick时执行,一个立即执行函数,返回一个function
这个函数的作用是在task或者microtask中推入一个timerFunc,在当前调用栈执行完以后以此执行直到执行到timerFunc
目的是延迟到当前调用栈执行完以后执行
*/
export const nextTick = (function () {
/*存放异步执行的回调*/
const callbacks = []
/*一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送*/
let pending = false
/*一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timerFunc被调用*/
let timerFunc
/*下一个tick时的回调*/
function nextTickHandler () {
/*一个标记位,标记等待状态(即函数已经被推入任务队列或者主线程,已经在等待当前栈执行完毕去执行),这样就不需要在push多个回调到callbacks时将timerFunc多次推入任务队列或者主线程*/
pending = false
/*执行所有callback*/
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 */
/*
这里解释一下,一共有Promise、MutationObserver以及setTimeout三种尝试得到timerFunc的方法
优先使用Promise,在Promise不存在的情况下使用MutationObserver,这两个方法都会在microtask中执行,会比setTimeout更早执行,所以优先使用。
如果上述两种方法都不支持的环境则会使用setTimeout,在task尾部推入这个函数,等待调用执行。
参考:https://www.zhihu.com/question/55364497
*/
if (typeof Promise !== 'undefined' && isNative(Promise)) {
/*使用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 (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 IE11, iOS7, Android 4.4
/*新建一个textNode的DOM对象,用MutationObserver绑定该DOM并指定回调函数,在DOM变化的时候则会触发回调,该回调会进入主线程(比任务队列优先执行),即textNode.data = String(counter)时便会触发回调*/
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 */
/*使用setTimeout将回调推入任务队列尾部*/
timerFunc = () => {
setTimeout(nextTickHandler, 0)
}
}
/*
推送到队列中下一个tick时执行
cb 回调函数
ctx 上下文
*/
return function queueNextTick (cb?: Function, ctx?: Object) {
let _resolve
/*cb存到callbacks中*/
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
})
}
}
})()
Ключ — timeFunc(), который действует как отложенное выполнение.
Из приведенного выше введения можно узнать, что существует три реализации timeFunc().
- Promise
- MutationObserver
- setTimeout
использовать
Цель nextTick
Сценарий приложения: необходимо работать на основе нового представления после обновления представления.
См. пример: Нажмите кнопку show, чтобы отобразить исходное поле ввода v-show: false и получить фокус:
<div id="app">
<input ref="input" v-show="inputShow">
<button @click="show">show</button>
</div>
new Vue({
el: "#app",
data() {
return {
inputShow: false
}
},
methods: {
show() {
this.inputShow = true
this.$nextTick(() => {
this.$refs.input.focus()
})
}
}
})
Можете обратить внимание на мой паблик-аккаунт «Muchen Classmate», фермера на гусиной фабрике, который обычно записывает какие-то банальные мелочи, технологии, жизнь, инсайты и срастается.