Одна статья, чтобы получить алгоритм Diff

Vue.js
Одна статья, чтобы получить алгоритм Diff

Подпишитесь на официальный аккаунт «Kite Handler», ответьте «Данные», чтобы получить данные 500G (все «руки»), и профессиональные коммуникационные группы ждут, когда вы соберетесь вместе. (Ха-ха)

Будь то Vue или React, чтобы сравнить изменения виртуальных узлов DOM и добиться минимальных обновлений, все они используют алгоритм сравнения, В этой статье мы рассмотрим алгоритм сравнения с ветеранами.

1. Основы

Алгоритм Diff реализует минимальные обновления виртуального DOM. Хотя это предложение короткое, оно включает в себя два основных элемента: виртуальный DOM, минимальное обновление.

  1. виртуальный DOM

Виртуальный DOM относится к построению реального дерева DOM в виде объекта js, чтобы решить проблему производительности браузера, работающего с реальным DOM.

Например: следующее сопоставление между DOM и виртуальным DOM

image.png

  1. минимальное обновление

Цель Diff — найти наименьшую обновленную часть между старой и новой виртуальной DOM, чтобы обновить DOM, соответствующую этой части.

2. Весь процесс

Алгоритм Diff действительно красив, весь процесс показан на следующем рисунке:

Diff算法3.jpg

1. Сначала сравните, являются ли старый и новый узлы одним и тем же узлом (сравнив, совпадают ли значения sel (селектор) и ключ (уникальный идентификатор), если они не являются одним и тем же узлом, удалите их насильно (Примечание: сначала используйте старый узел в качестве эталона. Вставьте новые узлы перед удалением старых).

2. Если это один и тот же узел, требуется дальнейшее сравнение

  1. точно так же, без лечения
  2. Содержимое нового узла — текст, и замена выполняется напрямую.
  3. Если у нового узла есть дочерние узлы, вы должны внимательно рассмотреть это в это время: если у старого узла нет дочерних элементов, вы можете напрямую очистить старый узел и вставить дочерние элементы нового узла; если у старого узла есть дочерние элементы , нужно следовать вышеизложенному. Стратегия обновления старая (помните стратегию обновления, в нее можно играть несколько лет, 666666).

3. Настоящий бой

Давайте поговорим об основном содержании алгоритма сравнения.

3.1 патч-функция

Функция входа алгоритма Diff в основном оценивает, являются ли старый и новый узлы одним и тем же узлом, а затем назначает их разной логике для обработки.

export default function patch(oldVnode, newVnode) {
    // 判断传入的第一个参数,是DOM节点还是虚拟节点
    if (oldVnode.sel === '' || oldVnode.sel === undefined) {
        // 传入的第一个参数是DOM节点,此时要包装成虚拟节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
    }

    // 判断oldVnode和newVnode是不是同一个节点
    if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
        //是同一个节点,则进行精细化比较
        patchVnode(oldVnode, newVnode);
    }
    else {
        // 不是同一个节点,暴力插入新的,删除旧的
        let newVnodeElm = createElement(newVnode);

        // 将新节点插入到老节点之前
        if (oldVnode.elm.parentNode && newVnodeElm) {
            oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
        }
        // 删除老节点
        oldVnode.elm.parentNode.removeChild(oldVnode.elm);
    }
}

3.2 функция patchVnode

Эта функция в основном отвечает за уточненное сравнение и использует другую логику обработки, определяя, к какой ветви принадлежит в соответствии с логикой на приведенной выше блок-схеме. (Идея ясна, алгоритм слишком хорош)

export default function patchVnode(oldVnode, newVnode) {
    // 判断新旧vnode是否是同一个对象
    if (oldVnode === newVnode) {
        return;
    }
    // 判断vnode有没有text属性
    if (newVnode.text !== undefined && (newVnode.children === undefined || newVnode.children.length === 0)) {
        console.log('新vnode有text属性');
        if (newVnode.text !== oldVnode.text) {
            oldVnode.elm.innerText = newVnode.text;
        }
    }
    else {
        // 新vnode没有text属性,有children
        console.log('新vnode没有text属性');
        // 判断老的有没有children
        if (oldVnode.children !== undefined && oldVnode.children.length > 0) {
            // 老的有children,新的也有children
            updateChildren(oldVnode.elm, oldVnode.children, newVnode.children);
        }
        else {
            // 老的没有children,新的有children
            // 清空老的节点的内容
            oldVnode.elm.innerHTML = '';
            // 遍历新的vnode的子节点,创建DOM,上树
            for (let i = 0; i < newVnode.children.length; i++) {
                let dom = createElement(newVnode.children[i]);
                oldVnode.elm.appendChild(dom);
            }
        }
    }
}

3.3 функция updateChildren

Основная функция в основном отвечает за ситуацию, когда как старый виртуальный узел, так и новый виртуальный узел имеют подэлементы, и сравнивает их по очереди в соответствии со стратегией сравнения и, наконец, обнаруживает измененную часть подэлементов для достижения минимальное обновление. Для этой части задействованы некоторые указатели, а именно:

image.png

  1. Старый фронт относится к указателю головы в виртуальном DOM до обновления.
  2. Старый относится к хвостовому указателю в виртуальном DOM до обновления.
  3. Новый фронт относится к указателю головы в обновленном виртуальном DOM.
  4. Новое относится к хвостовому указателю в обновленном виртуальном DOM.

