описание сцены
Когда я недавно использовал ведро семейства Vue в качестве фоновой системы, я столкнулся с очень странной проблемой: есть поле ввода, которое позволяет вводить только числа, при вводе других типов данных содержимое ввода будет сброшено до нуля. Для этого используются родительский компонент и дочерний компонент. Для удобства изложения бизнес-сценарий здесь упрощен, а конкретный код выглядит следующим образом:
// 父组件
<template>
<Input v-model="form.a" @on-change="onChange"></Input>
</template>
<script type="javascript">
export default {
data() {
return {
form: {
a: null
}
}
},
methods: {
async onChange(value) {
if (typeof value !== 'number') {
// await this.$nextTick()
this.form.a = null
}
}
}
}
</script>
// 子组件
<template>
<input v-model="currentValue" @input="onInput" />
</template>
<script type="javascript">
export default {
name: 'Input',
props: {
value: {
type: [Number, Null],
default: null
}
},
data() {
return {
currentValue: null
}
},
methods: {
onInput(event) {
const value = event.target.value
this.$emit('input', value)
const oldValue = this.value
if (oldValue === value) return
this.$emit('on-change', value)
}
},
watch: {
value(value, oldValue) {
this.currentValue = value
}
}
}
</script>
Поместите приведенный выше код в проект и запустите его, вы волшебным образом обнаружите, что после ввода строки «abc» в поле ввода значение поля ввода не сбрасывается до пустого, а остается неизменным как «abc». После раскомментирования комментария nextTick значение поля ввода сбрасывается до пустого. Действительно удивительно.
На самом деле коллеги уже несколько раз сталкивались с подобными сценариями: слой данных изменился, а дом не отреагировал соответствующим образом. После того, как уровень данных изменился, выполните nextTick, и дом будет обновлен, как и ожидалось. После нескольких таких раз мы даже пошутили: nextTick нерешителен.
порядок выполнения кода
Итак, что именно делает nextTick? Взяв приведенный выше код в качестве примера, давайте сначала разберемся, как выполняется наш код. В частности, последовательность выполнения приведенного выше кода выглядит следующим образом:
- Начальное значение form.a равно null
- Строка пользовательского ввода abc
- Запустите событие ввода, значение form.a изменится на abc
- Запускается событие изменения, и значение form.a изменяется на null
- Поскольку значение form.a здесь по-прежнему равно нулю
- После выполнения задачи основного потока проверьте, нужно ли выполнять функцию обратного вызова часов.
Как только этот порядок будет соблюден, мы обнаружим причину, по которой поле ввода показывает, что abc не пусто: хотя значение form.a изменилось в середине основного потока, оно всегда равно нулю от начала до конца. конец. То есть значение реквизита дочернего компонента не изменилось. Естественно, callback-функция часов выполняться не будет.
Но таким образом у нас возникает другой вопрос: почему вызывается событие ввода, когда значение form.a изменяется на ноль, обратный вызов watch не срабатывает? Чтобы проиллюстрировать это, нам нужно копаться в исходном коде Vue, чтобы увидеть, когда функции обратного вызова $emit и watch выполняются соответственно.
что делает $emit?
Давайте сначала посмотрим на исходный код, соответствующий $emit. Поскольку исходный код версии Vue 2.X написан с использованием потока, стоимость понимания увеличивается незаметно. Имея это в виду, мы непосредственно находим файл vue.js в пакете dist Vue и ищем функцию emit.
Vue.prototype.$emit = function (event) {
var vm = this;
{
var lowerCaseEvent = event.toLowerCase();
if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
tip(
"Event \"" + lowerCaseEvent + "\" is emitted in component " +
(formatComponentName(vm)) + " but the handler is registered for \"" + event + "\". " +
"Note that HTML attributes are case-insensitive and you cannot use " +
"v-on to listen to camelCase events when using in-DOM templates. " +
"You should probably use \"" + (hyphenate(event)) + "\" instead of \"" + event + "\"."
);
}
}
var cbs = vm._events[event];
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs;
var args = toArray(arguments, 1);
var info = "event handler for \"" + event + "\"";
for (var i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info);
}
}
return vm
};
function invokeWithErrorHandling (
handler,
context,
args,
vm,
info
) {
var res;
try {
res = args ? handler.apply(context, args) : handler.call(context);
if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); });
// issue #9511
// avoid catch triggering multiple times when nested calls
res._handled = true;
}
} catch (e) {
handleError(e, vm, info);
}
return res
}
Содержимое исходников на самом деле очень простое, то есть прописанные заранее функции (или подписка) поместить в массив, а при выполнении функции $emit функции из массива вынимаются и выполняются одна за другой . Видно, что это использование модели публикации-подписки.
То есть выполнение emit синхронно. Итак, как работают часы? Напротив, исполнение часов будет более громоздким. Когда вы понимаете процесс наблюдения, вы также понимаете суть Vue.
Во-первых, при инициализации компонента Vue есть функция initWatch, давайте посмотрим, что делает эта функция.
function initWatch (vm, watch) {
for (var key in watch) {
var handler = watch[key];
if (Array.isArray(handler)) {
for (var i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
} else {
createWatcher(vm, key, handler);
}
}
}
function createWatcher (
vm,
expOrFn,
handler,
options
) {
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
if (typeof handler === 'string') {
handler = vm[handler];
}
return vm.$watch(expOrFn, handler, options)
}
Vue.prototype.$watch = function (
expOrFn,
cb,
options
) {
var vm = this;
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {};
options.user = true;
var watcher = new Watcher(vm, expOrFn, cb, options);
if (options.immediate) {
try {
cb.call(vm, watcher.value);
} catch (error) {
handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));
}
}
return function unwatchFn () {
watcher.teardown();
}
}
var Watcher = function Watcher (
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
// options
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.lazy = !!options.lazy;
this.sync = !!options.sync;
this.before = options.before;
} else {
this.deep = this.user = this.lazy = this.sync = false;
}
this.cb = cb;
this.id = ++uid$2; // uid for batching
this.active = true;
this.dirty = this.lazy; // for lazy watchers
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = expOrFn.toString();
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
warn(
"Failed watching path: \"" + expOrFn + "\" " +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
);
}
}
this.value = this.lazy
? undefined
: this.get();
}
function parsePath (path) {
if (bailRE.test(path)) {
return
}
var segments = path.split('.');
return function (obj) {
for (var i = 0; i < segments.length; i++) {
if (!obj) { return }
obj = obj[segments[i]];
}
return obj
}
}
Watcher.prototype.get = function get () {
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value
}
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (customSetter) {
customSetter();
}
// #7981: for accessor properties without setter
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
});
}
var Dep = function Dep () {
this.id = uid++;
this.subs = [];
}
Dep.prototype.addSub = function addSub (sub) {
this.subs.push(sub);
};
Dep.prototype.removeSub = function removeSub (sub) {
remove(this.subs, sub);
};
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
Dep.prototype.notify = function notify () {
// stabilize the subscriber list first
var subs = this.subs.slice();
if (!config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort(function (a, b) { return a.id - b.id; });
}
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
}
Dep.target = null;
var targetStack = [];
function pushTarget (target) {
targetStack.push(target);
Dep.target = target;
}
function popTarget () {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}
Мы видим, что с часами связано около 20 функций. Когда так много функций прыгают туда-сюда, легко потерять логику. Здесь мы говорим обо всем процессе.
Когда экземпляр Vue инициализируется, выполняется initWatch, и функция initWatch завершает работу, чтобы создать экземпляр наблюдателя. Экземпляр наблюдателя выполняет функцию получения, а функция получения считывает значение свойства данных, тем самым вызывая функцию получения в Object.defineProperty. Функция get выполняет функцию dep.depend, которая используется для сбора зависимостей. Так называемые зависимости на самом деле являются функциями обратного вызова. В примере, о котором мы говорим, это функция обратного вызова watch со значением.
Говоря об этом, мы обнаружили, что функция обратного вызова часов только зарегистрирована здесь и не была выполнена. Итак, где настоящее исполнение часов? Вернемся к порядку выполнения исходного кода. На шаге 3, form.a=abc, выполняется операция набора. Эта операция запускает функцию set Object.defineProperty, а функция set выполняет функцию dep.notify. После выполнения функции обновления ядром функции обновления является функция queueWatcher. Для лучшей иллюстрации давайте перечислим функцию queueWatcher отдельно.
function queueWatcher (watcher) {
var 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.
var 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;
if (!config.async) {
flushSchedulerQueue();
return
}
nextTick(flushSchedulerQueue);
}
}
}
function flushSchedulerQueue () {
currentFlushTimestamp = getNow();
flushing = true;
var watcher, id;
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort(function (a, b) { return a.id - b.id; });
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null;
watcher.run();
// in dev build, check and stop circular updates.
if (has[id] != null) {
circular[id] = (circular[id] || 0) + 1;
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? ("in watcher with expression \"" + (watcher.expression) + "\"")
: "in a component render function."
),
watcher.vm
);
break
}
}
}
// keep copies of post queues before resetting state
var activatedQueue = activatedChildren.slice();
var updatedQueue = queue.slice();
resetSchedulerState();
// call component updated and activated hooks
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush');
}
}
Watcher.prototype.run = function run () {
if (this.active) {
var value = this.get();
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
var oldValue = this.value;
this.value = value;
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
}
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
}
function nextTick (cb, ctx) {
var _resolve;
callbacks.push(function () {
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(function (resolve) {
_resolve = resolve;
})
}
}
var timerFunc;
// 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 next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
timerFunc = function () {
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); }
};
isUsingMicroTask = true;
} 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
// (#6466 MutationObserver is unreliable in IE11)
var counter = 1;
var observer = new MutationObserver(flushCallbacks);
var textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true
});
timerFunc = function () {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
isUsingMicroTask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = function () {
setImmediate(flushCallbacks);
};
} else {
// Fallback to setTimeout.
timerFunc = function () {
setTimeout(flushCallbacks, 0);
};
}
В функции queueWatcher мы видим знакомое лицо: nextTick. Мы обнаружили, что nextTick — это плавная деградация микрозадачи: она будет использовать Promise, MutationObserver, setImmediate и setTimeout для выполнения задачи по очереди, в зависимости от среды. Мы видим, что при выполнении form.a=abc сначала регистрируется микрозадача, которую мы можем понимать как функцию-оболочку функции обратного вызова watch. Эта микрозадача будет выполнена после завершения задачи основного потока, поэтому она будет приостановлена первой.
Затем основной поток выполняет form.a=null и снова запускает сеттер. Поскольку все они зарегистрированы form.a, они будут дедуплицированы перед помещением в очередь микрозадач, чтобы избежать многократного выполнения контрольных обратных вызовов. В этот момент выполнение задачи основного потока завершено, и функция-оболочка функции обратного вызова наблюдателя в очереди микрозадач выталкивается для выполнения.Поскольку значение form.a всегда равно нулю, функция обратного вызова не будет казнен.
После добавления функции $nextTick функция nextTick выполняется до того, как form.a=null, а функция nextTick выполняет оборачивающую функцию функции обратного вызова наблюдателя.В это время значение form.a равно abc, а старое значение и новое значение отличаются друг от друга, поэтому выполняется функция обратного вызова watch. В этот момент вся логика выпрямляется.
позже
Неожиданно использование простого nextTick на самом деле связано с основным принципом Vue!