Я позволяю алгоритму сравнения виртуального DOM двигаться

Я написал статью в прошлом году и написал от руки виртуальную библиотеку DOM, чтобы вы могли полностью понять алгоритм сравнения и представить виртуальный DOMпроцесс patchи процесс алгоритма.В то время использовался diffдвухсторонний алгоритм.В этом году я увидел, что был использован быстрый алгоритм ,поэтому я тоже хотел написать статью.Позвольте мне ее записать,но кто-то наверняка уже написал ее,поэтому мне было интересно,может ли она быть немного другой.Последняя статья в основном показывала каждую ситуацию и процесс Алгоритм шаг за шагом рисуя картинки, поэтому мне было интересно, можно ли его изменить в виде анимации, поэтому есть эта статья. Конечно, текущая реализация по-прежнему основана на двустороннем алгоритме, а быстрый алгоритм будет добавлен позже .diffVue3diffdiffdiffdiff

Portal: анимационная демонстрация двустороннего алгоритма Diff .

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

Следует отметить, что эта анимация включает только diffпроцесс алгоритма, а не patchсам процесс.

Давайте сначала рассмотрим diffфункцию двустороннего алгоритма:

const diff = (el, oldChildren, newChildren) => {
    
    
  // 指针
  let oldStartIdx = 0
  let oldEndIdx = oldChildren.length - 1
  let newStartIdx = 0
  let newEndIdx = newChildren.length - 1
  // 节点
  let oldStartVNode = oldChildren[oldStartIdx]
  let oldEndVNode = oldChildren[oldEndIdx]
  let newStartVNode = newChildren[newStartIdx]
  let newEndVNode = newChildren[newEndIdx]
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    
    
    if (oldStartVNode === null) {
    
    
      oldStartVNode = oldChildren[++oldStartIdx]
    } else if (oldEndVNode === null) {
    
    
      oldEndVNode = oldChildren[--oldEndIdx]
    } else if (newStartVNode === null) {
    
    
      newStartVNode = oldChildren[++newStartIdx]
    } else if (newEndVNode === null) {
    
    
      newEndVNode = oldChildren[--newEndIdx]
    } else if (isSameNode(oldStartVNode, newStartVNode)) {
    
     // 头-头
      patchVNode(oldStartVNode, newStartVNode)
      // 更新指针
      oldStartVNode = oldChildren[++oldStartIdx]
      newStartVNode = newChildren[++newStartIdx]
    } else if (isSameNode(oldStartVNode, newEndVNode)) {
    
     // 头-尾
      patchVNode(oldStartVNode, newEndVNode)
      // 把oldStartVNode节点移动到最后
      el.insertBefore(oldStartVNode.el, oldEndVNode.el.nextSibling)
      // 更新指针
      oldStartVNode = oldChildren[++oldStartIdx]
      newEndVNode = newChildren[--newEndIdx]
    } else if (isSameNode(oldEndVNode, newStartVNode)) {
    
     // 尾-头
      patchVNode(oldEndVNode, newStartVNode)
      // 把oldEndVNode节点移动到oldStartVNode前
      el.insertBefore(oldEndVNode.el, oldStartVNode.el)
      // 更新指针
      oldEndVNode = oldChildren[--oldEndIdx]
      newStartVNode = newChildren[++newStartIdx]
    } else if (isSameNode(oldEndVNode, newEndVNode)) {
    
     // 尾-尾
      patchVNode(oldEndVNode, newEndVNode)
      // 更新指针
      oldEndVNode = oldChildren[--oldEndIdx]
      newEndVNode = newChildren[--newEndIdx]
    } else {
    
    
      let findIndex = findSameNode(oldChildren, newStartVNode)
      // newStartVNode在旧列表里不存在,那么是新节点,创建插入
      if (findIndex === -1) {
    
    
        el.insertBefore(createEl(newStartVNode), oldStartVNode.el)
      } else {
    
     // 在旧列表里存在,那么进行patch,并且移动到oldStartVNode前
        let oldVNode = oldChildren[findIndex]
        patchVNode(oldVNode, newStartVNode)
        el.insertBefore(oldVNode.el, oldStartVNode.el)
        // 将该VNode置为空
        oldChildren[findIndex] = null
      }
      newStartVNode = newChildren[++newStartIdx]
    }
  }
  // 旧列表里存在新列表里没有的节点,需要删除
  if (oldStartIdx <= oldEndIdx) {
    
    
    for (let i = oldStartIdx; i <= oldEndIdx; i++) {
    
    
      removeEvent(oldChildren[i])
      oldChildren[i] && el.removeChild(oldChildren[i].el)
    }
  } else if (newStartIdx <= newEndIdx) {
    
    
    let before = newChildren[newEndIdx + 1] ? newChildren[newEndIdx + 1].el : null
    for (let i = newStartIdx; i <= newEndIdx; i++) {
    
    
      el.insertBefore(createEl(newChildren[i]), before)
    }
  }
}

