Если вы не понимаете алгоритм DIff, просто порубите меня (с картинками)

внешний интерфейс алгоритм
Если вы не понимаете алгоритм DIff, просто порубите меня (с картинками)

предисловие

Интервьюер: «Вы понимаете虚拟DOM(Virtual DOM)а такжеDiff算法, пожалуйста, опишите их";

Я: "Эээ...гусь, это", кончено😰, вдруг мой IQ не в сети, я плохо организовала язык, плохо ответила, или вообще не смогла ответить;

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


Связанные точки знаний:

  • Виртуальный DOM (виртуальный DOM):
    • что такое виртуальный дом

    • Зачем использовать виртуальный дом

    • Виртуальная DOM-библиотека

  • РАЗЛИЧНЫЙ алгоритм:
    • Источник snabDom
      • функция инициализации
      • h функция
      • патч функция
      • функция patchVnode
      • функция updateChildren

Виртуальный DOM (Виртуальный DOM)

Что такое виртуальный DOM

В одном предложении виртуальный DOM — это описание реального DOM.JavaScript-объект, этого изображения может быть недостаточно, тогда давайте приведем пример 🌰: используйте код для описания соответственно真实DOMтак же как虚拟DOM

真实DOM:

<ul class="list">
    <li>a</li>
    <li>b</li>
    <li>c</li>
</ul>

对应的虚拟DOM:


let vnode = h('ul.list', [
  h('li','a'),
  h('li','b'),
  h('li','c'),
])

console.log(vnode)

консольная печатьVnode:

image.png

Исходный код объекта JS (Vnode) виртуального DOM, сгенерированного функцией h:

export interface VNodeData {
    props?: Props
    attrs?: Attrs
    class?: Classes
    style?: VNodeStyle
    dataset?: Dataset
    on?: On
    hero?: Hero
    attachData?: AttachData
    hook?: Hooks
    key?: Key
    ns?: string // for SVGs
    fn?: () => VNode // for thunks
    args?: any[] // for thunks
    [key: string]: any // for any other 3rd party module
}

export type Key = string | number

const interface VNode = {
    sel: string | undefined, // 选择器
    data: VNodeData | undefined, // VNodeData上面定义的VNodeData
    children: Array<VNode | string> | undefined, //子节点,与text互斥
    text: string | undefined, // 标签中间的文本内容
    elm: Node | undefined, // 转换而成的真实DOM
    key: Key | undefined // 字符串或者数字
}

Пополнить:

Вышеупомянутая функция h может быть всем немного знакома, но я долго ее не запоминал, не беда, я помогу вам вспомнить;开发中常见的现实场景,render函数渲染:

// 案例1 vue项目中的main.js的创建vue实例
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");

//案例2 列表中使用render渲染
columns: [
    {
        title: "操作",
        key: "action",
        width: 150,
        render: (h, params) => {
            return h('div', [
                h('Button', {
                    props: {
                        size: 'small'
                    },
                    style: {
                        marginRight: '5px',
                        marginBottom: '5px',
                    },
                    on: {
                        click: () => {
                            this.toEdit(params.row.uuid);
                        }
                    }
                }, '编辑')
            ]);
        }
    }
]

Зачем использовать виртуальный DOM

  • Фреймворк MVVM решает проблему синхронизации представления и состояния
  • Механизм шаблонов может упростить работу с представлением, нет возможности отслеживать состояние
  • Виртуальный DOM отслеживает изменения состояния
  • Обратитесь к гитхабуvirtual-domописание мотивации
    • Виртуальный DOM может поддерживать состояние программы и отслеживать последнее состояние.
    • Обновите настоящий DOM, сравнив разницу между двумя состояниями до и после.
  • Использование на разных платформах
    • Рендеринг DOM платформы браузера
    • Рендеринг SSR на стороне сервера (Nuxt.js/Next.js), внешний интерфейс — направление vue, последний — направление реакции
    • Нативное приложение (Weex/React Native)
    • Небольшие программы (mpvue/uni-app) и т.д.
  • Существует много атрибутов реального DOM, и стоимость создания узлов DOM очень высока.
  • Виртуальный DOM — это обычный объект JavaScript, для описания свойств не требуется много времени, а накладные расходы на создание невелики.
  • Повышение производительности рендеринга в сложных ситуациях просмотра(Производительность операционной высока, и сокращение объема операционной может улучшить производительность)

