Виртуальный DOM и алгоритм сравнения в Vue

Vue.js

виртуальный дом

  • Почему появляется:

    Браузерный анализ html примерно разделен на пять шагов: создать дерево DOM -> создать правила стиля -> построить дерево рендеринга -> макет макета -> нарисовать рисование. Каждый раз, когда работает настоящий DOM, браузер будет выполнять процесс от начала до конца, начиная с построения дерева DOM. Настоящая операция DOM стоит дорого, а частые операции также приведут к зависанию страницы и повлияют на взаимодействие с пользователем.Виртуальный дом был создан для решения этой проблемы с производительностью браузера.

    После того, как виртуальный дом выполняет операцию обновления дома, виртуальный дом не работает напрямую с реальным домом, а сохраняет обновленное содержимое diff в локальном объекте js, а затем одновременно прикрепляет его к дереву дома, уведомляя браузер для рисования дом, чтобы избежать большого количества бесполезных вычислений.

  • Как достичь:

    Объект js представляет структуру dom, а объект записывает метки, атрибуты и дочерние узлы узла dom.

    Функция рендеринга объекта js строит реальное дерево дома, рекурсивно используя свойства и дочерние узлы виртуального дома.

    Виртуальный DOM — это чистый JS-объект, к которому можно получить доступ черезdocument.createDocumentFragmentСоздайте, виртуальный DOM в Vue содержит следующие свойства:

    • tag: имя тега текущего узла
    • data: объект данных текущего узла
    • Children: тип массива, содержит дочерние узлы текущего узла
    • текст: текст текущего узла, общие текстовые узлы или узлы комментариев будут иметь этот атрибут
    • elm: реальный узел dom, соответствующий текущему виртуальному узлу
    • контекст: область компиляции
    • functionContext: область действия функционального компонента.
    • ключ: ключевой атрибут узла, который используется в качестве идентификатора узла, что способствует оптимизации патча.
    • SEL: селектор для узлов
    • componentOptions: информация об опциях, которая будет использоваться при создании экземпляра компонента.
    • дочерний элемент: экземпляр компонента, соответствующий текущему узлу
    • родитель: узел-заполнитель компонента
    • raw: raw html
    • isStatic: идентификатор статического узла
    • isRootInsert: следует ли вставлять в качестве корневого узла обернутый узел, значение этого свойства равно false
    • isComment: является ли текущий узел узлом комментариев
    • isCloned: является ли текущий узел клонированным узлом
    • isOnce: есть ли у текущего узла команда v-once

Краткое резюме: Virtual DOM имитирует настоящие узлы DOM с помощью JavaScript и помещает сравнение изменений DOM в слой Js.

  • Преимущество:

    • Кроссплатформенность: виртуальный DOM основан на объектах JavaScript и не зависит от среды реальной платформы, поэтому он имеет кроссплатформенные возможности, такие как платформы браузеров, Weex, Node и т. д.
    • Повышение эффективности операций DOM: скорость выполнения операций DOM намного меньше, чем у Javascript, поэтому большое количество операций DOM переносится на Javascript, а алгоритм исправления используется для расчета узлов, которые действительно нуждаются в обновлении, поэтому чтобы минимизировать операции DOM, тем самым значительно улучшив производительность.
    • Повышение производительности рендеринга: благодаря алгоритму сравнения представление может обновляться разумно и эффективно при больших и частых обновлениях данных.

Процесс преобразования шаблона в вид в vue

  • Vue.js преобразует шаблон шаблона в функцию рендеринга (рендеринга) путем компиляции, а выполнение функции рендеринга может получить виртуальное дерево узлов
  • Когда модель работает, объект Watcher в соответствующем Dep будет активирован. Объект Watcher вызовет соответствующее обновление для изменения представления. Этот процесс в основном предназначен для сравнения разницы между старыми и новыми виртуальными узлами (исправления), а затем выполнения операций DOM для обновления представления в соответствии с результатом сравнения.

Алгоритм diff — это метод оптимизации, в котором сравнивается разница между двумя модулями до и после, а процесс исправления (обновления) разницы называется исправлением.