Конкретные шаги реализации этой функции могут относиться к предыдущей статье, поэтому в этой статье они повторяться не будут.

Если мы хотим diffпереместить этот процесс, мы должны сначала выяснить, какие объекты анимируются.Начиная с параметров функции, сначала oldChildrenи newChildrenдва VNodeсписка необходимы, которые могут быть представлены двумя горизонтальными списками, а затем четырьмя указателями, что является ключ к двустороннему diffалгоритму. Мы представляем его четырьмя стрелками, указывающими на текущий сравниваемый узел, а затем запускаем цикл. На самом деле старый и новый VNodeсписки в цикле принципиально не меняются. То, чем мы фактически оперируем, это VNodeсоответствующие DOMэлементы Real, включая patchисправление, перемещение, удаление, добавление и т. д., поэтому у нас есть еще один горизонтальный список для представления текущего реального списка. В начале он должен соответствовать DOMстарому списку. Шаг за шагом через алгоритм, он станет и Новый список соответствует.VNodediffVNode

Давайте вернемся к функции VNode, которая создает объект h:

export const h = (tag, data = {
     
     }, children) => {
    
    
  let text = ''
  let el
  let key
  // 文本节点
  if (typeof children === 'string' || typeof children === 'number') {
    
    
    text = children
    children = undefined
  } else if (!Array.isArray(children)) {
    
    
    children = undefined
  }
  if (data && data.key) {
    
    
    key = data.key
  }
  return {
    
    
    tag, // 元素标签
    children, // 子元素
    text, // 文本节点的文本
    el, // 真实dom
    key,
    data
  }
}

VNodeДанные списка, которые мы вводим, будут hсозданы в VNodeобъекты с помощью функций, поэтому простейшая структура, которую можно ввести, выглядит следующим образом:

[
  {
    
    
    tag: 'div',
    children: '文本节点的内容',
    data: {
    
    
      key: 'a'
    }
  }
]

Введенные новые и старые VNodeданные списка будут сохранены в store, который можно получить следующими способами:

// 输入的旧VNode列表
store.oldVNode
// 输入的新VNode列表
store.newVNode

Затем определите соответствующие переменные:

// 指针列表
const oldPointerList = ref([])
const newPointerList = ref([])
// 真实DOM节点列表
const actNodeList = ref([])
// 新旧节点列表
const oldVNodeList = ref([])
const newVNodeList = ref([])
// 提示信息
const info = ref('')

Анимация перемещения указателя может быть реализована с использованием свойства cssэлемента transitionуказателя, пока значение элемента указателя изменяется , анимация перемещения leftреального списка может быть легко реализована с помощью компонента перехода списка TransitionGroup , шаблон выглядит следующим образом:DOMVue