душа просит: Будет ли использование виртуального DOM быстрее, чем прямой рендеринг реального DOM? Конечно, ответ такой:否定Да, послушай меня:2c3559e204c5aae6a1c6bfdc8557efcd.jpeg

Пример: DOMA->DOMB при изменении узла

image.pngВышеупомянутая ситуация:示例1заключается в созданииDOMBзатем заменитьDOMA; 示例2идти创建虚拟DOM+DIFF算法Сравнение найденоDOMBа такжеDOMAНе тот же узел, и, наконец, создатьDOMBзатем заменитьDOMA; Хорошо видно, что быстрее 1. Для того же результата 2 нужно создать виртуальный DOM+DIFF для сравнения. Таким образом, утверждение о том, что использование виртуального DOM быстрее, чем прямое управление реальным DOM,错误的,不严谨的

Пример: Когда содержимое дочернего узла в дереве DOM изменяется:

image.pngКогда некоторые сложные узлы, такие как родительский узел с несколькими дочерними узлами, когда изменилось только содержимое одного дочернего узла, то нам не нужно быть похожими на示例1перерисовать этоDOM树,В настоящее время虚拟DOM+DIFF算法может быть хорошо представлен示例2использовать虚拟DOM+Diff算法Перейдите, чтобы найти измененный дочерний узел и обновить его содержимое.

Суммировать:Повышение производительности рендеринга в сложных ситуациях просмотра,потому что虚拟DOM+Diff算法Может точно найти место, где меняется DOM-дерево, сократить операции с DOM (переставить и перерисовать)


библиотека виртуального дома

  • Snabbdom
    • Виртуальный DOM, используемый внутри Vue.js2.x, представляет собой модифицированный Snabbdom.
    • Около 200SLOC (одна строка кода)
    • Расширяемый с помощью модулей
    • Исходный код разработан с использованием TypeScript
    • Один из самых быстрых виртуальных DOM
  • virtual-dom

Алгоритм сравнения

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

Первая концепция алгоритма diff заключается в том, что объектом Diff является виртуальный DOM (виртуальный DOM), а обновление реального DOM является результатом алгоритма diff.

я разорву его на частиsnabbdomОсновная часть исходного кода открыта для всехDiffСердце, потерпи, не закрывай страницу, я знаю, вы все такие:

src=http___img.wxcha.com_file_201905_17_f5a4d33d48.jpg&refer=http___img.wxcha.jpeg


ядро снаббдома

  • init()модуль настройки.создатьpatch()функция
  • использоватьh()Функция для создания объекта JavaScript(Vnode)описывать真实DOM
  • patch()Сравнивать新旧两个Vnode
  • обновить изменения до真实DOM树

функция инициализации

Модуль настраивается во время функции инициализации, а затем создается функция patch () Сначала мы используем случай сцены, чтобы иметь интуитивное отражение:

import {init} from 'snabbdom/build/package/init.js'
import {h} from 'snabbdom/build/package/h.js'

// 1.导入模块
import {styleModule} from "snabbdom/build/package/modules/style";
import {eventListenersModule} from "snabbdom/build/package/modules/eventListeners";

// 2.注册模块
const patch = init([
  styleModule,
  eventListenersModule
])

// 3.使用h()函数的第二个参数传入模块中使用的数据(对象)
let vnode = h('div', [
  h('h1', {style: {backgroundColor: 'red'}}, 'Hello world'),
  h('p', {on: {click: eventHandler}}, 'Hello P')
])

function eventHandler() {
  alert('疼,别摸我')
}

const app = document.querySelector('#app')

patch(app,vnode)

Когда init использует импортированные модули, его можно создать в функции h с помощью API, предоставляемого этими модулями.虚拟DOM(Vnode)对象; использовано выше样式模块так же как事件模块Пусть созданный виртуальный DOM имеет атрибуты стиля и атрибуты событий и, наконец, проходитpatch函数В сравнении两个虚拟dom(Сначала приложение будет преобразовано в виртуальный дом), и представление будет обновлено;

