Простая и простая схема domDIff

внешний интерфейс алгоритм Vue.js React.js
Простая и простая схема domDIff

Виртуальный DOM/domDiff

Мы часто говорим, что виртуальный DOM моделируется объектами JS.DOMNode, 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 разделена на следующие четыре шага:

  1. Имитация реальных узлов DOM с помощью JS
  2. Преобразуйте виртуальный DOM в реальный DOM и вставьте его на страницу.
  3. Когда происходит изменение, сравните различия двух деревьев и сгенерируйте объект различия.
  4. Обновить реальный 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 или немного поможет вам, я очень счастливый.