В презентации мы опишем основную логику фреймворка Vue 2. Будет описана система реактивности, а также жизненный цикл компонента.
Мы начнем с создания собственной системы реактивности, но ее логика будет аналогична логике системы реактивности Vue.
После этого мы проанализируем систему реактивности Vue. А затем шаг за шагом проанализируем полный жизненный цикл компонента.
Материал основан на курсе Vue Mastery Advanced Components.
Ссылка: https://www.vuemastery.com/
Построение нашей системы реактивности.
Что такое реактивная система? Начнем с простого примера: (https://jsfiddle.net/bc6tp54x/ )
Понятно, Если мы напишем это, в последней строке мы получим начальное значение суммы. (https://jsfiddle.net/g0e4rby2/ )
После изменения цены мы все еще получаем предыдущее значение итога (10). Но мы хотим получить новое значение суммы (40). В отличие от Vue, ванильный javascript ничего не знает о зависимостях переменных.
Чтобы сделать наш пример реактивным, нам нужно сохранить код, вычисляющий общее значение, в хранилище. После этого, когда цена или количество изменятся, мы снова запускаем код и получаем новое значение total. Простым примером хранилища является массив. Хранилище может содержать не только код(функцию) для вычисления суммы. Хранилище может содержать другие функции для вычисления других переменных.
• Сохраняем наш код (запихиваем функцию в массив)
• Мы немедленно вызываем код и вычисляем начальное значение total.
• При изменении цены или количества мы снова запускаем код.
Простой пример (https://jsfiddle.net/aoh45e7g/ )
Здесь мы видим, как код коррелирует с нашей схемой
Мы можем продолжить пример. Если нам нужна переменная doublePrice, мы можем сделать это: (https://jsfiddle.net/4cd5ymwj/ )
Теперь, когда цена изменяется, мы вызываем replay и оцениваем total и doublePrice.
Пример не идеальный.
Если мы изменим количество и вызовем replay() после (мы вызовем replay, потому что нам нужно новое значение total ) — будут запущены все функции из хранилища.
Но нам не нужно запускать все функции. Только общее количество зависит от количества.
doublePrice не зависит от количества.
Если мы добавим еще одну переменную, например total2 = price2 * 4, а затем добавим этот код в хранилище, то получим избыточные пересчеты. Например, при изменении значения price — total пересчитывается, но total2 тоже пересчитывается. Нам нужно создать хранилище, запись и воспроизведение для каждой переменной по цене, по количеству, по цене2 и т. д. (альтернатива — одно хранилище и модифицированные функции записи и воспроизведения).
Позже мы улучшим наш пример и удалим это ограничение.
Нам нужно что-то вроде этого
Давайте инкапсулируем код, который вычисляет total, в отдельный класс.
Используйте класс (https://jsfiddle.net/5j8khfL2/):
Теперь вместо функций record и replay и глобального массива storage у нас есть один объект dep. Метод dep.depend выполняет работу функции record. Метод dep.notify выполняет работу функции replay. У нас больше нет глобального хранилища. Мы перемещаем наше хранилище в объект dep (назначаем его свойству subscribers)
Код кажется немного уродливым.
Вместо этого мы можем создать функцию watcher и вызвать ее:
Код функции watcher:
Мы видим, что наблюдатель просто скрывает (инкапсулирует) назначение анонимной функции глобальной переменной target, добавляя эту функцию в свойство subscribers (т.е. записать эту функцию) и первоначальный вызов анонимной функции.
«Анонимная функция» — это функция, вычисляющая переменную total.
Итоговые изменения:
Окончательный код (https://jsfiddle.net/8zjuev6p/)
Возникает вопрос: Почему мы должны использовать глобальную переменную target? Ответ на этот вопрос мы дадим позже. Есть пример без глобальной цели (https://jsfiddle.net/y54t2u1z/).
Продолжим улучшать наш пример.
Ранее мы заметили, что если у нас есть несколько переменных (переменные, от которых что-то зависит, например: цена, количество, цена2 и т. д.), нам нужно иметь несколько хранилищ. Это означает, что нам нужен объект класса Dep для каждой переменной.
Давайте переместим вещи в свойства, прежде чем идти дальше.
Представьте, что у каждого свойства есть свой объект класса Dep.
И мы запускаем функцию watcher.
Когда мы получаем data.price в нашей анонимной функции, мы хотим добавить эту анонимную функцию в объект Dep, принадлежащий свойству price. т.е. мы хотим добавить функцию в массив subscribers, свойство объекта Dep, вызвав dep.depend().
Это будет реализовано с помощью геттеров javascript.
Аналогично со свойством quantity. Мы добавляем функцию в объект Dep, принадлежащий свойству quantity.
На изображении ниже показано, как мы помещаем одну анонимную функцию в два разных массива — subscribers (и в два разных объекта Dep).
Если у нас есть другая функция с data.price в теле, нам также нужно добавить функцию в массив подписчиков объекта Dep.
Когда нам нужно вызывать dep.notify? Dep.notify должен вызываться, когда мы устанавливаем значение свойства объекта данных (это будет реализовано с помощью сеттеров javascript).
Мы хотим увидеть этот результат.
Для этих целей используется Object.defineProperty.
Давайте добавим геттеры и сеттеры к объекту data.
Объедините нашу самодельную систему реактивности (изображение 16) и предыдущий код с defineProperty (изображение 24).
В результате получаем окончательный код самодельной системы реактивности.
Несколько замечаний по коду:
У нас может быть несколько обратных вызовов для одного свойства (вспомните пример с doublePrice).
Если мы вызываем обратный вызов вне наблюдателя, цель имеет значение null, а геттеры просто возвращают значения свойств.
Обратите внимание, наблюдатель стал проще. Он больше не вызывает dep.depend, потому что dep.depend перемещается в геттер.
В результате мы получили реактивный код. Теперь и цена, и количество являются реактивными. Каждое свойство имеет соответствующий объект Dep и внутреннее значение.
Код запускается каждый раз при изменении цены или количества.
Наша реактивная система имеет ограничения. Давайте добавим новое свойство в data после цикла, делающего объект данных реактивным:
Очевидно, что factor не имеет геттера и сеттера. т.е. Фактор не является реактивным свойством. Предположим, у нас есть новая формула для суммы:
Изменение коэффициента не приводит к повторному запуску функции расчета итого.
Ограничение появляется из-за того, что мы делаем реактивным не полный объект, а «снимок» объекта. Цикл получает промежуточное состояние объекта и делает это промежуточное состояние реактивным. Когда мы добавляем новое свойство, мы создаем новое состояние («моментальный снимок») нашего объекта.
Вместо того, чтобы перебирать каждое свойство для добавления геттеров/сеттеров, мы можем настроить прокси для нашего объекта данных, используя:
Метод get вызывается, когда мы обращаемся к свойству прокси, dep.depend будет вызываться внутри метода get.
Метод set вызывается, когда мы изменяем значение свойства или когда добавляем новое свойство, внутри метода set будет вызываться dep.notify.
попробуем реализовать самодельную систему реактивности с помощью Proxy. Класс Dep остается прежним, объект данных тоже остается прежним. Давайте сохраним все наши данные на карте.
Параметр obj (внутри get и set) — это просто исходный объект данных, то есть data_without_proxy.
Мы также извлекаем итог в отдельную переменную, так как он не должен быть реактивным.
Полученный код.
Мы видим, что новые свойства являются реактивными.
Перейдем к Vue.
Наша цель — понять обе схемы из документации Vue.
Прежде чем мы начнем изучать, что происходит, когда компонент Vue создается или глобальный объект Vue инициализируется вызовом new Vue(…) (в обоих случаях алгоритм инициализации аналогичен), полезно узнать, какие преобразования происходят с шаблонами компоненты.
Шаблон компонента Vue компилируется в функцию рендеринга.
Например, этот код после компиляции генерирует соответствующую функцию рендеринга. Мы можем обратиться к этой функции рендеринга через app.$options.render.
Функция рендеринга генерирует и отображает html, например ‹div›hello, world‹/div›. (отрисовка html немного сложнее, но мы пока можем просмотреть его в таком упрощенном виде)
Мы можем использовать функцию рендеринга вместо свойства шаблона — (есть случаи использования, когда функции рендеринга удобнее, чем шаблоны https://ru.vuejs.org/v2/guide/render-function.html).
Оба примера эквивалентны.
Как мы уже упоминали выше, рендеринг html немного сложнее. Функция рендеринга не отображает html сразу, а возвращает виртуальный DOM. Виртуальный DOM используется для генерации и рендеринга html (другая функция принимает виртуальный dom и рендерит html, о ней мы поговорим позже). Виртуальный DOM простыми словами — это всего лишь объект javascript.
Например, функция рендеринга слева генерирует объект javascript справа.
Рендеринг включает в себя два этапа.
- Если у компонента Vue есть шаблон, этот шаблон компилируется в функцию рендеринга.
2. Функция рендеринга запускается и возвращает виртуальный DOM. Виртуальный DOM используется для создания и обновления «настоящего» DOM.
Для лучшего понимания Virtual DOM нам нужно знать, что такое DOM и среда браузера. Javascript можно использовать не только в браузере, но и на веб-сервере, или в другой среде (даже в кофемашине, например).
Каждая среда (веб-браузер, Node.js и т. д.) предоставляет собственный функционал. Среда предоставляет объекты и функции в дополнение к функциям и объектам, которые есть у vanilla javascript.
Например, веб-браузеры дают нам возможность манипулировать страницами. Node.js дает нам возможности для работы с файлами и т.д.
Вот изображение, показывающее, какую функциональность дает нам среда браузера:
У нас есть корневой объект window, который используется по-разному.
С одной стороны окно является глобальным объектом для javascript. С другой стороны, он представляет собой окно браузера и имеет методы для управления этим окном.
Вот пример использования window в качестве объекта браузера. Мы используем метод innerHeight, чтобы узнать высоту окна браузера:
Объектная модель документа (DOM) представляет все содержимое веб-страницы в виде дерева объектов, которое мы можем изменить. Мы можем получить доступ к объектам из дерева и можем их изменить. Эти изменения приводят к изменениям в «настоящем» html. DOM содержит функции, необходимые нам для работы с html-документом (например, «getElementsByName»).
Объектная модель браузера (BOM) — это дополнительные объекты, которые предоставляет нам среда браузера (setTimeout, alert, location и т. д.), кроме функций, работающих с html-документом.
Важно понимать, что DOM — это всего лишь спецификация. DOM — это не язык программирования. Проще говоря, DOM — это текстовый документ, описывающий, как получить доступ к элементу html или xml, как изменить содержимое элемента, как изменить элемент, как удалить элемент с веб-страницы. Фактически, этот текстовый документ описывает функции, которые могут это делать, их сигнатуры, параметры, возвращаемые значения и т.д. Пример функции: getElementsByTagName, getElementById. Среда браузера просто следует спецификации DOM.
Различные браузеры могут реализовать только часть спецификации. Не только браузеры могут поддерживать DOM, серверные скрипты также могут поддерживать DOM. Например, серверные скрипты, загружающие и анализирующие/обрабатывающие html-страницы, также могут использовать DOM. (Как браузеры они могут не полностью поддерживать спецификацию).
Другими словами, DOM — это API-интерфейс браузера, который позволяет скриптам (javascript, actionscript и т. д.) получать доступ к содержимому html-документа и изменять это содержимое. Каждый HTML-документ представлен в виде дерева DOM. Мы можем манипулировать деревом DOM. Все изменения в DOM-дереве приводят к изменениям в html-документе.
Есть пример использования DOM в среде браузера. Находим все теги h1, берем первый h1 и меняем содержимое тега.
DOM API имеет ограничения. Его методы показывают приемлемую производительность только для небольшого количества изменений. Методы DOM дороги и ресурсоемки. Мы получим плохую производительность, если запустим несколько функций DOM и обновим несколько элементов на веб-странице за короткое время.
Существует виртуальный DOM, созданный для решения проблемы частых обновлений DOM.
В отличие от DOM, виртуальный DOM не является официальной спецификацией. Это просто абстракция «настоящего» DOM или html-страницы, их представление в виде javascript-объекта. Когда мы изменяем этот объект javascript, мы не используем дорогие методы DOM, мы используем «легкие» методы ванильного javascript для изменения объектов или массивов.
После того, как мы закончим все изменения в виртуальном DOM (мы можем сделать много изменений), мы знаем, какие изменения нам нужно сделать в реальном DOM, и мы можем сделать их оптимизированным способом, используя минимальное количество DOM. вызовы API.
На изображении 48 показаны преобразования шаблона компонента при монтаже компонента. Прежде всего, когда компонент монтируется, его шаблон компилируется в функцию рендеринга. Этот шаг пропускается, если компонент не имеет шаблона и уже имеет функцию рендеринга. Затем функция рендеринга возвращает виртуальный DOM (Virtual Node — VNode). После этого платформа Vue преобразует виртуальный DOM (VNode) в DOM. В результате создается HTML-код компонента vue, который вставляется на веб-страницу.
Вот простой пример (изображение 49). Оба фрагмента кода выполняют одну задачу. Первый код использует шаблон, второй — функцию рендеринга.
Позже, если часть данных изменится (например, наш текст), это может вызвать повторный запуск нашей функции рендеринга, создав еще один виртуальный узел (Vnode). Затем Vue знает, как отличить новый VNode от старого VNode и сделать обновления фактической DOM наиболее эффективным способом.
Позже мы подробно покажем, как монтируется компонент Vue и как компонент перерисовывается после изменения реактивных данных.
Класс VUE и миксины
Теперь давайте посмотрим на жизненный цикл компонента. Мы собираемся посмотреть, что происходит, когда мы пишем новый Vue(…) или создаем новый компонент. (оба действия запускают аналогичный механизм инициализации).
У нас есть два примера. Первый простой:
Второй немного сложнее:
Мы видим явный вызов new Vue(…) в обоих примерах (в точке, где создается глобальный объект класса Vue), во втором примере у нас также есть вложенный компонент. Это означает, что вызов new Vue(…) для этого компонента происходит неявно. В результате у нас есть три экземпляра объекта Vue.
Мы можем найти код класса Vue в исходниках vue. https://github.com/vuejs/vue/blob/dev/src/core/instance/index.js
Мы можем думать о коде таким образом: в начале у нас есть «голый» код класса Vue. Это означает, что наш класс Vue не содержит никакого функционала, кроме вызова this._init. И у нас есть примеси, примеси — это функции, которые добавляют новую полезную функциональность к нашему исходному классу.
(я знаю, в Javascript нет классов, но для простоты мы называем функцию-конструктор классом)
Приступим к анализу миксинов.
Первый — это initMixin. Он добавляет в класс Vue функцию _init. Функция _init вызывается для каждого созданного компонента. https://github.com/vuejs/vue/blob/dev/src/core/instance/init.js Мы подробно рассмотрим функцию _init позже. В нескольких словах он вызывает beforeCreate, created, beforeMount, смонтированныехуки. И это место, где объект данных компонента становится реактивным.
Следующий миксин — stateMixin. Это место, где мы получаем свойства $data, $props, $set, $delete и $watch для компонента.
Свойства добавляются миксином к прототипу класса Vue. Помните, миксин добавляет новую функциональность в класс Vue. Новая функциональность добавляет к экземпляру компонента новые свойства ($data, $props, $set, $delete и $watch) во время инициализации.
Давайте рассмотрим свойства $data, $props, $set, $delete и $watch.
Свойство $data. В большинстве случаев объект, который мы отправляем в качестве аргумента в конструктор new Vue(..), имеет свойство data.
Свойство данных может быть функцией, которая возвращает объект или объект. Мы можем назвать этот результирующий объект как «объект данных».
Объект данных становится реактивным во время инициализации компонента.
Во время создания компонента «объект данных» сохраняется в свойстве _data.
В результате на реактивный «объект данных» ссылается _data внутри экземпляра Vue (внутри экземпляра компонента).
$data — это просто псевдоним для внутреннего свойства _data. (API — Vue.js) Каждый раз, когда мы пишем this.$data, мы получаем this._data.
Что происходит, когда мы пишем this.$data.blablabla? Поскольку на уровне экземпляра Vue нет свойства $data, это свойство ищется в Vue.prototype. Как мы видели выше, свойство $data определено в Vue.prototype через Object.defineProperty. Метод get из Object.defineProperty возвращает this._data. В результате, когда мы пишем this.$data.blablabla, мы получаем this._data.blablabla.
Ссылка на свойство $data в vue api: https://vuejs.org/v2/api/index.html#vm-data
Стоит отметить, что экземпляр vue проксирует доступ к свойствам своего объекта данных. т.е. vm.a прокси для vm.$data.a
Свойство $props. Его поведение похоже на $data. Мы можем получить $props, но не можем установить его.
https://vuejs.org/v2/api/index.html#vm-props
$set — это просто псевдоним глобального метода Vue.set (https://vuejs.org/v2/api/index.html#vm-set) .
$delete — это просто псевдоним глобального метода Vue.delete (https://vuejs.org/v2/api/index.html#vm-delete) .
И последнее свойство — $watch. Это функция, которая позволяет отслеживать изменение переменной и запускать callback после каждого изменения. https://vuejs.org/v2/api/index.html#vm-watch
$watch дает нам альтернативный метод отслеживания реактивной переменной, мы можем использовать не только «традиционный» метод watch компонента vue. Мы можем принудительно запустить this.$watch(….).
Помимо параметров expOrFn и обратного вызова функция $watch может получать и другие полезные параметры. Например, немедленно заставить немедленно запустить обратный вызов в строке кода, где мы определяем vm.$watch.
События Mixin. Миксин добавляет к Vue.prototype несколько функций: Vue.prototype.$on, Vue.prototype.$once, Vue.prototype.$off, Vue.prototype.$emit.
Давайте посмотрим на пример использования пары $on + $emit. Хорошие примеры можно найти в документации: https://vuejs.org/v2/api/index.html#vm-on Первый пример очень простой. Мы создаем подписку на событие в компоненте vue, т. е. устанавливаем, какой обратный вызов будет выполняться при срабатывании определенного события. Это достигается с помощью метода $on. После этого мы запускаем событие test для того же компонента, используя метод $emit. В результате $emit — запускается наш callback.
Стоит отметить, что все работает синхронно. Вызов $emit приводит к синхронному выполнению обратного вызова.
$emit и $on вызываются для одного и того же компонента.
Второй пример аналогичен первому. https://vuejs.org/v2/api/index.html#vm-emit Да, мы используем два компонента вместо одного, но логика та же. Это означает, что мы вызываем $on и $emit только внутри одного компонента. В примере у нас есть родительский компонент и дочерний компонент.
При создании компонента объект _events инициализируется пустым значением (vm._events = {}). Метод $on просто заполняет vm._events обратными вызовами событий. Если мы посмотрим на наш второй пример, vm — это компонент кнопка приветствия. Использование ‹welcome-button v-on:welcome="sayHi"›‹/welcome-button› в шаблоне родительского компонента приводит к следующему:
Метод sayHi из родительского компонента добавляется в vm._events дочернего компонента welcome-button. Мы получаем внутри _events что-то вроде этого {"welcome": [function() {alert('Hi!')}]}.
Метод $emit просто вызывает обратный вызов из vm._events. $emit(“welcome”)находит свойство welcome внутри vm._events и вызывает vm._events[“welcome”]() (т.е. вызывает функцию, на которую ссылается это свойство). Не забывайте, что this внутри метода sayHi указывает на родительский компонент. Объект _events имеет следующую структуру:
Код $on.
Код $emit.
примесь жизненного цикла.
Миксин добавляет к Vue.prototype несколько функций: Vue.prototype.$forceUpdate, Vue.prototype.$destroy. Мы можем прочитать, что они делают, из Vue API. И это место, где также определяется метод mountComponent. Vue.prototype не содержит mountComponent, но Vue.prototype.$mountвызывает mountComponent во время работы. LifecycleMixin также добавляет Vue.prototype._update, место, где происходит обновление DOM. Мы рассмотрим Vue.prototype._update позже.
Давайте посмотрим на mountComponent.
Прежде всего, мы запоминаем, где вызывается метод Vue.prototype.$mount. Он вызывается во время инициализации экземпляра Vue. https://github.com/vuejs/vue/blob/dev/src/core/instance/init.js
У нас уже было изображение, показывающее, что происходит в процессе монтажа компонента (рендеринг компонента). Метод $mount отвечает за процесс монтирования, таким образом, изображение показывает, что на самом деле происходит внутри метода $mount.
У нас может быть две версии фреймворка Vue. Первая версия — это фреймворк, который включает в себя компилятор шаблонов. Вторая версия — это укороченная версия без компилятора. Vue с компилятором может работать с компонентами, у которых есть шаблоны, но эта версия фреймворка имеет большой размер. Vue без компилятора шаблонов имеет меньший вес, но может работать только с компонентами, использующими функции рендеринга.
(Если мы хотим использовать Vue без компилятора с компонентами, которые используют шаблоны — мы можем использовать webpack или аналогичный сборщик для компиляции компонентов. Мы можем использовать результат процесса сборки с версией Vue без компилятора.)
Давайте посмотрим на $mount для укороченной версии Vue: https://github.com/vuejs/vue/blob/dev/src/platforms/web/runtime/index.js
Мы посмотрели mountComponent. В двух словах mountComponent отвечает за процесс монтирования (т.е. первоначальный рендеринг компонента).
Код функции можно найти здесь: https://github.com/vuejs/vue/blob/e20581fb400c36f69fe14ccf316d6001e6a86527/src/core/instance/lifecycle.js#L32
Функция mountComponent импортируется сюда: https://github.com/vuejs/vue/blob/dev/src/platforms/web/runtime/index.js
Несколько слов о методе Vue.prototype._update. Мы подробно проанализируем Vue.prototype._update (а также mountComponent) позже.
Но теперь мы можем описать в общих чертах, что он делает. Метод принимает виртуальный DOM в качестве параметра и обновляет DOM, используя информацию, полученную от виртуального DOM. Это метод, который «перерисовывает веб-страницу», перерисовывает компонент. Виртуальный DOM создается функцией рендеринга (Vue.prototype._render()).
Последний миксин — renderMixin. Миксин добавляет Vue.prototype._render. https://github.com/vuejs/vue/blob/e20581fb400c36f69fe14ccf316d6001e6a86527/src/core/instance/render.js#L69
Процесс инициализации экземпляра Vue.
Давайте начнем анализировать процесс инициализации экземпляра Vue. У нас есть простой пример.
Для наглядности возьмем более сложный пример. Пример с двумя компонентами.
Функция _init вызывается дважды, поскольку у нас есть два компонента. На изображении ниже мы видим взаимосвязь между схемой жизненного цикла компонента и кодом vue. https://github.com/vuejs/vue/blob/dev/src/core/instance/init.js
Вызов initLifecycle добавляет свойства $parent, $root, $children к каждому компоненту (рис. 82). https://github.com/vuejs/vue/blob/e20581fb400c36f69fe14ccf316d6001e6a86527/src/core/instance/lifecycle.js#L32
Стоит отметить, что если у компонента есть родительский компонент (свойство $parent не равно null), он добавляется в массив $children родительского компонента. Мы видим это в initLifecycle (строка 41).
Текущий компонент также имеет массив $children. Этот массив пуст во время инициализации компонента, но если у компонента есть какие-либо дочерние компоненты, каждый дочерний компонент будет помещен в массив $children позже (во время инициализации дочернего компонента).
Кроме того, initLifecycle добавляет несколько полезных свойств, например _isMounted. _isMounted указывает, смонтирован компонент или нет.
Функция initLifecycle говорит нам, что мы можем рассматривать компоненты отдельно от html. Мы можем рассматривать компоненты как дерево объектов javascript (дерево экземпляров Vue, где каждый экземпляр Vue хранит функцию рендеринга в качестве параметра или шаблон в качестве строкового параметра).
Функция initEvents выполняет две функции (рис. 83). Функция создает объект _events для компонента и заполняет этот объект. Рассмотрим дочерний компонент welcome-button из примера. Дочерний компонент имеет объект _events, который после процесса инициализации выглядит так: {welcome: sayHi}. (помните, что пара $on/$emit определена в eventMixin: $on добавляет обратный вызов к _events,$emit вызывает обратный вызов для определенных событий).
Код функции updateComponentListeners (изображение 84).
Есть несколько комментариев по поводу кода updateListeners (изображение 85). https://github.com/vuejs/vue/blob/e20581fb400c36f69fe14ccf316d6001e6a86527/src/core/vdom/helpers/update-listeners.js#L53
Резюмируя вышесказанное. Функция initEvents выполняет две функции. Функция создает объект _events для компонента и заполняет этот объект обратными вызовами, которые применяются к компоненту с помощью шаблона или функции рендеринга.
Мы не будем подробно рассматривать initRender (рис. 86). Он просто добавляет несколько свойств, таких как $listeners, $attr, $scopes. Информацию об этих свойствах можно получить из документации vue. https://github.com/vuejs/vue/blob/e20581fb400c36f69fe14ccf316d6001e6a86527/src/core/instance/render.js#L19
Мы рассмотрим defineReactive позже.
Следующим шагом в процессе инициализации является вызов хука «beforeCreate» (изображение 87).
После этого происходит инициализация впрысков и реактивности (рис. 88). Мы не будем рассматривать инъекции (initInjections/initProvide), потому что они не так важны для понимания внутренностей Vue. (всю необходимую информацию можно найти в документации)
Давайте посмотрим, как реактивность реализована в Vue (изображение 89). Нам нужно понять, что происходит внутри initState. Это то место, где наша самодельная реактивная система может нам помочь.
Давайте вспомним нашу самодельную реактивную систему. Мы выбираем вариант без прокси, потому что Vue 2 не использует прокси в реактивной системе.
https://jsfiddle.net/03m6syeh/
Вспомним основные части нашей самодельной реактивной системы (изображение 91).
Вернитесь к Вью. Давайте возьмем простой пример с одним компонентом вместо предыдущего примера с двумя компонентами.
Перейдите к месту, где данные становятся реактивными.
Загляните внутрь initState: https://github.com/vuejs/vue/blob/e20581fb400c36f69fe14ccf316d6001e6a86527/src/core/instance/state.js#L48
Давайте поговорим об initMethods, прежде чем перейти к initData.
Функция initData.
Функция прокси реализуется с помощью defineProperty (изображение 97).
Продолжим отслеживать, где добавляется реактивность: https://github.com/vuejs/vue/blob/529016bca92f6f098e903b1f77c70d3b0dadefaa/src/core/observer/index.js#L110
Объект класса Observer делает наши данные реактивными. https://github.com/vuejs/vue/blob/529016bca92f6f098e903b1f77c70d3b0dadefaa/src/core/observer/index.js#L31
defineReactive — это функция, которая получает свойства объекта данных одно за другим и делает каждое из них реактивным.
Для defineReactive(obj, «message», «Hello») key равно «message», а val равно «Hello».
Для defineReactive(obj, "message") key равно "message", val= obj["message"]=› val равно «Привет».
Давайте сравним два блока кода для лучшего понимания. Слева — код Vue, справа — код нашей самодельной реактивной системы (изображение 101).
Общий алгоритм: в первую очередь вызывается функция рендеринга компонента в процессе монтирования компонента. Мы можем рассматривать функцию рендеринга компонента как анонимную функцию, переданную функции наблюдателя нашей самодельной системы. К моменту вызова функции рендеринга объект данных является реактивным, и поэтому каждый доступ к некоторым свойствам объекта данных внутри функции рендеринга приводит к вызову геттеров для этих свойств. В нашем примере у нас есть только один доступ к свойству данных внутри функции рендеринга. Это доступ к свойству сообщения. Геттер вызывается во время доступа к свойству «сообщение», поэтому dep.depend() вызывается для соответствующего (свойству «сообщения») объекта Dep. Проще говоря, объект Dep получает текущую функцию рендеринга в собственный внутренний массив. Далее, когда свойство «сообщение» будет изменено, запускается сеттер. Сеттер не только устанавливает новое значение, но и вызывает dep.notify. Вызов dep.notify приводит к вызову всех обратных вызовов, хранящихся внутри внутреннего массива объекта Dep, поэтому также вызывается функция рендеринга. (Внутренний массив объекта Dep может хранить не только функцию рендеринга, но и другие обратные вызовы, например обратные вызовы, добавленные через $watch и т. д.).
В нашем примере объект Dep свойства «message» содержит функцию рендеринга, в то время как соответствующий объект Dep свойства «secondMessage» не содержит ее (поскольку мы не обращались к свойству «secondMessage» внутри функции рендеринга). Если мы запустим наше приложение, мы увидим в консоли, что работает функция рендеринга.
Когда мы изменяем свойство «message», запускается установщик и вызывается dep.notify(), и, поскольку соответствующий объект Dep содержит функцию рендеринга во внутреннем массиве, запускается функция рендеринга.
В противном случае свойство «secondMessage» не содержит функцию рендеринга в соответствующем объекте Dep. Изменение свойства «secondMessage» не вызывает функцию рендеринга, хотя свойство является реактивным.
Мы рассмотрели систему реактивности Vue в общих чертах. Далее мы разберем его более подробно. А пока давайте покажем вам весь код, через который мы прошли:
Давайте посмотрим на реализацию класса Dep во Vue в сравнении с классом Dep нашей самодельной системы. Классы похожи. Код немного упрощен.
Внутри самодельной системной цели находится функция, а this.subscribers = [] — массив обратных вызовов. Что является аналогом массива «подписчики» нашей самопальной системы в классе Dep во Vue? Это массив объектов класса Watcher. В первом приближении мы можем рассматривать объект класса Watcher как функцию, поскольку этот объект хранит в собственном внутреннем свойстве callback-функцию, например — функцию рендеринга. Можно считать, что вызов метода update объекта класса Watcher (класс Watcher содержит метод update) приводит к вызову функции (обратного вызова), хранящейся внутри этого объекта.
Внутри метода $mouth создается объект «Watcher» (объект класса Watcher), хранящий функцию рендеринга.
На изображении 108 показано, как самодельная система соотносится с кодом Vue’s Watcher.
Объект Watcher принимает функцию в качестве параметра expOrFn (в конструкторе) во время инициализации. Точно так же в самодельной системе функция watcher принимает аргумент myFunc (аргумент myFunc был анонимной функцией для вычисления итога в старом примере). ).
Функция принимается в параметре expOrFn и сохраняется в свойстве getter объекта Watcher. После этого функция сразу вызывается через метод get. Метод get аналогичен функции watcher (из самодельной системы). Метод get устанавливает текущий объект Watcher в глобальную Dep.target с помощью pushTarget(this). Затем get вызывает код, используя this.getter.call. После этого get вызывает popTarget, который удаляет объект Watcher из Dep.target.
Все реактивные свойства, к которым обращаются внутри this.getter.call — получают текущий объект Watcher в соответствующие им объекты Dep. Причина, по которой они получают текущий объект Watcher в свои соответствующие объекты Dep, заключается в том, что во время вызова this.getter.call текущий объект Watcher находится в «глобальном» Dep.target. При изменении любого свойства из этих свойств, к которым ранее осуществлялся доступ внутри this.getter.call, запускается метод notify для соответствующего объекта Dep. Метод уведомления вызывает метод обновления для каждого объекта Watcher, который объект Dep хранит в свойстве this.subs.
Теперь для простоты мы можем рассматривать функцию update как функцию, которая просто вызывает функцию run. Функция run вызывает метод get и, следовательно, вызывает код из this.getter (this.getter.call(vm, vm)).
Метод addDep получает в качестве параметра объект Dep, добавляет этот объект в текущий объект Watcher (в массив newDeps) и, что важно, добавляет текущего Watcher в объект Dep через вызов dep.addSub(this).
Напомним, что mountComponent вызывается через $mount(изображение 109).
Где создается объект Watcher, содержащий функцию рендеринга? Он создается внутри функции mountComponent.
Здесь (изображение 110) отчетливо видно, что new Watcher(…)принимает функцию в качестве параметра (updateComponent), и это похоже на передачу анонимной функции в наблюдатель в самодельной системе. updateComponent — это функция, которая вызывает функцию рендеринга, а затем вызывает функцию, которая «рисует» html — функцию _update. Для удобства мы будем называть этот Watcher «рендеринговым» Watcher.
Вернемся к нашему примеру:
Функция mountComponent вызывается внутри $mount (изображение 113). В первую очередь вызывается хук «beforeMount», после этого монтируется компонент, а затем вызывается хук «mounted». Монтирование компонента происходит в момент создания объекта «Watcher» — нового Watcher(vm, updateComponent, noop, …), который вызывает updateComponent при инстанцировании. Функция updateComponent монтирует компонент.
Давайте рассмотрим updateComponent (изображение 114). Мы знаем, что он состоит из последовательности вызовов функций: _render и _update. Функция _update принимает виртуальный DOM в качестве параметра и занимается фактическим монтированием компонента.
Функция _update: https://github.com/vuejs/vue/blob/dev/src/core/instance/lifecycle.js#L59
Функция __patch__ — это функция, которая изменяет DOM с помощью виртуального DOM (VDOM). Это зависит от окружающей среды. Для браузеров, для web будет вызываться такой код (изображение 116).
Функция __patch__ на самом деле является вызовом createPatchFunction с параметром nodeOps (второй параметр не важен для понимания).
createPatchFunction определена в библиотеке vdom, которая не имеет отношения к Vue.
Мы не будем изучать, как работает эта библиотека vdom, но учтем, что поведение функции createPatchFunction определяется параметром nodeOps, который передается . nodeOps — это набор всех функций, которые необходимы библиотеке vdom для взаимодействия с реальным DOM. Пример таких функций:
Здесь мы видим разделение между виртуальным DOM и операциями, необходимыми для управления реальным DOM. Например, мы можем добавить операции для управления реальным DOM для другой платформы (например, Android), а не для Интернета. В этом случае функция для обеих платформ будет одинаковой, а именно createPatchFunction, но параметр nodeOps будет разным для разных платформ.
Теперь диаграмма реактивности с сайта Vue docs должна быть понятна. Компонент создается через «новый Vue (..)». Данные делаются реактивными.
Создается «рендер»-Watcher, внутри него в момент инициализации «нового Watcher» вызывается функция рендеринга компонента (изображение 121).
Внутри функции рендеринга геттеры запускаются для свойств реактивного объекта data, к которому запрашивается доступ (изображение 122).
Функция рендеринга вызывается с помощью new Watcher (….). То есть в конструкторе класса Watcher, используя метод get этого класса. Это означает, что Dep.target! = null во время выполнения функции рендеринга, и все свойства реактивного объекта data, к которым осуществляется доступ в рамках этой функции рендеринга, получают «рендер»-наблюдатель от Dep.target (большой синий кружок на схеме) в соответствующих им (соответствующих) объектах отд. По сути, они получают объект Watcher, содержащий внутри код функции рендеринга.
И, в дальнейшем, при срабатывании сеттера для свойства будет срабатывать соответствующий dep.notify, что приведет к вызову метода update для всех объектов Watcher, хранящихся в объекте dep . В простейшем случае есть один наблюдатель, в котором хранится код функции рендеринга:
Метод update Watcher будет вызывать (пока мы рассматриваем самую простую версию) метод run, метод run будет вызывать метод get. И в результате снова будет вызвана функция рендеринга.
Мы рассмотрели схему жизненного цикла компонента вплоть до процесса его монтажа. Мы описали, что происходит при изменении реактивных свойств объекта данных. Важно, что мы рассмотрели вариант с «упрощенным» Watcher — Watcher в синхронном режиме, просто потому, что в начале нам было проще понять код. Повторим, что происходит в смонтированном компоненте при изменении свойства message (рассматриваем ситуацию с Watcher в синхронном режиме: sync = true).
Давайте изменим свойство message.
Вызывается функция рендеринга и происходит обновление «реального» DOM и виртуального DOM.
Мы охватили почти весь жизненный цикл компонента (за исключением уничтожения компонента). Но, во-первых, мы не видели, где вызываются хуки «beforeUpdate» и «updated». А во-вторых, у нас было предположение, что текущий объект Watcher является синхронным. Но чаще объект Watcher (особенно объект «render»-Watcher) является асинхронным. Что это означает? Мы расскажем об этом дальше.
Рассмотрим пример. После нажатия кнопки вызывается обратный вызов change. Обратный вызов увеличивает реактивное свойство с именем «counter» на 1 и делает это 200 раз, используя цикл. В соответствии с нашей схемой обновления компонента, которую мы показали выше, такое обновление в цикле должно привести к вызову функции рендеринга 200 раз.
Давайте проверим это. Когда мы запустим пример, мы увидим, что функция рендеринга будет запущена только один раз:
Когда мы нажимаем кнопку, мы видим, что функция рендеринга запускается только один раз (а не 200 раз).
Мы можем изменить поведение примера, чтобы оно соответствовало нашей «упрощенной» схеме обновления компонента, которую мы показали выше. Такое обновление в цикле должно привести к вызову функции рендеринга 200 раз. Для этого достаточно сделать так, чтобы Watcher «отрисовывался» синхронно:
Очевидно, что вариант с асинхронным обновлением компонента (при изменении реактивного свойства) имеет лучшую производительность, чем вариант с синхронным обновлением, поскольку нет необходимости многократно (200) раз запускать функцию рендеринга. Асинхронное обновление достигается либо с помощью setImmediate, либо с помощью promises, либо с помощью setTimeout(func, 0) (использование setTimeout — наихудший случай, когда другие параметры недоступны).
Мы рассмотрим это более подробно далее, но основная идея проста — мы асинхронно вызываем функцию рендеринга (либо через setImmediate, либо через promises, либо через setTimeout(func, 0))
Рассмотрим пример — изображение 135 (https://jsfiddle.net/xf0j7p5s/1/):
В примере у нас есть два компонента, дочерний и родительский. Дочерний компонент имеет два свойства в объекте data: a и b.
Оба свойства доступны в шаблоне дочернего компонента и, следовательно, в функции рендеринга компонента. Родительский компонент имеет одно свойство c. Это свойство также доступно в шаблоне (шаблоне родительского компонента). Дочерний компонент имеет кнопку, которая изменяет значения свойств a и b по клику. Эта кнопка также создает событие с именем «childemit». Событие приводит к вызову родительского метода с именем parentMethod, который изменяет свойство c.
Шаг за шагом: нажимаем кнопку, вызывается метод дочернего компонента с именем clickButton
Вызываются установщики свойств a и b. Методы notify вызываются в соответствующих объектах Dep свойств a и b. Вызывается метод update каждого объекта Watcher «рендеринга» (каждый объект Watcher «рендеринга» хранится внутри каждого объекта Dep, причем объекты Dep обоих свойств a и b хранит тот же «рендеринг» Watcher, который содержит функцию рендеринга дочернего компонента). Затем происходит вызов this.$emit(‘childemit’), а затем происходит вызов метода родительского компонента, подписанного на это событие (т. е. вызов parentMethod).
В родительском компоненте изменилось свойство c, и для него произошел процесс, аналогичный процессам, происходившим со свойствами a и b ( единственное, что функция рендеринга является функцией рендеринга родительского компонента). Все эти вызовы, включая создание события, запуск обработчика событий и вызов методов обновления, происходят синхронно.
Асинхронность появляется только внутри метода обновления.
Если метод update является асинхронным, this.sync = false и вызывается функция queueWatcher.
Когда мы нажимаем кнопку — происходят два синхронных вызова функции queueWatcher на одном и том же «render»-Watcher дочернего компонента (это означает, что в функцию queueWatcher передается один и тот же объект Watcher).
И один синхронный вызов queueWatcher происходит на «рендеринге» Watcher родительского компонента.
Посмотрим, как работает функция queueWatcher: https://github.com/vuejs/vue/blob/dev/src/core/observer/scheduler.js
Основная идея — заполнить массив queue объектами класса Watcher. При выполнении данного кода this.a = 111 — Watcher «рендеринга» дочернего компонента передается в качестве аргумента в queueWatcher.
Когда данный код this.b = 222 запускается, Watcher «рендеринга» дочернего компонента второй раз передается функции queueWatcher (изображение 142).
Затем выполняется следующий код: this.$emit(“childemit”) =› parentMethod =› this.c = 333.
Наблюдатель «рендеринга» родительского компонента передается в качестве аргумента функции queueWatcher. (изображение 143)
В результате мы добавили в queue два наших объекта Watcher для рендеринга. Первый в очереди — это наблюдатель рендеринга дочернего компонента, второй — наблюдатель рендеринга родительского компонента. Как только все синхронное закончит свою работу, т.е. освободится стек вызовов функций, произойдет асинхронный вызов flushSchedulerQueue. (есть интересное видео про стек вызовов функций и цикл событий — https://youtu.be/8aGhZQkoFbQ?t=1)
Вторая часть flushSchedulerQueue:
Функция, которая вызывает «обновленный» хук:
Функция, очищающая очередь. Функция также устанавливает waiting в false:
Нам осталось посмотреть, как происходит асинхронный вызов функции flushSchedulerQueue. Мы уже выяснили, что это происходит через вызов функции nextTick, давайте посмотрим на эту функцию. https://github.com/vuejs/vue/blob/dev/src/core/util/next-tick.js
Способы, которыми nextTick запускает timerFunc (vue/src/core/util/next-tick.js):
Первый:
Второй:
Третий (лучший способ):
Еще немного о методе $nextTick — он добавляется в класс Vue с помощью миксина renderMixin.
Наконец, вот пример использования $nextTick:
Мы что-то изменили, например реактивное свойство, но настоящий DOM сразу не изменился, и console.log возвращает старое значение.
Теперь понятно, что это связано с асинхронным обновлением DOM.
Нам нужен $nextTick, чтобы увидеть измененное значение.
Любопытное замечание. Предположим, у нас есть такой шаблон
‹div v-if="prop1"›‹p›{{reactiveText}}‹/p›‹/div›
Когда он скомпилирован в функцию рендеринга, то v-if превратится в стандартный if в Javascript, он просто будет запускаться внутри функции рендеринга.
Тогда, если при первом вызове функции рендеринга свойство prop1 было false, то мы не попадем в блок if внутри функции рендеринга. А именно в нем в блоке if мы ссылаемся на свойство reactiveText.
Так как мы не получаем доступ к свойству reactiveText, то геттер этого свойства не работает, поэтому, как бы мы дальше не меняли reactiveText, при значении prop1 равном false он не вызовет повторного рендеринга.
Еще о реактивности.
Давайте подробнее рассмотрим реактивность в Vue. Первый пример. https://jsfiddle.net/rw1ckoej/
Компонент использует функцию рендеринга.
Шаблон пустой (в комментариях показан шаблон, аналогичный функции рендеринга компонента).
Когда мы запускаем пример, создается и монтируется глобальный компонент. Поэтому функция рендеринга запускается один раз и мы видим сообщение в консоли:
Изменим наш код так: https://jsfiddle.net/nzwmh0a4/1/
Мы не используем реактивное свойство message в нашей функции рендеринга. Поэтому геттер переменной не срабатывает и реактивность не работает. Функция рендеринга не запускается нажатием кнопки.
Мы видим, что обратный вызов кнопки вызывается, но функция рендеринга не вызывается.
Давайте посмотрим пример с использованием шаблона. Ссылка https://jsfiddle.net/1yLznws8/
Мы не можем ввести в шаблон функцию console.log (как мы делали раньше с функцией рендеринга), но понятно, что рендеринг происходит при вызове change2:
Тот же пример с использованием функции рендеринга: https://jsfiddle.net/jf47oL38/
Похоже, что JSON.stringify (строка 22 в коде jsfiddle) перебирает все свойства объекта, поэтому срабатывают геттеры всех свойств объекта (соответственно, независимо от того, какое свойство объекта objProp объект, который мы меняем, функция рендеринга все равно будет работать).
Есть и другие важные примеры. Каждый из них полезен для понимания реактивности:
https://jsfiddle.net/dn0ky4bu/
В функции рендеринга нет свойства message, поэтому щелчок по кнопке «change1» не вызывает функцию рендеринга.
https://jsfiddle.net/ydcfxrqe/ Мы изменили функцию рендеринга, поэтому нажатие кнопок change1 и change3 не вызывает функция рендеринга (change2 изменяет objProp.outerProp, доступный в функции рендеринга):
https://jsfiddle.net/mrg4b8qp/1/ Интересный пример — мы необычным образом изменили функцию рендеринга.
Хотя переменная this.objProp.nestedProp.value не отображается в HTML-коде компонента, доступ к ней осуществляется внутри функции рендеринга. Поэтому срабатывает метод получения this.objProp.nestedProp.value.
Есть и другие важные примеры:
https://jsfiddle.net/jq5ny1af/ — стоит отметить, что мы меняем по нажатию на кнопки внутренности объекта objProp, а не ссылку на него.
Поэтому не происходит вызовов функции рендеринга при нажатии на кнопки:
https://jsfiddle.net/ehvtsfp2/ — здесь, если мы сначала нажмем change2, а затем change3 — оба клика вызовут повторный рендеринг.
Но если мы изменим порядок кликов — произойдет только один вызов функции рендеринга.
Мы рассмотрели примеры. Давайте более подробно рассмотрим реактивность.
Например, у нас есть такой объект данных:
Что происходит при создании компонента?
Этот объект data передается в функцию наблюдения (для простоты мы наблюдаем упрощенный код Vue):
После этого объект data передается в конструктор «new Observe(data)».
Во время вызова конструктора вызывается метод walk, принимающий данные. Вызов walk приводит к вызову defineProperty для каждого свойства объекта данных.
Давайте рассмотрим, что делает метод defineReactive.
Посмотрим, как реализована подстановка методов в массиве.
Во-первых, у нас есть объект arrayMethods. Это объект, который содержит олицетворенные методы (push, splice и т. д.).
Есть два варианта использования этих олицетворенных методов целевым массивом. Первый использует функцию protoAugment:
Массив получает «подстановку» в цепочке прототипов. Вместо ближайшего прототипа Array.prototype с «оригинальными» методами push, pop и т. д. он получает объект arrayMethods с видоизмененными методами push, pop, splice, slice.
Второй использует функцию copyAugment: она просто копирует методы из arrayMethods в целевой массив.
Решение о том, какую функцию использовать, принимается с помощью следующей переменной:
Давайте посмотрим, как создаются олицетворяемые методы в объекте arrayMethods.
Вернемся к нашему примеру, у нас есть объект data:
Как мы сказали выше, объект data передается в функцию observe:
Затем объект data передается в метод new Observe и метод walk называется:
Каждое свойство объекта данных передается функции defineReactive:
Давайте пройдемся по каждому свойству объекта данных.
Свойство outerProp (outerProp: 100):
Подводя итог, зачем нам нужны строки кода 1 и 4.
Со свойством objectProp (objectProp: {innerProp: «Hello»}) все то же самое.
Свойства arrayProp ([1, 5, 8, 10]) и arrayObjectsProp ([ {prop: 5}, …, {prop: 9} ]):
Стоит отметить, что для каждого элемента из массива не создается соответствующий объект Dep. Следовательно
Vue не может обнаружить следующие изменения в массиве:
1) Когда вы напрямую устанавливаете элемент с индексом
2) При изменении длины массива
https://vuejs.org/v2/guide/reactivity.html#For-Arrays
Почему это сделано именно так во Vue? Потому что массив обычно хранит много данных, и ситуация, когда каждому элементу массива соответствует свой объект Dep, приводит к снижению производительности. Поэтому каждый массив во Vue имеет только несколько объектов Dep для модифицированных методов push, pop и т.д., а каждый элемент массива, если этот элемент не является примитивом — имеет собственные объекты Dep (например, объект может иметь объекты Dep для своих вложенных характеристики).
Давайте посмотрим, что произошло с arrayProp([1, 5, 8, 10])и arrayObjectsProp([ {prop: 5} , … , {prop: 9} ]) свойства. Прежде всего — в конструктор объекта Observer передается полный объект данных. Вызывается функция walk,
Параметр «key» по очереди принимает следующие значения: «outerProp», «objectProp», «arrayProp», «arrayObjectsProp».
Функция defineReactive вызывается для каждого свойства исходного объекта data.
Функция defineReactive вызывается для arrayProp([1, 5, 8, 10])и arrayObjectsProp([ {prop: 5} , … , {реквизит: 9} ])
Класс Observer, параметр data в конструкторе — это либо arrayProp, либо arrayObjectsProp:
Не забывайте, что мы используем упрощенную версию кода, справа — фактический код VUE (не большая разница)
Что происходит, когда мы получаем массив (arrayProp или arrayObjectsProp объекта data) в функции рендеринга?
Во время первоначального вызова функции рендеринга наш объект данных уже реактивен.
Если мы обращаемся к реактивному свойству, которое является массивом (arrayProp или arrayObjectsProp) в функции рендеринга, вызывается метод get.
Резюмируя вышесказанное: для вызова функции рендеринга нам нужно изменить «полную переменную», например arrayProp , или если массив содержит объект в качестве элемента — нам нужно изменить свойство этого объекта, или если массив содержит вложенный массив — нам нужно вызвать олицетворенный метод вложенного массива.
Стоит отметить, что если у нас есть объект в массиве и мы обращаемся к свойству этого объекта в функции рендеринга, поэтому срабатывает геттер свойства, и соответствующий этому свойству объект Dep получает функцию рендеринга.
Вот несколько примеров с массивами:
https://jsfiddle.net/60w3tqb5/
https://jsfiddle.net/ze8xfm01/
Иван Голубев