image.png

Давайте кратко рассмотрим часть исходного кода init:

// src/package/init.ts
/* 第一参数就是各个模块
   第二参数就是DOMAPI,可以把DOM转换成别的平台的API,
也就是说支持跨平台使用,当不传的时候默认是htmlDOMApi,见下文
   init是一个高阶函数,一个函数返回另外一个函数,可以缓存modules,与domApi两个参数,
那么以后直接只传oldValue跟newValue(vnode)就可以了*/
export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {

...

return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {}
}

h функция

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

// h函数
export function h (sel: string): VNode
export function h (sel: string, data: VNodeData | null): VNode
export function h (sel: string, children: VNodeChildren): VNode
export function h (sel: string, data: VNodeData | null, children: VNodeChildren): VNode
export function h (sel: any, b?: any, c?: any): VNode {
  var data: VNodeData = {}
  var children: any
  var text: any
  var i: number
    ...
  return vnode(sel, data, children, text, undefined) //最终返回一个vnode函数
};
// vnode函数
export function vnode (sel: string | undefined,
  data: any | undefined,
  children: Array<VNode | string> | undefined,
  text: string | undefined,
  elm: Element | Text | undefined): VNode {
  const key = data === undefined ? undefined : data.key
  return { sel, data, children, text, elm, key } //最终生成Vnode对象
}

Суммировать:h函数сначала создайтеvnodeфункция, тоvnodeфункция для созданияVnodeобъект (виртуальный объект DOM)

Пополнить:

В части исходного кода функции h函数重载Концепция, кратко поясню:

  • Функции с разным количеством или типами параметров ()
  • В JavaScript нет концепции перегрузки
  • В TypeScript есть перегрузки, но реализация перегрузки по-прежнему настраивает параметры через код

Концепция перегрузки связана с параметрами и не имеет ничего общего с возвращаемыми значениями.

  • Пример 1 (перегрузка функции - количество параметров)

function add(a:number,b:number){

console.log(a+b)

}

function add(a:number,b:number,c:number){

console.log(a+b+c)

}

add(1,2)

add(1,2,3)

  • Пример 2 (перегрузка функции - тип параметра)

function add(a:number,b:number){

console.log(a+b)

}

function add(a:number,b:string){

console.log(a+b)

}

add(1,2)

add(1,'2')


функция исправления (ядро)

src=http___shp.qpic.cn_qqvideo_ori_0_e3012t7v643_496_280_0&refer=http___shp.qpic.jpeg

Если вы читали предыдущее предзнаменование, вы можете отвлечься, увидев это,醒醒啊,这是核心啊,上高地了兄弟;

  • pactch(oldVnode,newVnode)
  • Отобразите измененное содержимое в новом узле в реальном DOM и, наконец, верните новый узел в качестве старого узла (ядра) для следующей обработки.
  • Сравните старое и новоеVNodeЭто один и тот же узел (ключ и sel узла одинаковы)
  • Если это не тот же узел, удалите предыдущий контент и повторите рендеринг.
  • Если это тот же узел, то судить о новомVNodeЗдесьtext, если есть и иoldVnodeизtextРазличное непосредственное обновление текстового содержимого(patchVnode)
  • Если у нового VNode есть дочерние элементы, определите, изменился ли дочерний узел.(updateChildren,最麻烦,最难实现)

Исходный код:

return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {    
    let i: number, elm: Node, parent: Node
    const insertedVnodeQueue: VNodeQueue = []
    // cbs.pre就是所有模块的pre钩子函数集合
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
    // isVnode函数时判断oldVnode是否是一个虚拟DOM对象
    if (!isVnode(oldVnode)) {
        // 若不是即把Element转换成一个虚拟DOM对象
        oldVnode = emptyNodeAt(oldVnode)
    }
    // sameVnode函数用于判断两个虚拟DOM是否是相同的,源码见补充1;
    if (sameVnode(oldVnode, vnode)) {
        // 相同则运行patchVnode对比两个节点,关于patchVnode后面会重点说明(核心)
        patchVnode(oldVnode, vnode, insertedVnodeQueue)
    } else {
        elm = oldVnode.elm! // !是ts的一种写法代码oldVnode.elm肯定有值
        // parentNode就是获取父元素
        parent = api.parentNode(elm) as Node

        // createElm是用于创建一个dom元素插入到vnode中(新的虚拟DOM)
        createElm(vnode, insertedVnodeQueue)

        if (parent !== null) {
            // 把dom元素插入到父元素中,并且把旧的dom删除
            api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))// 把新创建的元素放在旧的dom后面
            removeVnodes(parent, [oldVnode], 0, 0)
        }
    }

    for (i = 0; i < insertedVnodeQueue.length; ++i) {
        insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
    }
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
    return vnode
}

