🚩Исходный код Vue - принцип реализации nextTick

Vue.js

предисловие

В предыдущем столбце ответ подписчика состоит в том, чтобы сначала добавить подписчика в очередь, а затемnextTickфункция для обхода очереди и ответа каждому подписчику. Знакомый Vue APIVue.nextTickглобальные методы иvm.$nextTickМетоды экземпляра вызываются внутриnextTickФункция, под функцией которой можно понимать асинхронное выполнение входящей функции.

1. Внутренняя логика Vue.nextTick

в исполненииinitGlobalAPI(Vue)В глобальном API инициализации Vue определите так:Vue.nextTick.

function initGlobalAPI(Vue) {
    //...
    Vue.nextTick = nextTick;
}

Видно, что прямойnextTickфункция, возложенная наVue.nextTick, можно, очень просто.

Во-вторых, внутренняя логика VM.$ NEXTTICK

Vue.prototype.$nextTick = function (fn) {
    return nextTick(fn, this)
};

Видно, чтоvm.$nextTickВнутренне также называетсяnextTickфункция.

3. Предварительные знания

nextTickРоль функции можно понимать как асинхронное выполнение входящей функции, Здесь давайте сначала представим, что такое асинхронное выполнение, начиная с рабочего механизма JS.

1. Механизм работы JS

Выполнение JS является однопоточным. Так называемый однопоточный означает, что событийную задачу нужно поставить в очередь на выполнение. После завершения предыдущей задачи будет выполняться следующая задача. Это синхронная задача. В случае там, где следующая задача не может быть выполнена, вводится понятие асинхронных задач. Механизм работы JS можно просто описать следующими шагами.

  • Все задачи синхронизации выполняются в основном потоке, образуя стек контекста выполнения.
  • В дополнение к основному потоку существует также очередь задач. Пока у асинхронной задачи есть работающий результат, ее функция обратного вызова будет добавлена ​​в очередь задач как задача.
  • Как только все задачи синхронизации в стеке выполнения выполнены, очередь задач считывается, чтобы увидеть, какие задачи в ней находятся, добавляются в стек выполнения, и начинается выполнение.
  • Основной поток продолжает повторять третий шаг выше. Также известен как цикл событий (Event Loop).

2. Типы асинхронных задач

nextTickФункция выполняет входящую функцию асинхронно и является асинхронной задачей. Существует два типа асинхронных задач.

Процесс выполнения основного потока представляет собой тик, а все асинхронные задачи выполняются одна за другой через очередь задач. Очередь задач хранит задачи одну за другой. В спецификации указано, что задачи делятся на две категории, а именно макрозадачи и микрозадачи, и после завершения каждой макрозадачи все микрозадачи должны быть очищены.

Используйте фрагмент кода для визуализации порядка выполнения задач.

for (macroTask of macroTaskQueue) {
    handleMacroTask();
    for (microTask of microTaskQueue) {
        handleMicroTask(microTask);
    }
}

В среде браузера Общие способы создания задач макросов:

  • setTimeout, setInterval, postMessage, MessageChannel (выполнение очереди имеет приоритет над setTimeiout)
  • сетевой запрос ввода-вывода
  • Взаимодействие со страницей: DOM, мышь, клавиатура, события прокрутки
  • рендеринг страницы

Распространенные способы создания микрозадач

  • Promise.then
  • MutationObserve
  • process.nexttick

существуетnextTickфункция для использования этих методов для передачи параметровcbВходящая функция обрабатывается как асинхронная задача.

3. функция следующего тика

var callbacks = [];
var pending = false;
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();
    }
    if (!cb && typeof Promise !== 'undefined') {
        return new Promise(function(resolve) {
            _resolve = resolve;
        })
    }
}

можно увидеть вnextTickпередать параметры в функциюcbПереданная функция, оберните ее и нажмите наcallbacksв массиве.

затем используйте переменнуюpendingЧтобы гарантировать, что выполнение выполняется только один раз в цикле событийtimerFunc().

окончательное исполнениеif (!cb && typeof Promise !== 'undefined'), параметр оценкиcbНе существует, и браузер поддерживает Promise, возвращает объект экземпляра класса Promise. НапримерnextTick().then(() => {}),когда_resolveКогда функция выполняется, логика then будет выполнена.

посмотриtimerFuncДля определения функции давайте просто рассмотрим использование Promise для создания асинхронного выполнения. ztimerFuncфункция .

