Виртуальный DOM/domDiff
Мы часто говорим, что виртуальный DOM моделируется объектами JS.DOM
Node, domDiff вычисляется по определенному алгоритму для выполнения операцииDOM
Разнообразие.
Виртуальный DOM используется как в React, так и в Vue. Я остаюсь только на уровне использования vue и не буду говорить больше. Если я узнаю больше о React, я расскажу о виртуальном DOM через React.
Код, задействованный в виртуальном DOM в React, в основном разделен на следующие три части, ядром которых является алгоритм domDiff на втором этапе:
- Преобразование JSX (или createElement API) в рендере в виртуальный DOM
- Пересчитайте виртуальный DOM и сгенерируйте объект исправления (domDiff) после изменения состояния или свойства.
- Обновите узлы DOM в представлении с помощью этого объекта исправления.
Виртуальный DOM не обязательно быстрее
Все, кто работает на фронтенде, знают, чтоDOM
Операции убивают производительность, потому что операцииDOM
Это приведет к переформатированию или перерисовке страницы. Для сравнения, он уменьшается за счет большего количества предварительных вычислений.DOM
эксплуатация более рентабельна.
Однако фраза «использование виртуального DOM будет быстрее» не обязательно применима ко всем сценариям. Например: на странице есть кнопка, нажмите на нее, число увеличивается на единицу, это должна быть прямая операцияDOM
Быстрее. Использование виртуального DOM просто увеличивает объем вычислений и кода. Даже в сложных ситуациях браузерDOM
Операция оптимизирована, и большинство браузеров будут выполнять пакетную обработку в зависимости от времени и количества наших операций, поэтому работайте напрямую.DOM
Это также не должно быть медленным.
Так почему же современные фреймворки используют виртуальный DOM? Потому что использование виртуального DOM может повысить предел производительности кода и значительно оптимизировать потери производительности, вызванные большим количеством операций DOM. В то же время эти фреймворки также гарантируют, что даже в некоторых сценариях, где виртуальный DOM не очень мощный, производительность находится в приемлемом диапазоне.
Более того, нам нравится использовать виртуальные DOM-фреймворки, такие как react, vue и т. д., не только потому, что они быстрые, но и по многим другим более важным причинам. Например, React дружественно относится к функциональному программированию, отличному опыту разработки Vue и т. д. В настоящее время в сообществе есть много людей, которые сравнивают два фреймворка и ведут словесную перепалку. принципы, когда вы понимаете оба.
Идея реализации domDiff
Реализация domDiff разделена на следующие четыре шага:
- Имитация реальных узлов DOM с помощью JS
- Преобразуйте виртуальный DOM в реальный DOM и вставьте его на страницу.
- Когда происходит изменение, сравните различия двух деревьев и сгенерируйте объект различия.
- Обновить реальный DOM на основе объекта diff
Не забывайте о старой работе дизайнера, смотрите, как я рисую картинку:
Объясните эту картинку:
Сначала посмотрите на первый блок красного цвета, здесь нужно поставить настоящийDOM
карта как виртуальнаяDOM
, На самом деле такого процесса в react нет, то что мы пишем напрямую это виртуальный DOM (JSX), просто этот виртуальныйDOM
представляет правдуDOM
.
Когда виртуальный DOM изменяется, как на картинке выше, его третийp
и второйp
серединаson2
был удален. В это время мы вычислим объект разницы на основе изменений до и послеpatches
.
Ключевое значение этого разностного объекта является старым.DOM
Индекс обхода узла, по этому индексу мы можем найти этот узел. Значением свойства является записанное изменение, вот оноremove
, что означает удалить.
Наконец, согласноpatches
Индекс каждого элемента в нем переходит в соответствующую позицию, чтобы изменить старый.DOM
узел.
Как код для этого?
Создайте настоящий DOM через виртуальный DOM
Следующий код является входным файлом, мы моделируем виртуальный DOM с именемoldEle
, мы мертвы написано здесь. В React абстрактное синтаксическое дерево (AST) получается путем разбора грамматики JSX через babel, а затем генерируется виртуальный DOM. Если вас интересует трансформация Babel, вы можете ознакомиться с другой статьей.Начало работы с babel — реализация конвертера классов es6.
import { createElement } from './createElement'
let oldEle = createElement('div', { class: 'father' }, [
createElement('h1', { style:'color:red' }, ['son1']),
createElement('h2', { style:'color:blue' }, ['son2']),
createElement('h3', { style:'color:red' }, ['son3'])
])
document.body.appendChild(oldEle.render())
Следующий файл экспортируетсяcreateElement
метод. это на самом делеnew
взял одинElement
класс, который вызывает этот классrender
метод может поставить виртуальныйDOM
преобразовать в реальныйDOM
.
class Element {
constructor(tagName, attrs, childs) {
this.tagName = tagName
this.attrs = attrs
this.childs = childs
}
render() {
let element = document.createElement(this.tagName)
let attrs = this.attrs
let childs = this.childs
//设置属性
for (let attr in attrs) {
setAttr(element, attr, attrs[attr])
}
//先序深度优先遍历子创建并插入子节点
for (let i = 0; i < childs.length; i++) {
let child = childs[i]
console.log(111, child instanceof Element)
let childElement = child instanceof Element ? child.render() : document.createTextNode(child)
element.appendChild(childElement)
}
return element
}
}
function setAttr(ele, attr, value) {
switch (attr) {
case 'style':
ele.style.cssText = value
break;
case 'value':
let tageName = ele.tagName.toLowerCase()
if (tagName == 'input' || tagName == 'textarea') {
ele.value = value
} else {
ele.setAttribute(attr, value)
}
break;
default:
ele.setAttribute(attr, value)
break;
}
}
function createElement(tagName, props, child) {
return new Element(tagName, props, child)
}
module.exports = { createElement }
Теперь этот код можно запустить, и результат после выполнения следующий:
Продолжайте смотреть на алгоритм domDIff
//keyIndex记录遍历顺序
let keyIndex = 0
function diff(oldEle, newEle) {
let patches = {}
keyIndex = 0
walk(patches, 0, oldEle, newEle)
return patches
}
//分析变化
function walk(patches, index, oldEle, newEle) {
let currentPatches = []
//这里应该有很多的判断类型,这里只处理了删除的情况...
if (!newEle) {
currentPatches.push({ type: 'remove' })
}
else if (oldEle.tagName == newEle.tagName) {
//比较儿子们
walkChild(patches, currentPatches, oldEle.childs, newEle.childs)
}
//判断当前节点是否有改变,有的话把补丁放入补丁集合中
if (currentPatches.length) {
patches[index] = currentPatches
}
}
function walkChild(patches, currentPatches, oldChilds, newChilds) {
if (oldChilds) {
for (let i = 0; i < oldChilds.length; i++) {
let oldChild = oldChilds[i]
let newChild = newChilds[i]
walk(patches, ++keyIndex, oldChild, newChild)
}
}
}
module.exports = { diff }
Приведенный выше код представляет собой суперупрощенную версию алгоритма domDiff:
- Сначала объявите переменную для записи порядка обхода
- Выполните метод прогулки, чтобы проанализировать изменения, если два элемента имеют одно и то же имя тега, рекурсивно пройти по дочерним узлам.
На самом деле логики в walk должно быть много, я разобрался только с одним случаем, то есть с удалением элемента. На самом деле должны быть дополнения, замены и т. д., и задействовано множество проверок границ. Настоящий алгоритм domDiff очень сложен, его сложность должна быть O(n3), React идет на ряд компромиссов, чтобы снизить сложность до линейной.
Я просто выбрал ситуацию для демонстрации здесь.Если вам интересно, вы можете посмотреть исходный код или поискать некоторые статьи по теме. Ведь эта статья называется "простое введение", очень поверхностно...
Хорошо, тогда давайте выполним этот алгоритм, чтобы увидеть эффект:
import { createElement } from './createElement'
import { diff } from './diff'
let oldEle = createElement('div', { class: 'father' }, [
createElement('h1', { style: 'color:red' }, ['son1']),
createElement('h2', { style: 'color:blue' }, ['son2']),
createElement('h3', { style: 'color:red' }, ['son3'])
])
let newEle = createElement('div', { class: 'father' }, [
createElement('h1', { style: 'color:red' }, ['son1']),
createElement('h2', { style: 'color:blue' }, [])
])
console.log(diff(oldEle, newEle))
Я создал новый элемент в файле ввода для представления измененного виртуального DOM, из него удалены два элемента, одинh3
, текстовый узелson2
, по идее должно быть две записи, посмотрим код выполнения:
Мы видим, что на выходеpatches
В объекте есть два атрибута.Имя атрибута-это порядковый номер обхода этого элемента, а значение атрибута-это записанная информация.Мы просто используем порядковый номер для обхода, чтобы найти старый.DOM
Узел соответственно обновляется посредством информации в значении атрибута.
обновить вид
Давайте посмотрим, как получитьpatches
Обзор объекта Обзор:
let index = 0;
let allPatches;
function patch(root, patches) {
allPatches = patches
walk(root)
}
function walk(root) {
let currentPatches = allPatches[index]
index++
(root.childNodes || []) && root.childNodes.forEach(child => {
walk(child)
})
if (currentPatches) {
doPatch(root, currentPatches)
}
}
function doPatch(ele, currentPatches) {
currentPatches.forEach(currentPatch => {
if (currentPatch.type == 'remove') {
ele.parentNode.removeChild(ele)
}
})
}
module.exports = { patch }
файл экспортированpatch
Метод имеет два параметра,root
Это правдаDOM
узел,patches
это объект патча, мы используем и просматриваем виртуальныйDOM
Такой же подход (предварительный заказ в глубину) для обхода реальных узлов важен, потому что мы проходим черезpatches
объектkey
Атрибут записывает, какой узел изменился, и тот же метод обхода может гарантировать правильность нашего соответствующего отношения.
doPatch
Метод очень прост, судя поtype
"удалить", удалитьDOM
узел. На самом деле, этот метод не должен быть таким простым, он также должен обрабатывать многие вещи, такие как удаление, обмен и т. д. На самом деле, он также должен оценивать изменения атрибутов и обрабатывать их соответствующим образом.
Разобраться несложно, поэтому я с такими не сталкивался, конечно не скажу, что совсем не умею писать...
Теперь применим этоpatch
метод:
import { createElement } from './createElement'
import { diff } from './diff'
import { patch } from './patch'
let oldEle = createElement('div', { class: 'father' }, [
createElement('h1', { style: 'color:red' }, ['son1']),
createElement('h2', { style: 'color:blue' }, ['son2']),
createElement('h3', { style: 'color:green' }, ['son3'])
])
let newEle = createElement('div', { class: 'father' }, [
createElement('h1', { style: 'color:red' }, ['son1']),
createElement('h2', { style: 'color:green' }, [])
])
//这里应用了patch方法,给原始的root节点打了补丁,更新成了新的节点
let root = oldEle.render()
let patches = diff(oldEle, newEle)
patch(root, patches)
document.body.appendChild(root)
Хорошо, давайте выполним код и посмотрим, как изменится вид:
Мы видим, что тег h3 исчез, тег h2 остался, но текстовый узел son2 внутри исчез, что соответствует нашим ожиданиям.
На данный момент алгоритм написан, а выложенные выше коды все выложены модулями, и они полностью рабочие.
открытые вопросы
Есть еще много проблем, с которыми не справился этот алгоритм, например:
- Не обрабатывает изменения свойств
- Только дело с ситуацией, удаленной, дополнения и замены не рассматриваются
- Если вы удалите первый элемент, то из-за того, что индекс неуместен, следующие элементы будут считаться другими и замененными.React использует ключевой атрибут для решения этой проблемы, а также идет на компромисс для производительности.
- Конечно есть много-много оптимизаций
наконец
Вышеприведенный код является просто простой реализацией основной идеи в реакции, просто для того, чтобы каждый понял идею алгоритма domDiff.Если мое описание вас немного заинтересует domDiff или немного поможет вам, я очень счастливый.