patch:

Основная часть виртуального DOM, он может преобразовать vnode в реальный DOM.Сравните различия между старыми и новыми виртуальными узлами, а затем найдите узлы, которые необходимо обновить, по результатам сравнения..

Патч сам по себе означает исправление и исправление, и его фактическая функция состоит в том, чтобы изменить существующий DOM для достижения цели обновления представления. Алгоритм исправления Vue Virtual DOM основан наSnabbdomреализации, и внес множество корректировок и улучшений в эти основы.

процесс сравнения

Когда данные изменятся, метод set вызоветDep.notifyОповестить всех подписчиков Watcher, абоненты будут звонитьpatchИсправьте настоящий DOM и обновите соответствующие представления.

Алгоритм сравнения VueРазличайте только vnodes одного уровня, рекурсивно выполняйте diff vnodes одного уровня и, наконец, реализуйте обновление всего дерева DOM.. Поскольку операций в иерархии очень мало и ими можно пренебречь, временная сложность изменяется с O(n3) до O(n).

Предположения алгоритма сравнения

  • Перемещение узлов DOM по иерархиям в веб-интерфейсе происходит очень редко и может быть проигнорировано.
  • Два компонента с одним и тем же классом будут генерировать похожие древовидные структуры, а два компонента с разными классами будут генерировать разные древовидные структуры.
  • Для группы дочерних узлов одного уровня их можно отличить по уникальному идентификатору.

процесс исправления

Когда ключ и sel старого и нового виртуальных узлов совпадают, выполняется патч глубины узла, если они не совпадают, заменяется весь виртуальный узел и одновременно создается реальный DOM для реализовать обновление представления.

Как определить, являются ли старый и новый узлы одним и тем же узлом:

Когда тег, ключ и isComment двух VNodes одинаковы, а данные определены или не определены одновременно, и если тег является входным, тип должен быть одинаковым. В это время два VNode рассматриваются как один и тот же Vnode, и операция patchVnode может выполняться напрямую.

function patch (oldVnode, vnode) {
    if (sameVnode(oldVnode, vnode)) { // 有必要进行patch, key和sel都相同时才进行patch
        patchVnode(oldVnode, vnode)
    } else {  // 没有必要进行patch, 整个替换
        const oEl = oldVnode.el
        let parentEle = api.parentNode(oEl)
        createEle(vnode) // vnode创建它的真实dom,令vnode.el =真实dom
        if (parentEle !== null) {
            api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 插入整个新节点树
            api.removeChild(parentEle, oldVnode.el) // 移出整个旧的虚拟DOM
            oldVnode = null
        }
    }
    return vnode
}

Глубокий патч:

 function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    /*两个VNode节点相同则直接返回*/
    if (oldVnode === vnode) {
      return
    }
    // reuse element for static trees.
    // note we only do this if the vnode is cloned -
    // if the new node is not cloned it means the render functions have been
    // reset by the hot-reload-api and we need to do a proper re-render.
    /*
      如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),
      并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),
      那么只需要替换elm以及componentInstance即可。
    */
    if (isTrue(vnode.isStatic) &&
        isTrue(oldVnode.isStatic) &&
        vnode.key === oldVnode.key &&
        (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {
      vnode.elm = oldVnode.elm
      vnode.componentInstance = oldVnode.componentInstance
      return
    }
    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      /*i = data.hook.prepatch,如果存在的话,见"./create-component componentVNodeHooks"。*/
      i(oldVnode, vnode)
    }
    const elm = vnode.elm = oldVnode.elm
    const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
      /*调用update回调以及update钩子*/
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    /*如果这个VNode节点没有text文本时*/
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        /*新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren*/
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        /*如果老节点没有子节点而新节点存在子节点,先清空elm的文本内容,然后为当前节点加入子节点*/
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        /*当新节点没有子节点而老节点有子节点的时候,则移除所有ele的子节点*/
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        /*当新老节点都无子节点的时候,只是文本的替换,因为这个逻辑中新节点text不存在,所以直接去除ele的文本*/
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      /*当新老节点text不一样时,直接替换这段文本*/
      nodeOps.setTextContent(elm, vnode.text)
    }
    /*调用postpatch钩子*/
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