<div class="playground">
  <!-- 指针 -->
  <div class="pointer">
    <div
         class="pointerItem"
         v-for="item in oldPointerList"
         :key="item.name"
         :style="{ left: item.value * 120 + 'px' }"
         >
      <div class="pointerItemName">{
   
   { item.name }}</div>
      <div class="pointerItemValue">{
   
   { item.value }}</div>
      <img src="../assets/箭头_向下.svg" alt="" />
    </div>
  </div>
  <div class="nodeListBox">
    <!-- 旧节点列表 -->
    <div class="nodeList">
      <div class="name" v-if="oldVNodeList.length > 0">旧的VNode列表</div>
      <div class="nodes">
        <TransitionGroup name="list">
          <div
               class="nodeWrap"
               v-for="(item, index) in oldVNodeList"
               :key="item ? item.data.key : index"
               >
            <div class="node">{
   
   { item ? item.children : '空' }}</div>
          </div>
        </TransitionGroup>
      </div>
    </div>
    <!-- 新节点列表 -->
    <div class="nodeList">
      <div class="name" v-if="newVNodeList.length > 0">新的VNode列表</div>
      <div class="nodes">
        <TransitionGroup name="list">
          <div
               class="nodeWrap"
               v-for="(item, index) in newVNodeList"
               :key="item.data.key"
               >
            <div class="node">{
   
   { item.children }}</div>
          </div>
        </TransitionGroup>
      </div>
    </div>
    <!-- 提示信息 -->
    <div class="info">{
   
   { info }}</div>
  </div>
  <!-- 指针 -->
  <div class="pointer">
    <div
         class="pointerItem"
         v-for="item in newPointerList"
         :key="item.name"
         :style="{ left: item.value * 120 + 'px' }"
         >
      <img src="../assets/箭头_向上.svg" alt="" />
      <div class="pointerItemValue">{
   
   { item.value }}</div>
      <div class="pointerItemName">{
   
   { item.name }}</div>
    </div>
  </div>
  <!-- 真实DOM列表 -->
  <div class="nodeList act" v-if="actNodeList.length > 0">
    <div class="name">真实DOM列表</div>
    <div class="nodes">
      <TransitionGroup name="list">
        <div
             class="nodeWrap"
             v-for="item in actNodeList"
             :key="item.data.key"
             >
          <div class="node">{
   
   { item.children }}</div>
        </div>
      </TransitionGroup>
    </div>
  </div>
</div>

diffНовый список не будет изменен во время работы алгоритма double-end VNode, но старый VNodeсписок может быть изменен, то есть когда сквозное сравнение не находит узел, который можно использовать повторно, но он находится VNodeпри прямом поиске в старом списке, то VNodeсоответствующая реальность будет перемещена DOM, После перемещения это VNodeфактически эквивалентно обработке, но VNodeположение объекта находится в середине текущего указателя и не может быть удалено напрямую, поэтому его необходимо установите значение empty null, чтобы вы могли видеть в шаблоне. Есть ручки для этой ситуации.

Кроме того, мы также создали infoэлемент для отображения текстовой информации подсказки в виде описания анимации.

Но этого недостаточно, потому что у каждого старого VNodeесть соответствующий реальный DOMэлемент, а то, что мы вводим, — это просто обычные jsonданные, поэтому шаблону также нужно добавить новый список в качестве связанного узла старого списка.Этому VNodeсписку нужно только обеспечить узлы На него можно ссылаться, и он не должен быть видимым, поэтому установите displayдля него значение none:

// 根据输入的旧VNode列表创建元素
const _oldVNodeList = computed(() => {
    
    
  return JSON.parse(store.oldVNode)
})
// 引用DOM元素
const oldNode = ref(null)
const oldNodeList = ref([])
<!-- 隐藏 -->
<div class="hide">
  <div class="nodes" ref="oldNode">
    <div
         v-for="(item, index) in _oldVNodeList"
         :key="index"
         ref="oldNodeList"
         >
      {
   
   { item.children }}
    </div>
  </div>
</div>

Затем, когда мы нажимаем кнопку запуска, мы можем присвоить значения нашим трем переменным списка и использовать hфункции для создания новых и старых VNodeобъектов, а затем передать их в исправленную patchфункцию, чтобы начать сравнение и обновление фактических DOMэлементов:

const start = () => {
    
    
  nextTick(() => {
    
    
    // 表示当前真实的DOM列表
    actNodeList.value = JSON.parse(store.oldVNode)
    // 表示旧的VNode列表
    oldVNodeList.value = JSON.parse(store.oldVNode)
    // 表示新的VNode列表
    newVNodeList.value = JSON.parse(store.newVNode)
    nextTick(() => {
    
    
      let oldVNode = h(
        'div',
        {
    
     key: 1 },
        JSON.parse(store.oldVNode).map((item, index) => {
    
    
          // 创建VNode对象
          let vnode = h(item.tag, item.data, item.children)
          // 关联真实的DOM元素
          vnode.el = oldNodeList.value[index]
          return vnode
        })
      )
      // 列表的父节点也需要关联真实的DOM元素
      oldVNode.el = oldNode.value
      let newVNode = h(
        'div',
        {
    
     key: 1 },
        JSON.parse(store.newVNode).map(item => {
    
    
          return h(item.tag, item.data, item.children)
        })
      )
      // 调用patch函数进行打补丁
      patch(oldVNode, newVNode)
    })
  })
}

Можно видеть, что новый и старый списки, которые мы вводим, VNodeиспользуются в качестве дочерних узлов узла, Это связано с тем, что только когда два сравниваемых узла имеют дочерние узлы, которые не являются текстовыми узлами, необходимо использовать алгоритмы для diffэффективного обновления их дочерних узлов. , Когда patchпосле запуска функции вы можете открыть консоль, чтобы просмотреть скрытый список, и вы обнаружите, что он согласуется с DOMновым списком.Тогда вы можете спросить, почему бы просто не использовать этот список как реальный список и создать дополнительный список самостоятельно в процессе работы алгоритма реальные узлы перемещаются другими методами , поэтому добавить анимацию перехода непросто, только чтобы увидеть, как узлы меняют положение мгновенно, что не удовлетворить наши потребности в анимации.VNodeDOMactNodeListdiffinsertBeforeDOM

Эффект здесь следующий:

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

const handles = {
    
    
  // 更新指针
  updatePointers(oldStartIdx, oldEndIdx, newStartIdx, newEndIdx) {
    
    
    oldPointerList.value = [
      {
    
    
        name: 'oldStartIdx',
        value: oldStartIdx
      },
      {
    
    
        name: 'oldEndIdx',
        value: oldEndIdx
      }
    ]
    newPointerList.value = [
      {
    
    
        name: 'newStartIdx',
        value: newStartIdx 
      },
      {
    
    
        name: 'newEndIdx',
        value: newEndIdx
      }
    ]
  },
}

Затем мы можем обновить указатель в diffфункции handles.updatePointers():

const diff = (el, oldChildren, newChildren) => {
    
    
  // 指针
  // ...
  handles.updatePointers(oldStartIdx, oldEndIdx, newStartIdx, newEndIdx)
  // ...
}

Итак, указатель выходит:

Тогда whileэти четыре указателя будут постоянно изменяться в цикле, поэтому их тоже нужно обновлять в цикле:

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    
    
  // ...
  handles.updatePointers(oldStartIdx, oldEndIdx, newStartIdx, newEndIdx)
}

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

const wait = t => {
    
    
  return new Promise(resolve => {
    
    
    setTimeout(
      () => {
    
    
        resolve()
      },
      t || 3000
    )
  })
}

Затем мы async/awaitможем легко реализовать ожидание в циклах, используя синтаксис:

const diff = async (el, oldChildren, newChildren) => {
    
    
  // ...
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    
    
    // ...
    handles.updatePointers(oldStartIdx, oldEndIdx, newStartIdx, newEndIdx)
    await wait()
  }
}

Затем мы добавляем две переменные, чтобы выделить две, которые мы сейчас сравниваем VNode:

// 当前比较中的节点索引
const currentCompareOldNodeIndex = ref(-1)
const currentCompareNewNodeIndex = ref(-1)

