предисловие
использоватьVue
Я работаю над проектом уже два года, да.Vue
изapi
Так же удобнее в использовании, хотя даVue
Некоторые принципы реализации, такие как виртуальныйDOM
,flow
, data-driven, принципы маршрутизации и т. д., но я намеренно не исследовал основы этих принципов иVue
Как исходный код использует эти принципы для реализации фреймворка, так что используйте свободное время для выполненияVue
Технические принципы иVue
Завершение бетонной реализации каркаса. если ты правVue
Если вам интересен принцип реализации, то вы можете начать читать этот цикл статей, который откроетVue
Базовые мировые врата , и более подробно рассмотрим детали их реализации. Эта статьяVirtual DOM
технические принципы иVue
Конкретная реализация фреймворка.
Я усердно работал над написанием в течение долгого времени, и я надеюсь вручную поставить лайк и поощрить~
Адрес гитхаба:GitHub.com/ревматизм123/…, который обобщает все статьи в блоге автора, если вам нравится или вдохновляет, пожалуйста, помогите дать звезду ~, что также является поощрением для автора.
1. РеальныйDOM
и процесс его разбора
В этом разделе мы в основном представляем реальныйDOM
Процесс синтаксического анализа, представляя свой процесс синтаксического анализа и существующие проблемы, приводит к тому, почему виртуальныеDOM
. Картинка стоит тысячи слов, как показано нижеwebkit
Рабочий процесс движка рендеринга
Все рабочие процессы браузерного движка рендеринга примерно разделены на 5 шагов: созданиеDOM
дерево —> создатьStyle Rules
-> построитьRender
дерево —> макетLayout
--> рисоватьPainting
.
- Первым шагом является построение дерева DOM: используйте анализатор HTML для анализа элементов HTML и построения дерева DOM;
- Второй шаг — создать таблицу стилей: используйте анализатор CSS для анализа встроенных стилей в файлах и элементах CSS и создайте таблицу стилей для страницы;
- Третий шаг, построение дерева рендеринга: Свяжите дерево DOM с таблицей стилей, чтобы построить дерево рендеринга (вложение). У каждого узла DOM есть метод attach, который принимает информацию о стиле и возвращает объект рендеринга (также известный как средство рендеринга), который в конечном итоге будет сконструирован в дереве рендеринга;
- На четвертом этапе определите координаты узла: в соответствии со структурой дерева рендеринга определите точную координату, появляющуюся на экране дисплея, для каждого узла в дереве рендеринга;
- Пятый шаг, отрисовка страницы: отобразите координаты в соответствии с деревом рендеринга и узлами, а затем вызовите метод рисования каждого узла, чтобы отрисовать их.
будь осторожен:
1,DOM
Построение дерева начинается при загрузке документа?ПостроитьDOM
Дерево представляет собой постепенный процесс, чтобы добиться лучшего взаимодействия с пользователем, механизм рендеринга будет отображать содержимое на экране как можно скорее, ему не нужно ждать, пока весьHTML
Сборка после завершения синтаксического анализа документаrender
Дерево и макет.
2,Render
деревоDOM
дерево иCSS
Начинается ли сборка после создания таблицы стилей?Эти три процесса не являются полностью независимыми, когда они происходят на самом деле, но будут пересечения, которые будут загружаться, анализироваться и отображаться одновременно.
3.CSS
Анализ точек внимания? CSS
Синтаксический анализ выполняется в обратном порядке справа налево, чем больше вложенных тегов, тем медленнее парсинг.
4.JS
действующий реальныйDOM
Цена?Используя нашу традиционную модель разработки, нативныеJS
илиJQ
действоватьDOM
, браузер выполнит процесс от начала до конца, начиная с построения DOM-дерева. За одну операцию мне нужно обновить 10DOM
узел, браузер получает первыйDOM
После запроса он не знает, что есть 9 операций обновления, поэтому процесс будет выполнен сразу, и в итоге будет выполнено 10 раз. Например, после первого расчета следующийDOM
Когда делается запрос на обновление, значение координаты этого узла изменяется, и предыдущий расчет бесполезен. рассчитатьDOM
Значения координат узлов и т. д. - все это потраченная впустую производительность. Несмотря на то, что компьютерное оборудование неоднократно обновлялось, операционнаяDOM
Цена по-прежнему высока, а частые операции по-прежнему будут вызывать застревание страниц, что повлияет на работу пользователей.
два,Virtual-DOM
Основание
2.1 ВиртуальныйDOM
преимущества
виртуальныйDOM
Он предназначен для решения проблем с производительностью браузера. Как и раньше, если в одной операции 10 обновленийDOM
действие, виртуальныйDOM
сразу не получитсяDOM
, вместо 10 обновленийdiff
Сохраните содержимое в локальномJS
объект, и в конечном итоге этоJS
объект одноразовыйattch
прибытьDOM
На дереве выполняются последующие операции, чтобы избежать большого количества ненужных вычислений. Итак, используйтеJS
моделирование объектовDOM
Преимущество узла в том, что обновление страницы можно полностью отразить в первую очередь.JS
объект (виртуальныйDOM
), манипулировать в памятиJS
Скорость объекта явно выше, а после завершения обновления окончательныйJS
Объекты сопоставляются с реальнымиDOM
, переданный браузеру для рисования.
2.2 Реализация алгоритма
2.2.1, используйтеJS
моделирование объектовDOM
Дерево
(1) Как использоватьJS
моделирование объектовDOM
Дерево
например настоящийDOM
Узлы следующие:
<div id="virtual-dom">
<p>Virtual DOM</p>
<ul id="list">
<li class="item">Item 1</li>
<li class="item">Item 2</li>
<li class="item">Item 3</li>
</ul>
<div>Hello World</div>
</div>
мы используемJavaScript
объект для представленияDOM
Node, используйте свойства объекта для записи типа, свойств, дочерних узлов и т. д. узла.
element.js
Код объекта узла, представленного в середине, выглядит следующим образом:
/**
* Element virdual-dom 对象定义
* @param {String} tagName - dom 元素名称
* @param {Object} props - dom 属性
* @param {Array<Element|String>} - 子节点
*/
function Element(tagName, props, children) {
this.tagName = tagName
this.props = props
this.children = children
// dom 元素的 key 值,用作唯一标识符
if(props.key){
this.key = props.key
}
var count = 0
children.forEach(function (child, i) {
if (child instanceof Element) {
count += child.count
} else {
children[i] = '' + child
}
count++
})
// 子元素个数
this.count = count
}
function createElement(tagName, props, children){
return new Element(tagName, props, children);
}
module.exports = createElement;
согласно сelement
установка объекта, вышеDOM
Структура может быть просто выражена как:
var el = require("./element.js");
var ul = el('div',{id:'virtual-dom'},[
el('p',{},['Virtual DOM']),
el('ul', { id: 'list' }, [
el('li', { class: 'item' }, ['Item 1']),
el('li', { class: 'item' }, ['Item 2']),
el('li', { class: 'item' }, ['Item 3'])
]),
el('div',{},['Hello World'])
])
Сейчасul
что мы используемJavaScript
объект представленDOM
структура, мы выводим представлениеul
Соответствующая структура данных выглядит следующим образом:
(2) Для рендерингаJS
указаноDOM
объект
Но такой структуры на странице нет.Далее мы представим, какul
визуализировать как реальный на страницеDOM
Структура, связанные функции рендеринга следующие:
/**
* render 将virdual-dom 对象渲染为实际 DOM 元素
*/
Element.prototype.render = function () {
var el = document.createElement(this.tagName)
var props = this.props
// 设置节点的DOM属性
for (var propName in props) {
var propValue = props[propName]
el.setAttribute(propName, propValue)
}
var children = this.children || []
children.forEach(function (child) {
var childEl = (child instanceof Element)
? child.render() // 如果子节点也是虚拟DOM,递归构建DOM节点
: document.createTextNode(child) // 如果字符串,只构建文本节点
el.appendChild(childEl)
})
return el
}
Глядя на вышеизложенноеrender
метод, согласноtagName
построить настоящийDOM
узел, затем задайте свойства этого узла и, наконец, рекурсивно создайте свои собственные дочерние узлы.
мы построимDOM
Добавить структуру на страницуbody
выше, следующим образом:
ulRoot = ul.render();
document.body.appendChild(ulRoot);
Таким образом, страницаbody
есть настоящийDOM
структура, эффект показан на следующем рисунке:
2.2.2 Сравните два виртуальных дереваDOM
разница деревьев —diff
алгоритм
diff
Алгоритм сравнения двух деревьевVirtual DOM
Разность деревьев, если требуется полное сравнение двух деревьев, тоdiff
Временная сложность алгоритмаO(n^3)
. Но во внешнем интерфейсе вы редко перемещаетесь по слоямDOM
элемент, поэтомуVirtual DOM
Будут сравниваться только элементы одного уровня, как показано на следующем рисунке.div
только на том же уровнеdiv
Напротив, второй уровень будет сравниваться только со вторым уровнем, так что сложность алгоритма может быть достигнута.O(n)
.
(1) Обход в глубину, запись различий
В реальном коде выполняется обход в глубину старого и нового деревьев, так что каждый узел будет иметь уникальный тег:
При обходе в глубину каждый раз при обходе узла узел сравнивается с новым деревом. Если есть разница, она записывается в объект.
// diff 函数,对比两棵树
function diff(oldTree, newTree) {
var index = 0 // 当前节点的标志
var patches = {} // 用来记录每个节点差异的对象
dfsWalk(oldTree, newTree, index, patches)
return patches
}
// 对两棵树进行深度优先遍历
function dfsWalk(oldNode, newNode, index, patches) {
var currentPatch = []
if (typeof (oldNode) === "string" && typeof (newNode) === "string") {
// 文本内容改变
if (newNode !== oldNode) {
currentPatch.push({ type: patch.TEXT, content: newNode })
}
} else if (newNode!=null && oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
// 节点相同,比较属性
var propsPatches = diffProps(oldNode, newNode)
if (propsPatches) {
currentPatch.push({ type: patch.PROPS, props: propsPatches })
}
// 比较子节点,如果子节点有'ignore'属性,则不需要比较
if (!isIgnoreChildren(newNode)) {
diffChildren(
oldNode.children,
newNode.children,
index,
patches,
currentPatch
)
}
} else if(newNode !== null){
// 新节点和旧节点不同,用 replace 替换
currentPatch.push({ type: patch.REPLACE, node: newNode })
}
if (currentPatch.length) {
patches[index] = currentPatch
}
}
Из вышеизложенного можно сделать вывод, чтоpatches[1]
выражатьp
,patches[3]
выражатьul
, и так далее.
(2) Тип разницы
DOM
К видам расхождений, вызванных операциями, относятся следующие:
- Замена узла: Меняется узел, например, указанный выше
div
заменитьh1
; - Обмен последовательностями: перемещение, удаление, добавление дочерних узлов, как указано выше.
div
дочерний узел, поставитьp
а такжеul
обмен заказами; - Изменение атрибута: изменены атрибуты узла, например добавление вышеуказанного
li
изclass
удаление класса стиля; - Изменение текста: измените текстовое содержимое текстового узла, например, как указано выше.
p
Изменить текстовое содержимое узла is "Real Dom
"
Некоторые из описанных выше типов различий определены в коде следующим образом:
var REPLACE = 0 // 替换原先的节点
var REORDER = 1 // 重新排序
var PROPS = 2 // 修改了节点的属性
var TEXT = 3 // 文本内容改变
(3) Алгоритм сравнения списка
Алгоритм сравнения дочерних узлов, напримерp, ul, div
изменил порядокdiv, p, ul
. Как это следует сравнивать? Если их сравнивать по порядку одного уровня, то все они будут заменены. Такие какp
а такжеdiv
изtagName
разные,p
Будетdiv
заменены. В конце концов, все три узла будут заменены, так чтоDOM
Стоимость очень большая. На самом деле нет необходимости заменять узел, а нужно только перемещаться по узлу, чтобы достичь, нам нужно только знать, как двигаться.
Абстрагирование этой проблемы на самом деле является проблемой минимального расстояния редактирования строк (Edition Distance
), наиболее распространенным обходным решением являетсяLevenshtein Distance
, Levenshtein Distance
представляет собой строку метрики, которая измеряет разницу между двумя последовательностями символов, разницей между двумя словамиLevenshtein Distance
минимальное количество односимвольных правок (вставок, удалений или замен), необходимых для преобразования одного слова в другое.Levenshtein Distance
Он был изобретен в 1965 году советским математиком Владимиром Левенштейном.Levenshtein Distance
Также известно как расстояние редактирования (Edit Distance
),пройти черездинамическое программированиеРешите, временная сложностьO(M*N)
.
Определение: для двух строкa、b
, то ихLevenshtein Distance
для:
Пример: строкаa
а такжеb
,a=“abcde” ,b=“cabef”
, согласно формуле расчета, приведенной выше, ихLevenshtein Distance
Процесс расчета выглядит следующим образом:
этой статьиdemo
Используйте плагиныlist-diff2
Алгоритмы сравниваются, временная сложность алгоритма великаO(n*m)
, хотя этот алгоритм не является оптимальным, он используется дляdom
Достаточно обычных манипуляций с элементами. Конкретный процесс реализации алгоритма здесь подробно описываться не будет.GitHub.com/Ли ВО РАН/История…
(4) Примеры вывода
два виртуальныхDOM
Объект показан на рисунке ниже, гдеul1
представляет собой исходный виртуальныйDOM
Дерево,ul2
представляет измененный виртуальныйDOM
Дерево
var ul1 = el('div',{id:'virtual-dom'},[
el('p',{},['Virtual DOM']),
el('ul', { id: 'list' }, [
el('li', { class: 'item' }, ['Item 1']),
el('li', { class: 'item' }, ['Item 2']),
el('li', { class: 'item' }, ['Item 3'])
]),
el('div',{},['Hello World'])
])
var ul2 = el('div',{id:'virtual-dom'},[
el('p',{},['Virtual DOM']),
el('ul', { id: 'list' }, [
el('li', { class: 'item' }, ['Item 21']),
el('li', { class: 'item' }, ['Item 23'])
]),
el('p',{},['Hello World'])
])
var patches = diff(ul1,ul2);
console.log('patches:',patches);
Смотрим на вывод двух манекеновDOM
Объект различия между объектами показан на рисунке ниже, мы можем получить его через объект различия, два виртуальныхDOM
Какие изменения были сделаны между объектами и, таким образом, в соответствии с этим объектом различия (patches
), чтобы изменить исходное реальноеDOM
структуру, чтобы страницаDOM
изменения структуры.
2.2.3, два виртуальныхDOM
Различия, применяемые к реальному объектуDOM
Дерево
(1) Обход в глубинуDOM
Дерево
Поскольку шаг 1 создаетJavaScript
дерево объектов иrender
выходи настоящимDOM
树的信息、结构是一样的。 Так что мы можемDOM
Также при обходе дерева в глубину обход, сгенерированный из двух шаговpatches
Найдите отличие текущего пройденного узла в объекте, как показано в следующем связанном коде:
function patch (node, patches) {
var walker = {index: 0}
dfsWalk(node, walker, patches)
}
function dfsWalk (node, walker, patches) {
// 从patches拿出当前节点的差异
var currentPatches = patches[walker.index]
var len = node.childNodes
? node.childNodes.length
: 0
// 深度遍历子节点
for (var i = 0; i < len; i++) {
var child = node.childNodes[i]
walker.index++
dfsWalk(child, walker, patches)
}
// 对当前节点进行DOM操作
if (currentPatches) {
applyPatches(node, currentPatches)
}
}
(2) Для оригиналаDOM
дерево нестиDOM
действовать
Мы выполняем различную обработку на текущем узле в соответствии с различными типами различий.DOM
Действия, например замена узла, если это сделаноDOM
Операция; если текст узла изменился, текст заменяетсяDOM
операций, а также перегруппировку дочерних узлов, изменение атрибутов и т. д.DOM
операция, связанный код, такой какapplyPatches
показано:
function applyPatches (node, currentPatches) {
currentPatches.forEach(currentPatch => {
switch (currentPatch.type) {
case REPLACE:
var newNode = (typeof currentPatch.node === 'string')
? document.createTextNode(currentPatch.node)
: currentPatch.node.render()
node.parentNode.replaceChild(newNode, node)
break
case REORDER:
reorderChildren(node, currentPatch.moves)
break
case PROPS:
setProps(node, currentPatch.props)
break
case TEXT:
node.textContent = currentPatch.content
break
default:
throw new Error('Unknown patch type ' + currentPatch.type)
}
})
}
(3) Изменения структуры DOM
Объединив два полученных в разделе 2.2.2DOM
Разница между объектами, примененная к первому (оригинальному)DOM
структура, мы можем видетьDOM
Структура изменится, как и ожидалось, как показано на следующем изображении:
2.3 Заключение
Соответствующая реализация кода размещена на github. Заинтересованные студенты могут клонировать для запуска эксперимента. Адрес github:GitHub.com/ревматизм123/…
Virtual DOM
Алгоритм в основном реализует три вышеуказанных шага для достижения:
-
использовать
JS
моделирование объектовDOM
Дерево -element.js
<div id="virtual-dom"> <p>Virtual DOM</p> <ul id="list"> <li class="item">Item 1</li> <li class="item">Item 2</li> <li class="item">Item 3</li> </ul> <div>Hello World</div> </div>
-
Сравните два виртуальных дерева
DOM
разница деревьев —diff.js
-
два виртуальных
DOM
Отличие объекта применяется к реальномуDOM
Дерево -patch.js
function applyPatches (node, currentPatches) { currentPatches.forEach(currentPatch => { switch (currentPatch.type) { case REPLACE: var newNode = (typeof currentPatch.node === 'string') ? document.createTextNode(currentPatch.node) : currentPatch.node.render() node.parentNode.replaceChild(newNode, node) break case REORDER: reorderChildren(node, currentPatch.moves) break case PROPS: setProps(node, currentPatch.props) break case TEXT: node.textContent = currentPatch.content break default: throw new Error('Unknown patch type ' + currentPatch.type) } }) }
три,Vue
исходный кодVirtual-DOM
Краткий анализ
Начнем со второй главы (Virtual-DOM
основы) уже освоилVirtual DOM
сделать как реальныйDOM
на самом деле испытатьVNode
Определение,diff
,patch
и так далее, поэтому эта главаVue
Анализ исходного кода также кратко анализируется в соответствии с этими процессами.
3.1,VNode
моделированиеDOM
Дерево
3.1.1,VNode
Класс Анализ
существуетVue.js
середина,Virtual DOM
используетсяVNode
этоClass
описать, он определяется вsrc/core/vdom/vnode.js
, как видно из следующего блока кодаVue.js
серединаVirtual DOM
Определение немного сложнее, потому что оно содержит многоVue.js
характеристики. ФактическиVue.js
серединаVirtual DOM
заимствовано из библиотеки с открытым исходным кодомsnabbdomреализации, а затем добавить некоторыеVue.js
некоторые характеристики.
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for SSR caching
devtoolsMeta: ?Object; // used to store functional render context for devtools
fnScopeId: ?string; // functional scope id support
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
}
Не здесь, потому чтоVNode
Если вас пугает такой атрибут, или вы скрипите зубами, чтобы узнать значение каждого атрибута, на самом деле мы в основном понимаем ключевые атрибуты его ядра, такие как:
-
tag
атрибут этоvnode
атрибут тега -
data
Свойство содержит окончательный рендер как реальныйdom
После узла, на узлеclass
,attribute
,style
и связанные события -
children
собственностьvnode
дочерний узел -
text
атрибут является текстовым атрибутом -
elm
атрибут для этогоvnode
соответствующая реальностьdom
узел -
key
собственностьvnode
Он отмечает, вdiff
процесс может улучшитьdiff
эффективность
3.1.2, создание исходного кодаVNode
Обработать
(1) Инициализировать vue
мы создаем экземплярvue
экземпляр, то естьnew Vue( )
при фактическом выполненииsrc/core/instance/index.js
определено вFunction
функция.
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
просмотревVue
изfunction
,мы знаемVue
только черезnew
инициализация ключевого слова, затем вызовитеthis._init
метод, который находится вsrc/core/instance/init.js
определено в .
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// 省略一系列其它初始化的代码
if (vm.$options.el) {
console.log('vm.$options.el:',vm.$options.el);
vm.$mount(vm.$options.el)
}
}
(2)Vue
монтирование экземпляра
Vue
через$mount
метод экземпляра для монтированияdom
, ниже анализируемcompiler
версияmount
Реализация, соответствующий исходный код находится в каталогеsrc/platforms/web/entry-runtime-with-compiler.js
Определено в файле: .
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
// 省略一系列初始化以及逻辑判断代码
return mount.call(this, el, hydrating)
}
Мы обнаружили, что в итоге мы вызвали исходный прототип с помощью$mount
Способ крепления на исходном прототипе$mount
метод вsrc/platforms/web/runtime/index.js
Определение определено.
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
мы обнаруживаем$mount
метод действительно вызоветmountComponent
метод, который определен вsrc/core/instance/lifecycle.js
в файле
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 省略一系列其它代码
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
// 生成虚拟 vnode
const vnode = vm._render()
// 更新 DOM
vm._update(vnode, hydrating)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// 实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
return vm
}
Как видно из приведенного выше кода,mountComponent
Ядро состоит в том, чтобы сначала создать экземпляр рендерингаWatcher
, в своей функции обратного вызова вызоветupdateComponent
метод, в этом вызове методаvm._render
Метод сначала создает виртуальный узел и, наконец, вызываетvm._update
возобновитьDOM
.
(3) Создайте виртуальный узел
Vue
из _render
Метод является закрытым методом экземпляра, который используется для рендеринга экземпляра как виртуального.Node
. Это определено вsrc/core/instance/render.js
В файле:
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
let vnode
try {
// 省略一系列代码
currentRenderingInstance = vm
// 调用 createElement 方法来返回 vnode
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`){}
}
// set parent
vnode.parent = _parentVnode
console.log("vnode...:",vnode);
return vnode
}
Vue.js
использовать_createElement
создание методаVNode
, который определен вsrc/core/vdom/create-elemenet.js
середина:
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// 省略一系列非主线代码
if (normalizationType === ALWAYS_NORMALIZE) {
// 场景是 render 函数不是编译生成的
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
// 场景是 render 函数是编译生成的
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// 创建虚拟 vnode
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
_createElement
Метод имеет 5 параметров,context
Представляет контекст VNode, которыйComponent
Типы;tag
Представляет метку, которая может быть строкой илиComponent
;data
Представляет данные VNode, который являетсяVNodeData
тип, который можно найти вflow/vnode.js
Найдите его определение в ;children
Представляет собой детский узел текущего Vnode, который имеет любой тип и должен быть стандартизирован как стандартныйVNode
множество;
3.1.3. Просмотр экземпляра
Для того, чтобы более интуитивно видеть то, что мы обычно пишемVue
Как использовать кодVNode
Класс для представления, у нас есть более глубокое понимание через преобразование экземпляра.
Например, создать экземплярVue
Пример:
var app = new Vue({
el: '#app',
render: function (createElement) {
return createElement('div', {
attrs: {
id: 'app',
class: "class_box"
},
}, this.message)
},
data: {
message: 'Hello Vue!'
}
})
Мы распечатываем соответствующийVNode
Выражать:
3.2,diff
Обработать
3.2.1,Vue.js
исходный кодdiff
логика вызова
Vue.js
Исходный код создает экземплярwatcher
, ~ добавляется к зависимостям переменных, связанных в шаблоне, послеmodel
Ответные данные в ответных данных изменились, ответные данные сохраненыdep
массив вызоветdep.notify()
Метод выполняет всю работу, выполняемую обходом зависимостей, включая обновление представления, т.е.updateComponent
вызов метода.watcher
а такжеupdateComponent
Метод определен вsrc/core/instance/lifecycle.js
в файле.
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 省略一系列其它代码
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
// 生成虚拟 vnode
const vnode = vm._render()
// 更新 DOM
vm._update(vnode, hydrating)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// 实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
return vm
}
Для завершения обновления представления на самом деле нужно вызватьvm._update
метод, первый параметр, полученный этим методом, просто генерируетсяVnode
, называетсяvm._update
Метод определен вsrc/core/instance/lifecycle.js
середина.
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
if (!prevVnode) {
// 第一个参数为真实的node节点,则为初始化
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 如果需要diff的prevVnode存在,那么对prevVnode和vnode进行diff
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
}
Самое главное в этом методеvm.__patch__
метод, который также является целымvirtual-dom
Среди них самый основной метод в основном завершаетprevVnode
а такжеvnode
изdiff
обрабатывать и работать по мере необходимостиvdom
попадание в узелpatch
, и, наконец, создать новый реальныйdom
node и завершите обновление представления.
Далее давайте посмотримvm.__patch__
логический процессvm.__patch__
метод определен вsrc/core/vdom/patch.js
середина.
function patch (oldVnode, vnode, hydrating, removeOnly) {
......
if (isUndef(oldVnode)) {
// 当oldVnode不存在时,创建新的节点
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
// 对oldVnode和vnode进行diff,并对oldVnode打patch
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}
......
}
}
существуетpatch
В методе мы видим, что будет два случая, один, когдаoldVnode
Когда его нет, будет создан новый узел, другой уже будет существоватьoldVnode
, тоoldVnode
а такжеvnode
провестиdiff
а такжеpatch
процесс. вpatch
процесс вызоветsameVnode
способ сопряжения входящих 2vnode
Сравнение основных свойств считается равным 2 только тогда, когда основные свойства совпадают.vnode
Происходит только частичное обновление, а потом эти дваvnode
провестиdiff
, если 2vnode
Есть несоответствие в основных свойствах , тогда оно будет пропущено напрямуюdiff
процесса, а затем на основеvnode
создать настоящийdom
, при удалении старогоdom
узел.
function sameVnode (a, b) {
return (
a.key === b.key &&
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
)
}
diff
Процесс в основном осуществляется путем вызоваpatchVnode
метод:
function patchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
......
const elm = vnode.elm = oldVnode.elm
const oldCh = oldVnode.children
const ch = vnode.children
// 如果vnode没有文本节点
if (isUndef(vnode.text)) {
// 如果oldVnode的children属性存在且vnode的children属性也存在
if (isDef(oldCh) && isDef(ch)) {
// updateChildren,对子节点进行diff
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
// 如果oldVnode的text存在,那么首先清空text的内容,然后将vnode的children添加进去
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 删除elm下的oldchildren
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// oldVnode有子节点,而vnode没有,那么就清空这个节点
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 如果oldVnode和vnode文本属性不同,那么直接更新真是dom节点的文本元素
nodeOps.setTextContent(elm, vnode.text)
}
......
}
Из приведенного выше кода
diff
В процессе бывает несколько ситуаций.oldCh
дляoldVnode
дочерние узлы ,ch
дляVnode
дочерние узлы:
- Сначала оцените текстовый узел, если
oldVnode.text !== vnode.text
, то текстовый узел будет заменен напрямую; - существует
vnode
В случае отсутствия текстового узла введите дочерний узелdiff
; - когда
oldCh
а такжеch
Если оба существуют и не совпадают, вызовитеupdateChildren
на дочерних узлахdiff
; - подобно
oldCh
не существует,ch
существует, очистить сначалаoldVnode
текстовый узел при вызовеaddVnodes
метод будетch
добавить вelm
реальностьdom
среди узлов; - подобно
oldCh
существует,ch
не существует, удалитеelm
под настоящим узломoldCh
дочерний узел; - подобно
oldVnode
имеет текстовые узлы, аvnode
Нет, тогда очистите текстовый узел.
3.2.2, дочерние узлыdiff
Анализ процесса
(1)Vue.js
исходный код
Здесь мы сосредоточимся на анализеupdateChildren
метод, это также весьdiff
Наиболее важная часть процесса заключается в следующем.Vue.js
Процесс исходного кода, чтобы лучше понятьdiff
Процесс, мы даем соответствующие схемы для объяснения.
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
// 为oldCh和newCh分别建立索引,为之后遍历的依据
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// 直到oldCh或者newCh被遍历完后跳出循环
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
в начале обходаdiff
прежде, сначала дайтеoldCh
а такжеnewCh
Выделите одинstartIndex
а такжеendIndex
как индекс обхода, когдаoldCh
илиnewCh
После обхода (условие обходаoldCh
илиnewCh
изstartIndex >= endIndex
), остановкаoldCh
а такжеnewCh
изdiff
Обработать. Далее давайте посмотрим на всеdiff
Процесс (без свойств узлаkey
Случай).
(2) Нетkey
изdiff
Обработать
Мы объясним приведенный выше процесс кода с помощью следующей схематической диаграммы:
(2.1) Сначала начните сравнение с первого узла, будь тоoldCh
ещеnewCh
Запуск или завершающий узел не существуетsameVnode
, при этом атрибут узла не сkey
отмечено, поэтому первый раундdiff
После завершения,newCh
изstartVnode
был добавлен вoldStartVnode
напротивnewStartIndex
двигаться вперед на один;
(2.2) Второй раундdiff
средний, доволенsameVnode(oldStartVnode, newStartVnode)
, поэтому для этих 2vnode
провестиdiff
, и наконецpatch
ударoldStartVnode
на, в то же времяoldStartVnode
а такжеnewStartIndex
продвинуться на одну позицию вперед;
(2.3) Третий раундdiff
средний, доволенsameVnode(oldEndVnode, newStartVnode)
, то сначалаoldEndVnode
а такжеnewStartVnode
провестиdiff
и кoldEndVnode
провестиpatch
, и завершитьoldEndVnode
сменная операция и, наконец,newStartIndex
двигаться вперед на одно место,oldStartVnode
двигаться назад на один;
(2.4) Четвертый раундdiff
, процесс такой же, как шаг 3;
(2.5) Пятый раундdiff
, то же, что процесс 1;
(2.6) После завершения процесса обходаnewStartIdx > newEndIdx
, что свидетельствует о том, что в это времяoldCh
Если есть избыточные узлы, то, наконец, эти избыточные узлы необходимо удалить.
(3) Даkey
изdiff
обработать
существуетvnode
Безkey
случае, каждый раундdiff
во время процесса起始
а также结束
узлы сравниваются до тех пор, покаoldCh
илиnewCh
был пройден. в то время как вvnode
представлятьkey
атрибут, в каждом раундеdiff
процесс, когда起始
а также结束
узел не найденsameVnode
Когда тогда определяетnewStartVnode
Есть лиkey
, и будь тоoldKeyToIndx
Найдите соответствующий узел в:
- если этого не существует
key
, затем установите этоnewStartVnode
Создан как новый узел и вставлен в исходныйroot
в дочернем узле ; - если это существует
key
, затем выньтеoldCh
существование этогоkey
изvnode
, а затем перейдите кdiff
над;
Благодаря приведенному выше анализуvdom
добавитьkey
После свойства повторитеdiff
процесс, когдаотправная точка,конечная точкаизпоиска такжеdiff
Когда совпадения по-прежнему нет, оно будет использованоkey
в качестве уникального идентификатора для выполненияdiff
, чтобы он мог увеличиватьсяdiff
эффективность.
с участиемKey
атрибутvnode
изdiff
Процесс можно увидеть на следующем рисунке:
(3.1) Сначала начните сравнение с первого узла, будь тоoldCh
ещеnewCh
ни начальный, ни конечный узел не существуетsameVnode
, но атрибут узла сkey
отмечено, то вoldKeyToIndx
Найдите соответствующий узел в первом раундеdiff
послеoldCh
ВверхB节点
был удален, ноnewCh
ВверхB节点
начальствоelm
право собственностиoldCh
начальствоB节点
изelm
Цитировать.
(3.2) Второй раундdiff
средний, доволенsameVnode(oldStartVnode, newStartVnode)
, поэтому для этих 2vnode
провестиdiff
, и наконецpatch
ударoldStartVnode
на, в то же времяoldStartVnode
а такжеnewStartIndex
продвинуться на одну позицию вперед;
(3.3) Третий раундdiff
средний, доволенsameVnode(oldEndVnode, newStartVnode)
, то сначалаoldEndVnode
а такжеnewStartVnode
провестиdiff
, и кoldEndVnode
провестиpatch
, и завершитьoldEndVnode
сменная операция и, наконец,newStartIndex
двигаться вперед на одно место,oldStartVnode
двигаться назад на один;
(3.4) Четвертый раундdiff
, процесс такой же, как шаг 2;
(3.5) Пятый раундdiff
, потому что в это времяoldStartIndex
было больше, чемoldEndIndex
, поэтому оставшиесяVnode
Очередь вставляется в конец очереди.
3.3,patch
Обработать
введено через главу 3.2diff
В процессе мы увидимnodeOps
Метод корреляции с реальнымDOM
структура для работы,nodeOps
определено вsrc/platforms/web/runtime/node-ops.js
, что является основнымDOM
Подробно операция здесь не описывается.
export function createElementNS (namespace: string, tagName: string): Element {
return document.createElementNS(namespaceMap[namespace], tagName)
}
export function createTextNode (text: string): Text {
return document.createTextNode(text)
}
export function createComment (text: string): Comment {
return document.createComment(text)
}
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
parentNode.insertBefore(newNode, referenceNode)
}
export function removeChild (node: Node, child: Node) {
node.removeChild(child)
}
3.4, резюме
Через анализ первых трех подразделов мы выведем шаблон и данные в окончательный вид из основной строки.DOM
Анализ процесса завершен, мы можем более интуитивно увидеть инициализацию на следующем рисунке.Vue
весь процесс до окончательного рендеринга.
4. Резюме
Эта статья из введения реальногоDOM
Его структура и процесс решения проблем, что приводит к тому, почему виртуальныйDOM
; затем проанализируйте виртуальныйDOM
Преимущества , а также реализация некоторых его теоретических основ и базовых алгоритмов, наконец, на основе освоенных нами базовых знаний мы проверим его шаг за шагом.Vue.js
Как реализован исходный код. От существующих проблем -> теоретическая база -> конкретная практика, шаг за шагом, чтобы помочь каждому лучше понять, что естьVirtual DOM
, зачем нужноVirtual DOM
,так же какVirtual DOM
Конкретная реализация, я надеюсь, что эта статья будет вам полезна.
** Я много работал над написанием в течение долгого времени. Если это будет полезно для вас, я надеюсь вручную лайкнуть и поощрить ~~~~~~**
Адрес гитхаба:GitHub.com/ревматизм123/…, который обобщает все статьи в блоге автора, если вам нравится или вдохновляет, пожалуйста, помогите дать звезду ~, что также является поощрением для автора.
использованная литература
1. Технология Vue показала:США ТБ Huang Yi.GitHub.IO/v UE-Anarias…
2. Углубленный анализ: как реализовать алгоритм Virtual DOM:сегмент fault.com/ah/119000000…
3. Виртуальный DOM (vdom) ядра vue:woo woo Краткое описание.com/afraid/Afan 0 no 39860…
4. Краткий анализ виртуального дома (реализация Vue):сегмент fault.com/ah/119000001…