Дополнение 1: функция sameVnode

function sameVnode(vnode1: VNode, vnode2: VNode): boolean { 通过key和sel选择器判断是否是相同节点
    return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel
}

patchVnode

  • триггер первой ступениprepatchфункция иupdateфункция (оба запускают функцию предварительного исправления, два не совсем одинаковых запускают функцию обновления)
  • Второй этап, реальное сравнение старого и новогоvnodeгде разница
  • Третий этап, триггерpostpatchузел обновления функции

Исходный код:

function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    const hook = vnode.data?.hook
    hook?.prepatch?.(oldVnode, vnode)
    const elm = vnode.elm = oldVnode.elm!
    const oldCh = oldVnode.children as VNode[]
    const ch = vnode.children as VNode[]
    if (oldVnode === vnode) return
    if (vnode.data !== undefined) {
        for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
        vnode.data.hook?.update?.(oldVnode, vnode)
    }
    if (isUndef(vnode.text)) { // 新节点的text属性是undefined
        if (isDef(oldCh) && isDef(ch)) { // 当新旧节点都存在子节点
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue) //并且他们的子节点不相同执行updateChildren函数,后续会重点说明(核心)
        } else if (isDef(ch)) { // 只有新节点有子节点
            // 当旧节点有text属性就会把''赋予给真实dom的text属性
            if (isDef(oldVnode.text)) api.setTextContent(elm, '') 
            // 并且把新节点的所有子节点插入到真实dom中
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
        } else if (isDef(oldCh)) { // 清除真实dom的所有子节点
            removeVnodes(elm, oldCh, 0, oldCh.length - 1)
        } else if (isDef(oldVnode.text)) { // 把''赋予给真实dom的text属性
            api.setTextContent(elm, '')
        }
    } else if (oldVnode.text !== vnode.text) { //若旧节点的text与新节点的text不相同
        if (isDef(oldCh)) { // 若旧节点有子节点,就把所有的子节点删除
            removeVnodes(elm, oldCh, 0, oldCh.length - 1)
        }
        api.setTextContent(elm, vnode.text!) // 把新节点的text赋予给真实dom
    }
    hook?.postpatch?.(oldVnode, vnode) // 更新视图
}

Это может немного сбивать с толку, вот еще одна карта разума:

image.png


Отступление: введение в алгоритм Diff

Традиционный алгоритм сравнения

  • Алгоритм сравнения в виртуальном DOM
  • 传统算法Найдите разницу между каждым узлом двух деревьев
  • Он запустит n1 (количество узлов dom1) * n2 (количество узлов dom2), чтобы сравнить, найти разницу и затем обновить ее.

image.png

Оптимизация алгоритма сравнения snabdom

  • Согласно характеристикам DOM, Snbbdom сделал традиционный алгоритм сравнения优化
  • Узлы редко манипулируются на разных уровнях во время манипулирования DOM.
  • сравнить только同级别узел

image.png

src=http___img.wxcha.com_file_202004_03_1ed2e19e4f.jpg&refer=http___img.wxcha.jpeg

Ниже мы представимupdateChildrenКак функция сравнивает дочерние узлы异同,СлишкомDiff算法Ядро и сложная точка внутри;


updateChildren (ядро в ядре: оценка разницы между дочерними узлами)

  • Эта функция делится на три части,部分1:声明变量,部分2:同级别节点比较,部分3:循环结束的收尾工作(Смотри ниже);