В соответствии с приведенной выше стратегией обновления процесс обновления старого виртуального DOM до нового виртуального DOM выглядит следующим образом:

  1. Примените стратегию «новый после старого до», затем переместите соответствующий узел DOM (то есть узел 1) за буквой на задний план старого заднего узла (узел 3), затем указатель старого переднего узла переместится вниз, и указатель нового заднего узла перемещается вверх.
  2. По-прежнему используйте стратегию «новое после старого до», сделайте то же самое, переместите узел 2 за старый задний узел (узел 3), переместите старый передний узел вниз и переместите новый задний узел вверх.
  3. Применяйте стратегию «Новый фронт — старый фронт», узел DOM остается неизменным, а старый и новый передние узлы перемещаются вниз.
  4. Выход из цикла заканчивается.
export default function updateChildren(parentElm, oldCh, newCh) {
    // 旧前
    let oldStartIdx = 0;
    // 新前
    let newStartIdx = 0;
    // 旧后
    let oldEndIdx = oldCh.length - 1;
    // 新后
    let newEndIdx = newCh.length - 1;
    // 旧前节点
    let oldStartVnode = oldCh[0];
    // 旧后节点
    let oldEndVnode = oldCh[oldEndIdx];
    // 新前节点
    let newStartVnode = newCh[0];
    // 新后节点
    let newEndVnode = newCh[newEndIdx];

    let keyMap = null;

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        // 略过已经加undefined标记的内容
        if (oldStartVnode == null || oldCh[oldStartIdx] === undefined) {
            oldStartVnode = oldCh[++oldStartIdx];
        }
        else if (oldEndVnode == null || oldCh[oldEndIdx] === undefined) {
            oldEndVnode = oldCh[--oldEndIdx];
        }
        else if (newStartVnode == null || newCh[newStartIdx] === undefined) {
            newStartVnode = newCh[++newStartIdx];
        }
        else if (newEndVnode == null || newCh[newEndIdx] === undefined) {
            newEndVnode = newCh[--newEndIdx];
        }
        else if (checkSameVnode(oldStartVnode, newStartVnode)) {
            // 新前与旧前
            console.log('新前与旧前命中');
            patchVnode(oldStartVnode, newStartVnode);
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];
        }
        else if (checkSameVnode(oldEndVnode, newEndVnode)) {
            // 新后和旧后
            console.log('新后和旧后命中');
            patchVnode(oldEndVnode, newEndVnode);
            oldEndVnode = oldCh[--oldEndIdx];
            newEndVnode = newCh[--newEndVnode];
        }
        else if (checkSameVnode(oldStartVnode, newEndVnode)) {
            console.log('新后和旧前命中');
            patchVnode(oldStartVnode, newEndVnode);
            // 当新后与旧前命中的时候,此时要移动节点,移动新后指向的这个节点到老节点旧后的后面
            parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
        }
        else if (checkSameVnode(oldEndVnode, newStartVnode)) {
            // 新前和旧后
            console.log('新前和旧后命中');
            patchVnode(oldEndVnode, newStartVnode);
            // 当新前和旧后命中的时候,此时要移动节点,移动新前指向的这个节点到老节点旧前的前面
            parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[++newStartIdx];
        }
        else {
            // 四种都没有命中
            // 制作keyMap一个映射对象,这样就不用每次都遍历老对象了
            if (!keyMap) {
                keyMap = {};
                for (let i = oldStartIdx; i <= oldEndIdx; i++) {
                    const key = oldCh[i].key;
                    if (key !== undefined) {
                        keyMap[key] = i;
                    }
                }
            }
            // 寻找当前这项(newStartIdx)在keyMap中的映射的位置序号
            const idxInOld = keyMap[newStartVnode.key];
            if (idxInOld === undefined) {
                // 如果idxInOld是undefined表示踏实全新的项,此时会将该项创建为DOM节点并插入到旧前之前
                parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm);
            }
            else {
                // 如果不是undefined,则不是全新的项,则需要移动
                const elmToMove = oldCh[idxInOld];
                patchVnode(elmToMove, newStartVnode);
                // 把这项设置为undefined,表示已经处理完这项了
                oldCh[idxInOld] = undefined;
                // 移动
                parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
            }
            // 指针下移,只移动新的头
            newStartVnode = newCh[++newStartIdx];
        }
    }

    // 循环结束后,处理未处理的项
    if (newStartIdx <= newEndIdx) {
        console.log('new还有剩余节点没有处理,要加项,把所有剩余的节点插入到oldStartIdx之前');
        // 遍历新的newCh,添加到老的没有处理的之前
        for (let i = newStartIdx; i <= newEndIdx; i++) {
            // insertBefore方法可以自动识别null,如果是null就会自动排到队尾去
            // newCh[i]现在还没有真正的DOM,所以要调用createElement函数变为DOM
            parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm);
        }
    }
    else if (oldStartIdx <= oldEndIdx) {
        console.log('old还有剩余节点没有处理,要删除项');
        // 批量删除oldStart和oldEnd指针之间的项
        for (let i = oldStartIdx; i <= oldEndIdx; i++) {
            if (oldCh[i]) {
                parentElm.removeChild(oldCh[i].elm);
            }
        }
    }
}

использованная литература

Эта статья представляет собой резюме, сделанное автором после просмотра видео г-на Шао Шанхуаня, речь г-на Шао действительно хороша и заслуживает похвалы.

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

2. Подпишитесь на официального владельца учетной записи, получайте учебные материалы (внешние «многорукие» материалы) и регулярно публикуйте для вас оригинальные и подробные полезные статьи.