Keep Moving
Сегодня представление о знаниях в интерфейсе становится все более и более высоким, и оно стало выше.
О всевозможных фреймворках уже можно сказать, что они более или менее использовались как в работе, так и в повседневной жизни.
Я слышал, что у многих людей спрашивали, как реализованы алгоритмы виртуального DOM и DOM-diff, вы изучали это?
Предположительно тот, кто задал этот вопрос, тоже мастер из мастеров.Многие говорили полушутя: "Интервью строить авианосцы, работать гайки закручивать"
Итак, без лишних слов, давайте сегодня вместе изучим этот материал.
Хорошая еда не боится опоздать, угомонитесь и угомонитесь! Мы шли медленно, но никогда не останавливались
Волшебный виртуальный DOM
Прежде всего, не будем обращать внимание на магию или нет, коротко поговорим о том, что такое виртуальный DOM.
виртуальный DOMКороче говоря, используя JS для реализации объекта древовидной структуры в соответствии со структурой DOM, вы также можете вызвать егоDOM-объект
Ну, я объяснил такую замечательную вещь в одном предложении, так что не теряйте больше времени, давайте перейдем к основной ссылке
Конечно, есть и весь проектадреслегко просматривать
Реализовать виртуальный DOM
Прежде чем идти в бой лично, давайте начнем с еды и травы и отправим изображение, чтобы увидеть, как выглядит вся структура каталогов.
Эта структура каталоговСоздание-реагирование-скаффолдинг приложенияСгенерировано напрямую, но и для облегчения отладки компиляции// 全局安装
npm i create-react-app -g
// 生成项目
create-react-app dom-diff
// 进入项目目录
cd dom-diff
// 编译
npm run start
Теперь давайте начнем писать формально, начиная с создания виртуального DOM и рендеринга DOM.
Создать виртуальный DOM
В файле element.js показано, как создать виртуальный DOM и преобразовать созданный виртуальный DOM в реальный DOM.
Сначала реализуйте, как создать виртуальный DOM, посмотрите на код:
// element.js
// 虚拟DOM元素的类,构建实例对象,用来描述DOM
class Element {
constructor(type, props, children) {
this.type = type;
this.props = props;
this.children = children;
}
}
// 创建虚拟DOM,返回虚拟节点(object)
function createElement(type, props, children) {
return new Element(type, props, children);
}
export {
Element,
createElement
}
После написания метода давайте начнем с файла index.js, чтобы проверить, успешно ли он работает.
вызвать метод createElement
В основном файле записи основная операция, которую мы выполняем, — это создание объекта DOM, визуализация DOM и обновление DOM с помощью исправлений после сравнения.Давайте не будем вдаваться в подробности, просто посмотрите на код:
// index.js
// 首先引入对应的方法来创建虚拟DOM
import { createElement } from './element';
let virtualDom = createElement('ul', {class: 'list'}, [
createElement('li', {class: 'item'}, ['周杰伦']),
createElement('li', {class: 'item'}, ['林俊杰']),
createElement('li', {class: 'item'}, ['王力宏'])
]);
console.log(virtualDom);
Метод createElement также является методом, используемым vue и react для создания виртуального DOM, Мы также называем его этим именем для облегчения запоминания. Получает три параметра, которыеtype,propsа такжеchildren
- Параметрический анализ
- тип: указывает тип метки элемента, например, «li», «div», «a» и т. д.
- props: указывает атрибуты указанного элемента, такие как класс, стиль, пользовательские атрибуты и т. д.
- Children: указывает, есть ли у указанного элемента дочерние узлы, параметр передается в виде массива
Давайте взглянем на распечатанный виртуальный DOM, как показано ниже.
До сих пор создать виртуальный DOM было легко. Итак, переходите к следующему шагу, визуализируйте его как настоящий DOM, не стесняйтесь, вернитесь к файлу element.js.визуализировать виртуальный DOM
// element.js
class Element {
// 省略
}
function createElement() {
// 省略
}
// render方法可以将虚拟DOM转化成真实DOM
function render(domObj) {
// 根据type类型来创建对应的元素
let el = document.createElement(domObj.type);
// 再去遍历props属性对象,然后给创建的元素el设置属性
for (let key in domObj.props) {
// 设置属性的方法
setAttr(el, key, domObj.props[key]);
}
// 遍历子节点
// 如果是虚拟DOM,就继续递归渲染
// 不是就代表是文本节点,直接创建
domObj.children.forEach(child => {
child = (child instanceof Element) ? render(child) : document.createTextNode(child);
// 添加到对应元素内
el.appendChild(child);
});
return el;
}
// 设置属性
function setAttr(node, key, value) {
switch(key) {
case 'value':
// node是一个input或者textarea就直接设置其value即可
if (node.tagName.toLowerCase() === 'input' ||
node.tagName.toLowerCase() === 'textarea') {
node.value = value;
} else {
node.setAttribute(key, value);
}
break;
case 'style':
// 直接赋值行内样式
node.style.cssText = value;
break;
default:
node.setAttribute(key, value);
break;
}
}
// 将元素插入到页面内
function renderDom(el, target) {
target.appendChild(el);
}
export {
Element,
createElement,
render,
setAttr,
renderDom
};
Теперь, когда все готово, давайте посмотрим на результаты
вызвать метод рендеринга
Снова вернитесь к файлу index.js и измените его на следующий код.
// index.js
// 引入createElement、render和renderDom方法
import { createElement, render, renderDom } from './element';
let virtualDom = createElement('ul', {class: 'list'}, [
createElement('li', {class: 'item'}, ['周杰伦']),
createElement('li', {class: 'item'}, ['林俊杰']),
createElement('li', {class: 'item'}, ['王力宏'])
]);
console.log(virtualDom);
// +++
let el = render(virtualDom); // 渲染虚拟DOM得到真实的DOM结构
console.log(el);
// 直接将DOM添加到页面内
renderDom(el, document.getElementById('root'));
// +++
Преобразуйте DOM в настоящий DOM, вызвав метод рендеринга, и напрямую добавьте DOM на страницу, вызвав метод renderDom.
На следующем рисунке показан результат после печати:
На данный момент мы реализовали виртуальный DOM и визуализировали настоящий DOM на странице. Затем мы пригласим DOM-diff для грандиозного дебюта, давайте посмотрим, как сияет этот великолепный алгоритм сравнения!Дебют DOM-diff
Когда дело доходит до DOM-diff, вы должны четко понимать смысл его существования.Для любых двух деревьев используйтеПредварительный заказ обхода в глубинуАлгоритм поиска наименьшего количества шагов преобразования
DOM-diff сравнивает разницу между двумя виртуальными DOM, то есть сравнивает разницу между двумя объектами.
эффект:Создайте патч на основе двух виртуальных объектов, опишите изменения и используйте этот патч для обновления DOM.
Теперь, когда вы знаете, для чего нужен DOM-diff, тут и говорить нечего, продолжим писать
// diff.js
function diff(oldTree, newTree) {
// 声明变量patches用来存放补丁的对象
let patches = {};
// 第一次比较应该是树的第0个索引
let index = 0;
// 递归树 比较后的结果放到补丁里
walk(oldTree, newTree, index, patches);
return patches;
}
function walk(oldNode, newNode, index, patches) {
// 每个元素都有一个补丁
let current = [];
if (!newNode) { // rule1
current.push({ type: 'REMOVE', index });
} else if (isString(oldNode) && isString(newNode)) {
// 判断文本是否一致
if (oldNode !== newNode) {
current.push({ type: 'TEXT', text: newNode });
}
} else if (oldNode.type === newNode.type) {
// 比较属性是否有更改
let attr = diffAttr(oldNode.props, newNode.props);
if (Object.keys(attr).length > 0) {
current.push({ type: 'ATTR', attr });
}
// 如果有子节点,遍历子节点
diffChildren(oldNode.children, newNode.children, patches);
} else { // 说明节点被替换了
current.push({ type: 'REPLACE', newNode});
}
// 当前元素确实有补丁存在
if (current.length) {
// 将元素和补丁对应起来,放到大补丁包中
patches[index] = current;
}
}
function isString(obj) {
return typeof obj === 'string';
}
function diffAttr(oldAttrs, newAttrs) {
let patch = {};
// 判断老的属性中和新的属性的关系
for (let key in oldAttrs) {
if (oldAttrs[key] !== newAttrs[key]) {
patch[key] = newAttrs[key]; // 有可能还是undefined
}
}
for (let key in newAttrs) {
// 老节点没有新节点的属性
if (!oldAttrs.hasOwnProperty(key)) {
patch[key] = newAttrs[key];
}
}
return patch;
}
// 所有都基于一个序号来实现
let num = 0;
function diffChildren(oldChildren, newChildren, patches) {
// 比较老的第一个和新的第一个
oldChildren.forEach((child, index) => {
walk(child, newChildren[index], ++num, patches);
});
}
// 默认导出
export default diff;
Хотя код вонючий и длинный, эти коды позволяют нам реализовать алгоритм diff, поэтому, пожалуйста, не двигайтесь вслепую, не двигайтесь вслепую и слушайте ветер, позвольте мне собраться по одному
правила сравнения
- Новый узел DOM не существует {Тип: «Удалить», индекс}
- Изменение текста {тип: 'ТЕКСТ', текст: 1}
- Если типы узлов совпадают, проверьте, совпадают ли атрибуты, и сгенерируйте пакет исправлений атрибутов {тип: 'ATTR', атрибут: {класс: 'list-group'}}
- Типы узлов не совпадают, напрямую используйте режим замены {type: REPLACE, newNode}
Согласно этимправило, давайте посмотрим на код diffметод ходьбыключевой джентльмен
Что делает метод прогулки?
- Каждый элемент имеет патч, поэтому вам нужно создать массив текущего патча
- Если нет нового узла, напрямую поместите тип, тип которого REMOVE, в текущий патч.
if (!newNode) {
current.push({ type: 'REMOVE', index });
}
- Если старый и новый узлы являются текстовыми, оцените, согласован ли текст, затем укажите тип ТЕКСТ и поместите новый узел в текущий патч.
else if (isString(oldNode) && isString(newNode)) {
if (oldNode !== newNode) {
current.push({ type: 'TEXT', text: newNode });
}
}
- Если старый и новый узлы одного типа, сравните их свойство props.
- сравнение свойств
- diffAttr
- чтобы сравнить, совпадают ли старый и новый Attr
- Назначьте пару ключ-значение newAttr объекту исправления и верните этот объект.
- diffAttr
- Затем, если есть дочерние узлы, сравните различия между дочерними узлами и снова отрегулируйте обход.
- diffChildren
- Пройдите через oldChildren, затем рекурсивно вызовите walk, а затем передайте child и newChildren[index] в diff
- diffChildren
- сравнение свойств
else if (oldNode.type === newNode.type) {
// 比较属性是否有更改
let attr = diffAttr(oldNode.props, newNode.props);
if (Object.keys(attr).length > 0) {
current.push({ type: 'ATTR', attr });
}
// 如果有子节点,遍历子节点
diffChildren(oldNode.children, newNode.children, patches);
}
- Если не происходит ничего из трех вышеперечисленных, это означает, что узел просто заменяется, тип REPLACE, и его можно заменить непосредственно на newNode.
else {
current.push({ type: 'REPLACE', newNode});
}
- Если в текущем патче есть значение, поместите соответствующий патч в большой пакет патчей.
if (current.length > 0) {
// 将元素和补丁对应起来,放到大补丁包中
patches[index] = current;
}
Выше приведен процесс анализа алгоритма diff. Не имеет значения, если вы не очень хорошо его понимаете. Если вы будете пытаться снова и снова, несчастные случаи всегда случаются неожиданно.
diff сделан, так что последний шаг всем известенПластырьохватывать
Как играть в патч? Так что пусть выйдет давно потерянный патч
обновление патча
Исправление должно передавать два параметра: один — это элемент, который нужно исправить, а другой — патч, который нужно исправить, а затем посмотрите непосредственно на код.
import { Element, render, setAttr } from './element';
let allPatches;
let index = 0; // 默认哪个需要打补丁
function patch(node, patches) {
allPatches = patches;
// 给某个元素打补丁
walk(node);
}
function walk(node) {
let current = allPatches[index++];
let childNodes = node.childNodes;
// 先序深度,继续遍历递归子节点
childNodes.forEach(child => walk(child));
if (current) {
doPatch(node, current); // 打上补丁
}
}
function doPatch(node, patches) {
// 遍历所有打过的补丁
patches.forEach(patch => {
switch (patch.type) {
case 'ATTR':
for (let key in patch.attr) {
let value = patch.attr[key];
if (value) {
setAttr(node, key, value);
} else {
node.removeAttribute(key);
}
}
break;
case 'TEXT':
node.textContent = patch.text;
break;
case 'REPLACE':
let newNode = patch.newNode;
newNode = (newNode instanceof Element) ? render(newNode) : document.createTextNode(newNode);
node.parentNode.replaceChild(newNode, node);
break;
case 'REMOVE':
node.parentNode.removeChild(node);
break;
default:
break;
}
});
}
export default patch;
После прочтения кода нужно сделать простой анализ
что делает патч?
- Используйте переменную, чтобы получить все патчи, переданные в allPatches
- Метод patch принимает два параметра (узел, патчи)
- Вызовите метод ходьбы внутри метода, чтобы исправить элемент
- Получить все дочерние узлы в методе ходьбы
- Предварительный обход в глубину также выполняется на дочерних узлах, рекурсивный обход
- Если текущий патч существует, то его патч (doPatch)
- Метод исправления doPatch будет проходить в соответствии с переданными исправлениями.
- Определите тип патча для выполнения различных операций
-
атрибут АТТР для обхода объекта attrs, если текущее значение ключа существует, установите атрибут setAttr напрямую; если нет соответствующего значения ключа, удалите атрибут ключевого ключа напрямую
-
Текст ТЕКСТ Просто назначьте текст патча textContent узла узла
-
заменить ЗАМЕНИТЬ Чтобы заменить старый узел новым узлом, необходимо сначала определить, является ли новый узел экземпляром Element, и если да, то вызвать метод рендеринга для рендеринга нового узла;
Если нет, это указывает на то, что новый узел является текстовым узлом, и он в порядке, чтобы напрямую создать текстовый узел.
Затем замените его новым узлом, вызвав метод replaceChild родительского узла parentNode.
-
удалить УДАЛИТЬ Непосредственно вызовите родительский метод removeChild, чтобы удалить узел
-
- Определите тип патча для выполнения различных операций
- Экспорт метода исправления по умолчанию для легкого вызова
Ну все тихо. Вернемся к файлу index.js и вызовем два важных метода diff и patch, чтобы посмотреть, произойдет ли чудо.
вернуть
// index.js
import { createElement, render, renderDom } from './element';
// +++ 引入diff和patch方法
import diff from './diff';
import patch from './patch';
// +++
let virtualDom = createElement('ul', {class: 'list'}, [
createElement('li', {class: 'item'}, ['周杰伦']),
createElement('li', {class: 'item'}, ['林俊杰']),
createElement('li', {class: 'item'}, ['王力宏'])
]);
let el = render(virtualDom);
renderDom(el, window.root);
// +++
// 创建另一个新的虚拟DOM
let virtualDom2 = createElement('ul', {class: 'list-group'}, [
createElement('li', {class: 'item active'}, ['七里香']),
createElement('li', {class: 'item'}, ['一千年以后']),
createElement('li', {class: 'item'}, ['需要人陪'])
]);
// diff一下两个不同的虚拟DOM
let patches = diff(virtualDom, virtualDom2);
console.log(patches);
// 将变化打补丁,更新到el
patch(el, patches);
// +++
Сохраните измененный код, и вы увидите, что DOM обновился в браузере, как показано ниже.
На этом все закончено. Контента много, и это может быть не очень хорошее потребление, но это не имеет значения. Позвольте мне использовать последние несколько предложений, чтобы подытожить весь процесс реализации.четыре предложения
Давайте разберемся во всемDOM-diffпроцесс:
- Моделирование DOM с помощью объектов JS (виртуальный DOM)
- Преобразуйте этот виртуальный DOM в реальный DOM и вставьте его на страницу (рендеринг)
- Если происходит событие, изменяющее виртуальный DOM, сравните разницу между двумя виртуальными деревьями DOM и получите объект различия (diff).
- Применить объект diff к реальному дереву DOM (патч)
Хорошо, только эти четыре слова, скажем больше, есть маленькая змейка. Давно не писал статью, спасибо за просмотр, трудолюбие, 886