var timerFunc;
if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve();
    timerFunc = function() {
        p.then(flushCallbacks);
        if (isIOS) {
            setTimeout(noop);
        }
    };
}

нашел в немtimerFuncФункция вызывается с помощью множества асинхронно выполняемых методов.flushCallbacksфункция.

посмотриflushCallbacksфункция

var callbacks = [];
var pending = false;
function flushCallbacks() {
    pending = false;
    var copies = callbacks.slice(0);
    callbacks.length = 0;
    for (var i = 0; i < copies.length; i++) {
        copies[i]();
    }
}

воплощать в жизньpending = falseвключить следующий цикл событийnextTickвызов функцииtimerFuncфункция.

воплощать в жизньvar copies = callbacks.slice(0);callbacks.length = 0;Набор функций, которые должны выполняться асинхронноcallbacksклонировать в константуcopies, затем поставьтеcallbacksПустой.

затем пройтиcopiesВыполнить каждую функцию. назадnextTickin - передать параметрcbПереданная функция упаковывается и помещается вcallbacksв коллекции. Посмотрим, как он упакован.

function() {
    if (cb) {
        try {
            cb.call(ctx);
        } catch (e) {
            handleError(e, ctx, 'nextTick');
        }
    } else if (_resolve) {
        _resolve(ctx);
    }
}

Логика проста. если параметрcbиметь значение. выполнить в инструкции trycb.call(ctx),параметрctxпараметр, передаваемый функции. Если выполнение не выполняетсяhandleError(e, ctx, 'nextTick').

если параметрcbНеважно. воплощать в жизнь_resolve(ctx),Потому чтоnextTickКак использовать параметры в функцииcbЕсли значения нет, он вернет объект экземпляра класса Promise, а затем выполнит_resolve(ctx), тогда будет выполнена логика.

сюдаnextTiceОсновная логика функции очень понятна. Определить переменнуюcallbacks, передать параметрcbВходящая функция обернута функцией, в которой будет выполняться входящая функция, а также будут обрабатываться сбой выполнения и параметрыcbнесуществующую сцену, затем добавьте вcallbacks. передачаtimerFuncфункция, в которой можно пройтиcallbacksвыполнять каждую функцию, потому чтоtimerFuncэто функция, которая выполняется асинхронно и определяет переменнуюpendingчтобы убедиться, что он вызывается только один раз в цикле событийtimerFuncфункция. Вот и всеnextTiceФункция асинхронно выполняет функцию переданной функции.

Итак, ключ в том, как определитьtimerFuncфункция. Поскольку методы создания функций асинхронного выполнения различаются в каждом браузере, требуется обработка совместимости.Различные методы представлены ниже.

1, обещание создать асинхронную функцию выполнения

if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve();
    timerFunc = function() {
        p.then(flushCallbacks);
        if (isIOS) {
            setTimeout(noop);
        }
    };
    isUsingMicroTask = true;
}

воплощать в жизньif (typeof Promise !== 'undefined' && isNative(Promise))Определить, поддерживает ли браузер Promise,

вtypeof PromiseЕсли он поддерживается, то это function , а не undefined, поэтому условие выполняется, что легко понять.

Рассмотрим другое условие, гдеisNativeКак определяется метод, код выглядит следующим образом.

function isNative(Ctor) {
    return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}

когдаCtorэто тип функции, выполнить/native code/.test(Ctor.toString()), чтобы определить, есть ли в строке после функции toString фрагмент нативного кода, так зачем его так мониторить. Это связано с тем, что toString здесь является методом экземпляра Function.Если встроенная функция браузера вызывает метод экземпляра toString, возвращаемый результатfunction Promise() { [native code] }.

Если браузер поддерживает это, выполнитеvar p = Promise.resolve(),Promise.resolve()Метод можно вызывать без параметров, и он напрямую возвращает разрешенный объект Promise.

затем вtimerFuncвыполнять в функцииp.then(flushCallbacks)будет выполняться напрямуюflushCallbacksфункция, в которой он проходит для выполнения каждогоnextTickВходящая функция, поскольку Promise — это тип микрозадачи, эти функции становятся асинхронными.

воплощать в жизньif (isIOS) { setTimeout(noop)}Добавить пустой таймер под браузер IOS для принудительного обновления очереди микрозадач.

2. MutationObserver создает функцию асинхронного выполнения