image.png

  • 同级别节点比较из五种условие:
    1. oldStartVnode/newStartVnode(старый начальный узел/новый начальный узел) то же самое
    2. oldEndVnode/newEndVnode(старый конечный узел/новый конечный узел) то же самое
    3. oldStartVnode/newEndVnode(старый начальный узел/новый конечный узел) то же самое
    4. oldEndVnode/newStartVnode(старый конечный узел/новый начальный узел) то же самое
    5. 特殊情况当1,2,3,4的情况都不符合будет выполнен, когдаoldVnodesнайти внутриnewStartVnodeтот же узел, а затем перешел кoldStartVnode, если не найдено, тоoldStartVnodeСоздавать
  • Процесс выполнения представляет собой цикл.В каждом цикле, пока выполняется один из пяти вышеперечисленных случаев, цикл завершается.
  • 循环结束的收尾工作: до тех пор, пока oldStartIdx>oldEndIdx || newStartIdx>newEndIdx (представляющий старый узел или новый узел не был пройден)

Для более интуитивного понимания рассмотрим同级别节点比较из五种Детали реализации для случая:

Новый начальный узел и старый начальный узел (вариант 1)

image.png

  • подобно情况1符合:(从新旧节点的开始节点开始对比,oldCh[oldStartIdx]和newCh[newStartIdx]провестиsameVnode(key和sel相同)Судя по тому, один ли это узел)
  • затем выполнитьpatchVnodeНайдите разницу между ними и обновите график; если разницы нет, ничего не делайте и завершите цикл
  • oldStartIdx++/newStartIdx++

новый конечный узел и старый конечный узел (вариант 2)

image.png

  • подобно情况1不符合просто судить情况2, если он совпадает: (Сравните от конечного узла старого и нового узлов,oldCh[oldEndIdx]和newCh[newEndIdx]сравнить, выполнитьsameVnode(key和sel相同)Судя по тому, один ли это узел)
  • воплощать в жизньpatchVnodeНайдите разницу между ними, обновите представление; если разницы нет, ничего не делайте, завершите цикл
  • oldEndIdx--/newEndIdx--

старый начальный узел/новый конечный узел (вариант 3)

image.png

  • подобно情况1,2Если он не совпадает, он попробует случай 3: (начальный узел старого узла сравнивается с конечным узлом нового узла,oldCh[oldStartIdx]和newCh[newEndIdx]сравнить, выполнитьsameVnode(key和sel相同)Судя по тому, один ли это узел)
  • воплощать в жизньpatchVnodeНайдите разницу между ними, обновите вид, если разницы нет, ничего не делайте, завершите цикл
  • oldCh[oldStartIdx]对应的真实domперейти кoldCh[oldEndIdx]对应的真实domназад
  • oldStartIdx++/newEndIdx--;

старый конечный узел/новый начальный узел (вариант 4)

image.png

  • подобно情况1,2,3Не встречаются, мы постараемся запустить узел конечный узел и новый узел Case 4 :( Старый узел начинается контраст,oldCh[oldEndIdx]和newCh[newStartIdx]сравнить, выполнитьsameVnode(key和sel相同)Судя по тому, один ли это узел)
  • воплощать в жизньpatchVnodeНайдите разницу между ними, обновите вид, если разницы нет, ничего не делайте, завершите цикл
  • oldCh[oldEndIdx]对应的真实domперейти кoldCh[oldStartIdx]对应的真实domвперед
  • oldEndIdx--/newStartIdx++;

Найти узел в массиве новых начальных узлов/старых узлов (случай 5)

image.png

  • Ищите со старого узла, если найдете такой жеnewCh[newStartIdx]Тот же узел (и называемый对应节点[1]),воплощать в жизньpatchVnodeНайдите разницу между ними, обновите вид, если разницы нет, ничего не делайте, завершите цикл
  • 对应节点[1]对应的真实domперейти кoldCh[oldStartIdx]对应的真实domвперед

image.png

  • 若没有寻找到相同的节点, затем создайтеnewCh[newStartIdx]соответствующий узлу真实domвставить вoldCh[oldStartIdx]对应的真实domвперед
  • newStartIdx++

379426071b8130075b11ba142f9468e2.jpeg


Далее мы вводим结束循环последние штрихи(oldStartIdx>oldEndIdx || newStartIdx>newEndIdx):