Правила для patchVnode

1. Если старый и новый VNodes являются статическими, а их ключи одинаковыми (представляющими один и тот же узел), то необходимо заменить только elm и componentInstance (повторное использование на месте).

2. Если у старого и нового узлов есть дочерние узлы, и они разные, выполните операцию сравнения над дочерними узлами и вызовите updateChildren, этоupdateChildren также является ядром diff.

3. Если только новый узел имеет дочерние узлы, сначала очистите текстовое содержимое DOM старого узла, а затем добавьте дочерние узлы к текущему узлу DOM.

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

5. Когда старый и новый узлы не имеют дочерних узлов, это просто замена текста.

updateChildren

Далее идет понимание самого сложного алгоритма diff

updateChildren (parentElm, oldCh, newCh) {
    let oldStartIdx = 0, 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
    let idxInOld
    let elmToMove
    let before
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        if (oldStartVnode == null) {   // 对于vnode.key的比较,会把oldVnode = null
            oldStartVnode = oldCh[++oldStartIdx] 
        }else if (oldEndVnode == null) {
            oldEndVnode = oldCh[--oldEndIdx]
        }else if (newStartVnode == null) {
            newStartVnode = newCh[++newStartIdx]
        }else if (newEndVnode == null) {
            newEndVnode = newCh[--newEndIdx]
        }else if (sameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
        }else if (sameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
        }else if (sameVnode(oldStartVnode, newEndVnode)) {
            patchVnode(oldStartVnode, newEndVnode)
            api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
        }else if (sameVnode(oldEndVnode, newStartVnode)) {
            patchVnode(oldEndVnode, newStartVnode)
            api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        }else {
           // 使用key时的比较
            if (oldKeyToIdx === undefined) {
                oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
            }
            idxInOld = oldKeyToIdx[newStartVnode.key]
            if (!idxInOld) {
                api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
                newStartVnode = newCh[++newStartIdx]
            }
            else {
                elmToMove = oldCh[idxInOld]
                if (elmToMove.sel !== newStartVnode.sel) {
                    api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
                }else {
                    patchVnode(elmToMove, newStartVnode)
                    oldCh[idxInOld] = null
                    api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
                }
                newStartVnode = newCh[++newStartIdx]
            }
        }
    }
    if (oldStartIdx > oldEndIdx) {
        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
    }else if (newStartIdx > newEndIdx) {
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
}

Обзор процесса

  • БудуVnodeУзел ребенкаVchа такжеoldVnodeдочерний узелoldChизвлечен
  • oldChа такжеvChПеременные с двумя головками и хвостамиStartIdxа такжеEndIdx, их две переменные сравниваются друг с другом, всего существует 4 метода сравнения, когда два из них могут совпасть, соответствующий узел в реальном доме будет перемещен в соответствующую позицию Vnode. Если ни одно из 4 сравнений не совпадает, если установленоkey, буду использоватьkeyСравните, в процессе сравнения переменная окажется посередине, один разStartIdx>EndIdxпоказыватьoldChа такжеvChПо крайней мере один из них был пройден, и сравнение заканчивается.

Слева и справа от нового и старого узлов VNode есть переменная метка, и эти переменные будут перемещаться ближе к середине в процессе обхода. Завершите цикл, когда oldStartIdx

Давайте разберем весь процесс сравнения на примере:

реальные узлы: а, б, г

старые узлы: а, б, г

новые узлы: а, в, г, б

первый шаг:

oldS = a, oldE = d;
S = a, E = b;

Сравните старое S с S, E; сравните старое E с S, E, получитеoldSа такжеSВ соответствии с выводом, узел должен быть помещен первым в порядке новых узлов. В это время узел a старого узла также является первым, поэтому положение не меняется;

В конце первого раунда сравнения oldS и S являются одним и тем же узлом, перемещаются назад, а oldE и E не перемещаются;

второй шаг:

старые узлы: а, б, г

новые узлы: а, в, г, б