if (!isIE && typeof MutationObserver !== 'undefined' &&
    (isNative(MutationObserver) ||
    MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
    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;
}

MutationObserver() создает и возвращает новый MutationObserver, который будет вызываться при изменении указанного DOM, совместим только с браузерами IE11, поэтому просто выполните!isIEИсключить браузер IE. воплощать в жизньtypeof MutationObserver !== 'undefined' && (isNative(MutationObserver)Суждение, принцип которого был представлен выше. воплощать в жизньMutationObserver.toString() === '[object MutationObserverConstructor]')Это для того, чтобы судить о поддержке браузера PhantomJS и браузера iOS 7.x.

воплощать в жизньvar observer = new MutationObserver(flushCallbacks), создайте новый MutationObserver и назначьте его константеobserver, и положиflushCallbacksпередается как функция возврата, когдаobserverВызывается, когда указанное свойство DOM прослушивает измененияflushCallbacksфункция.

воплощать в жизньvar textNode = document.createTextNode(String(counter))Создайте текстовый узел.

воплощать в жизньvar counter = 1,counterСделайте содержимое текстового узла.

воплощать в жизньobserver.observe(textNode, { characterData: true }), который вызывает метод экземпляра MutationObserverobserveконтролироватьtextNodeСодержимое текстового узла.

Очень умное использование здесьcounter = (counter + 1) % 2,ПозволятьcounterВарьируется от 1 до 0. повторно выполнитьtextNode.data = String(counter)изменитьcounterУстановите содержимое текстового узла. такobserverОн будет следить за изменением содержимого текстового узла, за которым он наблюдает, и вызоветflushCallbacksфункция, в которой он проходит для выполнения каждогоnextTickВходящая функция, т.к. MutationObserver относится к типу микрозадачи (микрозадачи), поэтому эти функции становятся асинхронным выполнением.

3. setImmediate создает функцию асинхронного выполнения

if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    timerFunc = function() {
        setImmediate(flushCallbacks);
    };
} 

setImmediateСовместим только с браузерами выше IE10, другие браузеры не совместимы. Это макрозадача, которая потребляет меньше ресурсов.

4. setTimeout создает функцию асинхронного выполнения

timerFunc = function() {
    setTimeout(flushCallbacks, 0);
}

Совместимость с браузерами ниже IE10, создание асинхронной задачи, которая является макрозадачей и потребляет много ресурсов.

5. Порядок создания функций асинхронного исполнения

Vue исторически был выпущен вnextTickреализован в функцииtimerFuncвнесены некоторые коррективы в порядок

первое изданиеnextTickреализован в функцииtimerFuncполучатель чего-тоPromise,MutationObserver,setTimeout.

Реализовано в версии 2.5.0timerFuncизменить порядокsetImmediate,MessageChannel,setTimeout. В этой версии убраны все способы создания микрозадач, так как приоритет микрозадач слишком высок, номер одной из задач #6566, ситуация следующая:

<div class="header" v-if="expand"> // block 1
    <i @click="expand = false;">Expand is True</i> // element 1
</div>
<div class="expand" v-if="!expand" @click="expand = true;"> // block 2
    <i>Expand is False</i> // element 2
</div>

При нажатии на элемент 1 по обычной логике будетexpandустановлен вfalse, блок 1 не будет отображаться, а блок 2 будет отображаться, нажмите на блок 2, он будет отображатьсяexpandустановлен вfalse, то будет отображаться блок 1.

В то время реальная ситуация заключалась в том, чтобы щелкнуть элемент 1, и отобразился бы только блок 1. Вот почему и что вызывает эту ошибку. Вот как Vue официально объясняет это

Событие клика — это задача макроса,<i>Событие click на nextTick (микрозадача) запускает первое обновление. Обрабатывайте микрозадачи до того, как события всплывут во внешний div. Во время обновления во внешний div будет добавлен прослушиватель кликов. Поскольку структура DOM одинакова, и внешний элемент div, и внутренний элемент используются повторно. Событие в конечном итоге достигает внешнего элемента div, запуская прослушиватель, добавленный первым обновлением, которое, в свою очередь, запускает второе обновление. Чтобы исправить это, вы можете просто дать двум внешним div разные ключи, чтобы заставить их заменяться во время обновлений. Это предотвратит получение всплывающих событий.

Конечно, официальное решение все же было дано в то время.timerFuncвместо этого реализуются путем создания задачи макроса, порядок которойsetImmediate,MessageChannel,setTimeout, поэтому nextTick — это задача макроса.

Событие клика — это задача макроса.Обновление nextTick (макроса задачи), запущенное после выполнения события клика, будет выполняться только в следующем цикле событий, так что его всплытие событий уже выполнено. Багов не будет.