image.png

  • 新节点的所有子节点Первый переход (newStartIdx>newEndIdx), цикл заканчивается
  • 新节点的所有子节点Конец обхода - поставить узлы, не соответствующие одному и тому же узлу子节点удалять

image.png

  • 旧节点的所有子节点Первый переход (oldStartIdx>oldEndIdx), цикл заканчивается
  • 旧节点的所有子节点В конце обхода есть еще子节点вставить в旧节点结束节点раньше; (источник:newCh[newEndIdx + 1].elm), что соответствует旧结束节点的真实dom, NewEndIdx + 1 совпадает с тем, что совпавший узел должен быть -1, необходимо добавить внутренний узел

Наконец, прикрепите исходный код:

function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
    let oldStartIdx = 0;                // 旧节点开始节点索引
    let 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) {
            oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
        }
        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)) { // 判断情况1
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];
        }
        else if (sameVnode(oldEndVnode, newEndVnode)) {   // 情况2
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
            oldEndVnode = oldCh[--oldEndIdx];
            newEndVnode = newCh[--newEndIdx];
        }
        else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right情况3
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
            api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm));
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
        }
        else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left情况4
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
            api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[++newStartIdx];
        }
        else {                                             // 情况5
            if (oldKeyToIdx === undefined) {
                oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
            }
            idxInOld = oldKeyToIdx[newStartVnode.key];
            if (isUndef(idxInOld)) { // New element        // 创建新的节点在旧节点的新节点前
                api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
            }
            else {
                elmToMove = oldCh[idxInOld];
                if (elmToMove.sel !== newStartVnode.sel) { // 创建新的节点在旧节点的新节点前
                    api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
                }
                else {
                                                           // 在旧节点数组中找到相同的节点就对比差异更新视图,然后移动位置
                    patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
                    oldCh[idxInOld] = undefined;
                    api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm);
                }
            }
            newStartVnode = newCh[++newStartIdx];
        }
    }
    // 循环结束的收尾工作
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
        if (oldStartIdx > oldEndIdx) {
            // newCh[newEndIdx + 1].elm就是旧节点数组中的结束节点对应的dom元素
            // newEndIdx+1是因为在之前成功匹配了newEndIdx需要-1
            // newCh[newEndIdx + 1].elm,因为已经匹配过有相同的节点了,它就是等于旧节点数组中的结束节点对应的dom元素(oldCh[oldEndIdx + 1].elm)
            before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
            // 把新节点数组中多出来的节点插入到before前
            addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
        }
        else {
            // 这里就是把没有匹配到相同节点的节点删除掉
            removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
        }
    }
}

Роль ключа

  • Дифференциальная операция может更加快速;
  • Операция Diff может быть более точной;(避免渲染错误)
  • 不推荐使用索引как ключ

Ниже мы рассмотрим примеры таких действий:

Операция Diff может быть более точной;(避免渲染错误):

Пример: вставьте элемент z между b и c в три элемента dom a, b и c

ключ не установленimage.pngКогда ключ установлен:

image.png

Операция Diff может быть более точной;(避免渲染错误)

Пример: a, b, c три элемента dom, измените атрибут элемента a, а затем добавьте элемент z перед элементом a.

Ключ не установлен:

image.png

image.png

Поскольку ключ не установлен, значение по умолчанию не определено, поэтому узлы все одинаковые, содержимое текста обновляется, но по-прежнему используется предыдущий dom, поэтому на самом делеa->z(a原本打勾的状态保留了,只改变了text),b->a,c->b,d->c, после обхода выясняется, что нужно добавить dom, а в конце добавляется dom элемент с текстом d

ключ установлен:

image.png

image.png

Когда ключ установлен, a, b, c, d имеют соответствующие ключи,a->a,b->b,c->c,d->d, тот же контент обновлять не нужно, обход заканчивается, и добавляется элемент dom с текстом z

不推荐使用索引как ключ:

Установить индекс как ключ:

image.png

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

image.png


наконец

Если описание неверно или неясно, свяжитесь со мной в комментариях ниже, я немедленно обновлю его, если у вас есть какие-либо достижения, пожалуйста, поставьте мне лайк👍 Это отличная поддержка для меня, спасибо