oldS = b, oldE = d;
S = c, E = b;

Четыре переменные можно сравнить с двумя транспортными средствамиoldSа такжеEMatch, переместите исходный узел b в конец, потому чтоEявляется последним узлом, их позиции должны быть одинаковыми, об этом сказано выше:Когда два из них могут совпасть, соответствующий узел в реальном доме будет перемещен в соответствующую позицию Vnode.;

Второй раунд сравнения окончен, oldE и E — один и тот же узел, продвигаемся вперед, а oldS и S остаются без изменений;

третий шаг:

старые узлы: а, г, б

новые узлы: а, в, г, б

oldS = d, oldE = d;
S = c, E = d;

oldEа такжеEсовпадение, положение без изменений;

четвертый шаг:

старые узлы: а, г, б

новые узлы: а, в, г, б

oldS++;
oldE--;
oldS > oldE;

Обход завершен, что указывает на то, что старый узел был пройден первым. Вставьте оставшийся новый узел c в реальный дом в соответствии с его собственным индексом.

старые узлы: а, в, г, б

новые узлы: а, в, г, б

Сравнение завершено.

Конечно, будетЧетыре переменные не могут соответствовать друг другу, в двух случаях

  • Если старые и новые дочерние узлыключ существует, то будет сгенерирована хеш-таблица на основе ключа старого узла, используяSСопоставьте ключ с хеш-таблицей и оцените успешность совпадения.Sи является ли соответствующий узелsameNode, если это так, переместите успешный узел вперед в реальном доме, в противном случае переместите успешный узел впередSСоздайте соответствующий узел и вставьте его в соответствующий домoldSМесто нахождения,oldSа такжеSУказатель перемещается в середину.
  • Если нет ключа, напрямуюSСоздать новую вставку узла真实DOM(Здесь можно объяснить, почему установка ключа делает diff более эффективным

В конце есть два конкретных случая:

  1. oldS > oldE, можно считать, что старый узел пройден первым. Конечно, также возможно, что новый узел только что завершил обход в это время, и он весь относится к этой категории. В это время добавляется vnode между S и E, вызовите addVnodes и вставьте все эти виртуальные node.elm в конец предыдущего.

  2. S> EЭто можно считать новым узлом, впервые пройденным. В это время узел между старыми и Олд в новом узере дочернего ребенка еще не существует, удалить

Опыт моделирования двух примеров

eg.1
O b,a,d,f,e
N a,b,e
1. 
oldS = b, oldE = e;
S = a, E = e;
O b,a,d,f,e
N a,b,e
2.
oldS = b, oldE = f;
S = a, E = b;
O a,d,f,b,e
N a,b,e
3.
s>e d,f 删除
O a,b,e
N a,b,e

eg.2
O b,d,c,a
N a,e,b,f
1. 
oldS = b, oldE = a;
S = a, E = f;
O a,b,d,c
N a,e,b,f
2.
oldS = b, oldE = c;
S = e, E = f;
此时四个参数无法匹配,根据key来对比O中是否有S对应的节点,没有,则在O的S位置插入对应节点
O a,e,b,d,c
N a,e,b,f
3.
oldS = b, oldE = c;
S = b, E = f;
olds与s对应,原地不动
O a,e,b,d,c
N a,e,b,f
4.
oldS = d, oldE = c;
S = f, E = f;
此时四个参数无法匹配,根据key查找是否有S对应的f节点,没有,则在O的S位置插入对应节点
O a,e,b,d,c,f
N a,e,b,f

5.
oldS = d, oldE = c;
s>f
循环结束,oldS与oldE之间的节点删除



Суммировать:

  • Старайтесь не изменять дом на разных уровнях
  • При разработке компонентов поддержание стабильной структуры DOM поможет повысить производительность.
  • Ключ настройки может сделать diff более эффективным

Ссылаться на:

Подробно объясните виртуальный DOM в Vue.

Виртуальный DOM и различия (реализовано Vue)

Введение в виртуальный DOM

Обновление данных для просмотра обновления, что делает Vue

Подробно объясните алгоритм сравнения vue.