Не позволяйте виртуальному DOM и DOM-diff становиться для вас камнем преткновения

Vue.js опрос
Не позволяйте виртуальному DOM и DOM-diff становиться для вас камнем преткновения

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 объекту исправления и верните этот объект.
    • Затем, если есть дочерние узлы, сравните различия между дочерними узлами и снова отрегулируйте обход.
      • diffChildren
        • Пройдите через oldChildren, затем рекурсивно вызовите walk, а затем передайте child и newChildren[index] в diff
    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 будет проходить в соответствии с переданными исправлениями.
    • Определите тип патча для выполнения различных операций
      1. атрибут АТТР для обхода объекта attrs, если текущее значение ключа существует, установите атрибут setAttr напрямую; если нет соответствующего значения ключа, удалите атрибут ключевого ключа напрямую

      2. Текст ТЕКСТ Просто назначьте текст патча textContent узла узла

      3. заменить ЗАМЕНИТЬ Чтобы заменить старый узел новым узлом, необходимо сначала определить, является ли новый узел экземпляром Element, и если да, то вызвать метод рендеринга для рендеринга нового узла;

        Если нет, это указывает на то, что новый узел является текстовым узлом, и он в порядке, чтобы напрямую создать текстовый узел.

        Затем замените его новым узлом, вызвав метод replaceChild родительского узла parentNode.

      4. удалить УДАЛИТЬ Непосредственно вызовите родительский метод 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процесс:

  1. Моделирование DOM с помощью объектов JS (виртуальный DOM)
  2. Преобразуйте этот виртуальный DOM в реальный DOM и вставьте его на страницу (рендеринг)
  3. Если происходит событие, изменяющее виртуальный DOM, сравните разницу между двумя виртуальными деревьями DOM и получите объект различия (diff).
  4. Применить объект diff к реальному дереву DOM (патч)

Хорошо, только эти четыре слова, скажем больше, есть маленькая змейка. Давно не писал статью, спасибо за просмотр, трудолюбие, 886