const handles = {
    
    
  // 更新当前比较节点
  updateCompareNodes(a, b) {
    
    
    currentCompareOldNodeIndex.value = a
    currentCompareNewNodeIndex.value = b
  }
}
<div
     class="nodeWrap"
     v-for="(item, index) in oldVNodeList"
     :key="item ? item.data.key : index"
     :class="{
         current: currentCompareOldNodeIndex === index,
     }"
     >
  <div class="node">{
   
   { item ? item.children : '空' }}</div>
</div>
<div
     class="nodeWrap"
     v-for="(item, index) in newVNodeList"
     :key="item.data.key"
     :class="{
         current: currentCompareNewNodeIndex === index,
     }"
     >
  <div class="node">{
   
   { item.children }}</div>
</div>

Добавляем имя класса в ноду в текущем сравнении для подсветки, следующий шаг все тот же, нужно вызывать diffфункцию в функции, но как ее добавить:

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    
    
    if // ...
    } else if (isSameNode(oldStartVNode, newStartVNode)) {
    
    
      // ...
      oldStartVNode = oldChildren[++oldStartIdx]
      newStartVNode = newChildren[++newStartIdx]
    } else if (isSameNode(oldStartVNode, newEndVNode)) {
    
    
      // ...
      oldStartVNode = oldChildren[++oldStartIdx]
      newEndVNode = newChildren[--newEndIdx]
    } else if (isSameNode(oldEndVNode, newStartVNode)) {
    
    
      // ...
      oldEndVNode = oldChildren[--oldEndIdx]
      newStartVNode = newChildren[++newStartIdx]
    } else if (isSameNode(oldEndVNode, newEndVNode)) {
    
    
      // ...
      oldEndVNode = oldChildren[--oldEndIdx]
      newEndVNode = newChildren[--newEndIdx]
    } else {
    
    
      // ...
      newStartVNode = newChildren[++newStartIdx]
    }

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

const isSameNode = async () => {
    
    
  // ...
  handles.updateCompareNodes()
  await wait()
}

if (await isSameNode(oldStartVNode, newStartVNode))

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

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    
    
  let stop = false
  let _isSameNode = false
  if (oldStartVNode === null) {
    
    
    callbacks.updateInfo('')
    oldStartVNode = oldChildren[++oldStartIdx]
    stop = true
  }
  // ...
  if (!stop) {
    
    
    callbacks.updateInfo('头-头比较')
    callbacks.updateCompareNodes(oldStartIdx, newStartIdx)
    _isSameNode = isSameNode(oldStartVNode, newStartVNode)
    if (_isSameNode) {
    
    
      callbacks.updateInfo(
        'key值相同,可以复用,进行patch打补丁操作。新旧节点位置相同,不需要移动对应的真实DOM节点'
      )
    }
    await wait()
  }
  if (!stop && _isSameNode) {
    
    
    // ...
    oldStartVNode = oldChildren[++oldStartIdx]
    newStartVNode = newChildren[++newStartIdx]
    stop = true
  }
  // ...
}

Мы используем переменную, чтобы указать, вошли ли мы в определенную ветку, а затем сохраняем результат проверки возможности повторного использования узла в переменной, чтобы мы могли постоянно проверять значения этих двух переменных, чтобы определить, следует ли входить В последующей ветке сравнения логики сравнения нет в ifусловии, и ее можно использовать awaitПри этом также используем updateInfoдобавленную подсказку:

const handles = {
    
    
  // 更新提示信息
  updateInfo(tip) {
    
    
    info.value = tip
  }
}

Далее, давайте посмотрим на операцию перемещения узла.Когда голова ( oldStartIdxсоответствующий oldStartVNodeузел) и хвост ( newEndIdxсоответствующий newEndVNodeузел) сравниваются и обнаруживаются, что их можно использовать повторно, после завершения исправления соответствующий oldStartVNodeреальный DOMэлемент необходимо переместить в позиция oldEndVNodeсоответствующего реального DOMэлемента, то есть Вставить перед oldEndVNodeсоответствующим реальным элементом DOMназад на один узел:

