предисловие
использовать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объект для представленияDOMNode, используйте свойства объекта для записи типа, свойств, дочерних узлов и т. д. узла.
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.jsfunction 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, и, наконец, создать новый реальныйdomnode и завершите обновление представления.
Далее давайте посмотрим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…