Но вскоре осознаниеtimerFuncПорядок изменен наPromise,MutationObserver,setImmediate,setTimeoutИспользование макрозащитников в любом месте приведет к некоторым замечательным проблемам. Представитель номер выпуска - номер # 6813, и код будет набран. Вы можете увидетьздесь. Здесь есть два ключевых элемента управления

  • Медиа-запрос, когда ширина страницы больше 1000 пикселей, тип отображения li — это встроенное поле, а когда он меньше 1000 пикселей, тип отображения — элемент уровня блока.
  • Мониторинг масштабирования страницы, когда ширина страницы меньше 1000 пикселей, ul используетv-show="showList"Элементы управления скрыты.

Начальное состояние:

Когда веб-граница быстрого перетаскивания уменьшает ширину страницы, сначала отобразите первую карту ниже, а затем быстро скройте, а не скрывайте напрямую.

Чтобы появилась такая ошибка, мы должны сначала понять концепцию, время выполнения UI Render (рендеринга пользовательского интерфейса), как показано ниже:

    1. макрос принимает задачу макроса.
    1. micro очищает очередь микрозадач.
    1. Определите, стоит ли обновлять текущий кадр, в противном случае повторите шаг 1.
    1. Перед отрисовкой кадра выполните задачу очереди requestAnimationFrame.
    1. Обновление пользовательского интерфейса, выполнение рендеринга пользовательского интерфейса.
    1. Если очередь задач макроса не пуста, повторите шаг

Этот процесс также относительно прост для понимания. Раньше для мониторинга масштабирования окна выполнялась макрозадача. Когда размер окна меньше 1000 пикселей,showListстанетflase, вызовет выполнение nextTick, которое является задачей макроса. Между двумя задачами макроса будет выполняться рендеринг пользовательского интерфейса. В это время параметр встроенного кадра li недействителен и отображается как кадр на уровне блока. После выполнения задачи макроса nextTick, когда рендеринг пользовательского интерфейса выполняется снова , отображаемое значение ul. Переключитесь на none, чтобы скрыть список.

Поэтому Vue считает, что с управляемостью nextTick, созданного с помощью микрозадач, все в порядке, в отличие от nextTick, созданного с помощью макрозадач, которые будут иметь неконтролируемые сценарии.

Используйте метку времени в 2.6+, чтобы исправить #6566 эту ошибку, установите переменнуюattachedTimestamp, при выполнении входящегоnextTickв функцииflushSchedulerQueueфункция, выполнениеcurrentFlushTimestamp = getNow()Получить временную метку и присвоить ее переменнойcurrentFlushTimestamp, а затем выполнить захват перед прослушиванием событий в DOM. Егоaddреализован в функции.

function add(name, handler, capture, passive) {
    if (useMicrotaskFix) {
        var attachedTimestamp = currentFlushTimestamp;
        var original = handler;
        handler = original._wrapper = function(e) {
            if (
                e.target === e.currentTarget ||
                e.timeStamp >= attachedTimestamp ||
                e.timeStamp <= 0 ||
                e.target.ownerDocument !== document
            ) {
                return original.apply(this, arguments)
            }
        };
    }
    target.addEventListener(
        name,
        handler,
        supportsPassive ? {
            capture: capture,
            passive: passive
        } : capture
    );
}

воплощать в жизньif (useMicrotaskFix),useMicrotaskFixУстановите при создании асинхронно выполняющихся функций с микрозадачамиtrue.

воплощать в жизньvar attachedTimestamp = currentFlushTimestampНазначьте отметку времени, когда функция обратного вызова nextTick выполняется переменнойattachedTimestamp, затем выполнитеif(e.timeStamp >= attachedTimestamp)e.timeStampСобытие в DOM было запущено с отметкой времени больше, чемattachedTimestamp, это событие будет выполнено.

Почему, вернемся к ошибке #6566. Поскольку приоритет выполнения микрозадачи очень высок, это происходит быстрее, чем всплывающее событие в ошибке #6566, из-за которой появляется эта ошибка. при нажатииiСобытие всплытия запускается, когда метка предшествует выполнению nextTick, затемe.timeStampСравниватьattachedTimestampНебольшой, если всплывающее событие выполняется, это приведет к ОШИБКЕ # 6566, поэтому этой ОШИБКИ можно избежать только в том случае, если всплывающее событие запускается позже, чем выполнение nextTick, поэтомуe.timeStampСравниватьattachedTimestampПузырьковые события могут быть только выполнены.