if (!stop && _isSameNode) {
    
    
  // 头-尾
  patchVNode(oldStartVNode, newEndVNode)
  // 把oldStartVNode节点移动到最后
  el.insertBefore(oldStartVNode.el, oldEndVNode.el.nextSibling)
  // 更新指针
  oldStartVNode = oldChildren[++oldStartIdx]
  newEndVNode = newChildren[--newEndIdx]
  stop = true
}

Тогда мы можем вызвать метод перемещения узлов нашего смоделированного списка сразу после того, как метод insertBeforeпереместит реальные элементы:DOM

if (!stop && _isSameNode) {
    
    
  // ...
  el.insertBefore(oldStartVNode.el, oldEndVNode.el.nextSibling)
  callbacks.moveNode(oldStartIdx, oldEndIdx + 1)
  // ...
}

То, чем мы хотим оперировать, на самом деле представляет собой список, представляющий реальные DOMузлы actNodeList, поэтому ключ в том, чтобы выяснить, какой из них, в первую очередь, четыре указателя узла в начале и в конце указывают позицию в старом и новом списках VNode, поэтому мы можем получить oldStartIdxсоответствующее Position , а затем найти соответствующий узел в списке через значение и выполнить такие операции, как перемещение, удаление и вставка:oldEndIdxoldVNodeListVNodekeyactNodeList

const handles = {
    
    
  // 移动节点
  moveNode(oldIndex, newIndex) {
    
    
    let oldVNode = oldVNodeList.value[oldIndex]
    let newVNode = oldVNodeList.value[newIndex]
    let fromIndex = findIndex(oldVNode)
    let toIndex = findIndex(newVNode)
    actNodeList.value[fromIndex] = '#'
    actNodeList.value.splice(toIndex, 0, oldVNode)
    actNodeList.value = actNodeList.value.filter(item => {
    
    
      return item !== '#'
    })
  }
}

const findIndex = (vnode) => {
    
    
  return !vnode
    ? -1
    : actNodeList.value.findIndex(item => {
    
    
        return item && item.data.key === vnode.data.key
      })
}

Другие узлы вставки и удаления аналогичны:

Вставить узел:

const handles = {
    
    
  // 插入节点
  insertNode(newVNode, index, inNewVNode) {
    
    
    let node = {
    
    
      data: newVNode.data,
      children: newVNode.text
    }
    let targetIndex = 0
    if (index === -1) {
    
    
      actNodeList.value.push(node)
    } else {
    
    
      if (inNewVNode) {
    
    
        let vNode = newVNodeList.value[index]
        targetIndex = findIndex(vNode)
      } else {
    
    
        let vNode = oldVNodeList.value[index]
        targetIndex = findIndex(vNode)
      }
      actNodeList.value.splice(targetIndex, 0, node)
    }
  }
}

удалить узел:

const handles = {
    
    
  // 删除节点
  removeChild(index) {
    
    
    let vNode = oldVNodeList.value[index]
    let targetIndex = findIndex(vNode)
    actNodeList.value.splice(targetIndex, 1)
  }
}

Позиция выполнения этих методов в diffфункции на самом деле является местом, где выполняется метод insertBeforeВы removeChildможете обратиться к исходному коду этой статьи для получения подробной информации, поэтому я не буду его здесь представлять.

Кроме того, вы также можете выделить элементы, которые были сравнены, элементы, которые будут добавлены, элементы, которые будут удалены и т. д., окончательный эффект:

По причинам времени в настоящее время реализован только эффект двухстороннего алгоритма diff, а анимация процесса быстрого алгоритма будет добавлена ​​в будущем.Если diffвам интересно, вы можете обратить внимание~

Склад: https://github.com/wanglin2/VNode_visualization .

Supongo que te gusta

Origin blog.csdn.net/sinat_33488770/article/details/127604224
Recomendado
Clasificación