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

JavaScript

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

Мы знаем, что наши обычные страницы состоят из множества Домов. Что такое виртуальный Дом (виртуальный дом)? Проще говоря, это имитация реальных узлов Домов с помощью JavaScript, а сравнение изменений Домов помещается в слой Js. сделай это.

Ниже приведен традиционный узел dom, с ним должен быть знаком каждый.

И виртуальный дом, соответствующий этому дому, может быть выражен следующим образом

Это очень просто, каждый может понять,tagпредставляет имя метки,attrsЭто атрибут dom.Если у каждого dom есть дочерние элементы, он будет отображаться в виде массива в дочерних элементах, и каждый элемент массива представляет собой структуру виртуального dom.

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

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

Некоторые люди спросят: DOM — это очень хорошо. Когда мы впервые изучим внешний интерфейс, мы обязательно столкнемся с JQuery. JQuery — это типичная библиотека фреймворков для работы с DOM. Мы используем JQuery для разработки сцены, чтобы объяснить полезность и стоимость виртуального DOM.

Вот сценарий спроса

var data = [
      {
        name: '张三',
        age: '20',
        address: '杭州'
      },
      {
        name: '李四',
        age: '22',
        address: '北京'
      },
      {
        name: '隔壁老王',
        age: '24',
        address: "西溪水岸"
      }
    ]

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

  <div id="container"></div>
  <button id="btn-change">change</button>

  <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
  <script>
    var data = [
      {
        name: '张三',
        age: '20',
        address: '杭州'
      },
      {
        name: '李四',
        age: '22',
        address: '北京'
      },
      {
        name: '隔壁老王',
        age: '24',
        address: "西溪水岸"
      }
    ]

    function render(data) {
      var $container = $('#container')

      //清空现有内容
      $container.html('')

      // 拼接 table
      var $table = $('<table>')
      $table.append($('<tr><td>name</td><td>age</td><td>address</td></tr>'))
      data.forEach(function (item) {
        $table.append($('<tr><td>'+ item.name +'</td><td>'+item.age+'</td><td>'+item.address+'</td></tr>'))
      })
      
      // 渲染到页面
      $container.append($table)
    }

    $('#btn-change').click(function () {
      data[1].age = 30
      data[2].address = '上海'
      render(data)
    })
    
    // 初始化时候渲染
    render(data)

Как видите, мы будемdataпункта 2ageи пункт 3 изaddressДанные заменены, нажмите кнопку изменить:

Проблемы, которые решает vdom

Из рисунка видно, что мы изменили только часть данных таблицы, а весьtabelВсе узлы мигают, показывая, что всеtableВсе были заменены.

Эта операция JQuery, основанная на здравом смысле, сильно снижает производительность веб-страницы во времени. Поскольку он изменяет узлы dom, которые не нужно менять, если вы не понимаете серьезности вопроса, вы можете продолжать смотреть вниз.

Работа следующего кода очень проста, создание пустогоdivметка, прокрутите в ней свойства и произнесите ее по буквам

    var div = document.createElement('div')
    var item ,result = ''
    for (item in div) {
      result += ' | ' + item
    }
    console.log(result)

Есть плотные атрибуты, не говоря уже о том, что это атрибут только первого уровня. Можно себе представить, насколько трудоемко работать непосредственно с DOM. Работа с DOM трудоемка, но как язык Js работает очень быстро , Слой Js выполняет сравнение DOM и минимизирует ненужные операции с DOM, вместо того, чтобы обновлять его каждый раз, наша эффективность будет значительно увеличена. И vdom отлично может решить эту проблему.

Как пользоваться виртуальным домом

Сказав так много о виртуальном доме, некоторые студенты спросят, как использовать виртуальный дом?

Чтобы понять, как использовать vdom, мы можем использовать существующую библиотеку реализации vdom, чтобы понять ее API, а затем понять, как использовать vdom в разработке.

Здесь мы выбираем библиотеку виртуального дома, используемую в Vue2.snabbdom, на следующем рисунке показан пример перехвата его домашней страницы на github:

После тщательного наблюдения мы можем обнаружить, что в этом официальном случае snabbdom основное содержание состоит из двух функций:h函数а такжеpatch函数

h функция

можно увидетьhфункция с тремя параметрами

  • Селектор тегов
  • Атрибуты
  • дочерний узел

например первыйh函数Сгенерированный vnode представляет собойdivЭтикетка, привязка события щелчкаsomeFn, первый ребенок со стилемspan,sapnявляется текстовым узломThis is bold, второй потомок — это непосредственно текстовый узел, а третий — этоherfизaСвязь

патч функция

patchна два случая

  • Первый — при первом рендеринге.patchперетащите vnode наcontainerв пустой таре
       var vnode = h('ul#list',{},[
        h('li.item',{},'大冰哥'),
        h('li.item',{},'伦哥'),
        h('li.item',{},'阿孔')
      ])
    
      patch(container, vnode) // vnode 将 container 节点替换
    

Когда патч рендерится в первый раз, сгенерированный vnode выбрасывается в пустой контейнер Вы можете сравнить предыдущий Jquery при первом рендеринге таблицы, добавить таблицу html в контейнер

  • Второй — при обновлении узлов,newVnodeБудуoldVnodeзаменять
    btn.addEventListener('click',function() {
      var newVnode = h('ul#list',{},[
        h('li.item',{},'大冰哥'),
        h('li.item',{},'伦哥'),
        h('li.item',{},'孔祥宇'),
        h('li.item',{},'小老弟'),
      ])
      patch(vnode, newVnode)
    })
    

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

Мы можем ясно видеть, что, по сравнению с предыдущим случаем замены всего dom страницы на JQuery, использование vdompathcФункция модифицирует только те места, где изменился наш относительно старый vnode, а места, которые не изменились, бесполезны (что видно по мерцанию страницы)

Повторить предыдущий случай Jq, используя vdom

vdom ядро ​​​​апиhфункция иpatchФункция, которую мы имеем базовое понимание, для того чтобы закрепить свои знания, и мы собираемся использоватьsnabbdomПовторите наш предыдущий случай JQuery

код напрямую

<div id="container"></div>
  <button id="btn-change">change</button>
  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
  <script src="https://cdn.bootcss.com/snabbdom/0.7.2/snabbdom-class.js"></script>
  <script src="https://cdn.bootcss.com/snabbdom/0.7.2/snabbdom-props.js"></script>
  <script src="https://cdn.bootcss.com/snabbdom/0.7.2/snabbdom-style.js"></script>
  <script src="https://cdn.bootcss.com/snabbdom/0.7.2/snabbdom-eventlisteners.js"></script>
  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
  <script>
    let container = document.getElementById('container')
    let btn = document.getElementById('btn-change')
    let snabbdom = window.snabbdom
    let patch = snabbdom.init([
      snabbdom_class,
      snabbdom_props,
      snabbdom_style,
      snabbdom_eventlisteners
    ])
    let h = snabbdom.h
    let data = [
      {
        name: '张三',
        age: '20',
        address: '杭州'
      },
      {
        name: '李四',
        age: '22',
        address: '北京'
      },
      {
        name: '隔壁老王',
        age: '24',
        address: "西溪水岸"
      }
    ]
    data.unshift({
      name: '姓名',
      age: '年龄',
      address: '地址'
    })
    let vnode
    function render(data) {
      // 创建虚拟table节点 第三个参数,也就是虚拟table的孩子 应该是虚拟的 行节点
      let newVnode = h('table', {}, data.map(item => {
        let tds = [] // 列,作为虚拟行的子项
        let i
        for(i in item) {
          if (item.hasOwnProperty(i)) {
            tds.push(h('td', {}, item[i]+''))
          }
        }
        return h('tr', {}, tds) // 虚拟行节点的 孩子 应该是虚拟的 列节点
      }))

      if (vnode) {
        patch(vnode,newVnode)
      } else {
        // 初次渲染
        patch(container,newVnode)
      }
      vnode = newVnode
    }

    btn.addEventListener('click', function(){
      data[1].age = 30,
      data[3].name = '一个女孩',
      render(data)
    })
    
    // 初始化时候渲染
    render(data)
  </script>
</body>

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

  • Введите основной файл snabbdom, инициализируйте функцию h и функцию исправления.
  • Когда он загружается в первый раз, суть рендера фактическиpatch(container,newVnode)
  • нажмите послеchangeСоздайте новый VNODE, а затемpatch(vnode,newVnode)

здесьrenderСосредоточьтесь на функции

  • Когда создается newVnode, третий параметр является дочерним.
  • а такжеtableДети - это узлы строки
  • trУзел строки также является vnode, который также используется при его регенерации.hфункция, третий параметрtdстолбец vnode
  • tdТретий параметр столбца vnode — это непосредственно текстовый узел, он обходит каждый элемент item и проталкивает вtdsв массиве

К этому моменту у вас должно быть общее представление о vdom.На самом деле, это не столько то, что vdom быстрый, точнее сказать, что он не медленный по сравнению со способом свержения dom Jquer.

Суммировать

ядро ​​апи vdom

  • h('имя тега', 'атрибут', [дочерний элемент])
  • h('имя тега', 'атрибут', 'текст')
  • patch(container, vnode)
  • patch(oldVnode,newVnode)

Краткое введение в алгоритм diff

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

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

Например, вы используете git при отправке кодаgit diffКоманды, или какие-то инструменты сравнения кода в интернете, а ядром нашего виртуального дома является алгоритм diff.Как мы упоминали ранее, нам нужно выяснить узлы, которые необходимо обновить, и не перемещать узлы, которые не обновлено. Суть этого заключается в том, как узнать, какие обновления и какие не обновлены.Для этого процесса требуется алгоритм сравнения.

пройти черезpatchпростой дифференциал

Давайте ковать железо, пока горячо, или использовать предыдущую библиотеку snabbdom, чтобы кратко рассказать об общей идее алгоритма diff.В snabbdom diff в основном отражается вpatch, далее мы рассмотрим два случая patchpatch(container, vnode)а такжеpatch(vnode, newVnode)

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

patch(container, vnode)

Мы знаем, что процесс этого патча — это процесс добавления vnode (vdom) в пустой контейнер для создания реального dom.Основной поток кода выглядит следующим образом:

function creatElement(vnode) {
  let tag = vnode.tag
  let attrs = vnode.attrs || {}
  let children = vnode.children || []
  // 无标签 直接跳出
  if (!tag) {
    return null
  }
  // 创建元素
  let elem = document.createElement(tag)
  // 添加属性
  for(let attrName in attrs) {
    if (attrs.hasOwnProperty(attrName)) {
      elem.setAttribute(arrtName, arrts[attrName])
    }
  }
  // 递归创建子元素
  children.forEach((childVnode) => {
    elem.appendChild(createElement(childVnode))
  })

  return elem
}

Упрощенный код прост, разобраться сможет каждый, один из важных моментовСаморекурсивный вызов для создания дочерних узлов, условие окончанияtagдляnullСлучай

patch(vnode, newVnode)

Этот процесс исправления представляет собой процесс сравнения различий Мы здесь моделируем только простейший сценарий.

Изменен третий пункт и добавлен четвертый пункт

// 简化流程 假设跟标签相同的两个虚拟dom
function updateChildren (vnode, newVnode) {
  let children = vnode.children || []
  let newChildren = newVnode.children || []

  // 遍历现有的孩子
  children.forEach((oldChild, index) => {
    let newChild = newChildren[index]
    if (newChild === null) {
      return
    }
    // 两者tag一样,值得比较
    if (oldChild.tag === newChild.tag) {
      // 递归继续比较子项
      updateChildren(oldchild, newChild)
    } else {
      // 两者tag不一样
      replaceNode(oldChild, newChild)
    }
  })
}

Точка здесь также рекурсивная, вот просто простой взятьtagСудить об условиях обновления, на самом деле, фактический намного сложнее, чем это; иreplaceФактическая работа функции заключается вnewVnodeВновь сгенерированный реальный DOM заменяет старый DOM, который включает в себя больше собственных операций с DOM, поэтому я не буду вдаваться в подробности.

На этом этапе каждый должен иметь хорошее представление об основной концепции сравнения. Опять же, чтобы облегчить понимание, процесс алгоритма сравнения был значительно упрощен. Фактический алгоритм сравнения намного сложнее, чем приведенный выше. Например. ,

  • Добавление и удаление узлов
  • Переупорядочение и оптимизация этого процесса
  • Изменения узел свойств стилей событий
  • А как оптимизировать алгоритм до предела и так далее. .

Кому интересно, может пойти узнать больше.

Суммировать

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

Дополнительный ключ Vue

При написании статей я столкнулся с проблемой привязки Vue Key.Вот с этим горячим совместите алгоритм виртуального DOM и DIFF, чтобы узнать ключ в Vue.

ключи в вью

Прежде всего, объяснение официального сайта Vue:

Когда Vue.js используетv-forПри обновлении отображаемого списка элементов по умолчанию используется стратегия «повторное использование на месте». Если порядок элементов данных изменен, Vue не будет перемещать элементы DOM в соответствии с порядком элементов данных, а просто будет повторно использовать каждый элемент здесь и убедиться, что он показывает каждый элемент, который был отрисован по определенному индексу.

Стратегия мультиплексирования на месте здесь повторно использует элементы, которые не изменились, а остальные должны быть переставлены последовательно.

Чтобы дать Vue подсказку, чтобы он мог отслеживать идентификатор каждого узла и, таким образом, повторно использовать и изменять порядок существующих элементов, вам необходимо предоставить каждому элементу уникальныйkeyАтрибуты. идеальныйkeyЗначение представляет собой уникальный идентификатор, который имеет каждый элемент.

Мы используем использование часто будем использоватьindex(то есть нижний индекс массива) какkey, но на самом деле это метод, который не рекомендуется

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

Вот массив данных

const list = [
    {
        id: 1,
        name: 'test1',
    },
    {
        id: 2,
        name: 'test2',
    },
    {
        id: 3,
        name: 'test3',
    },
]

Теперь мы хотим добавить линию данных

const list = [
    {
        id: 1,
        name: 'test1',
    },
    {
        id: 2,
        name: 'test2',
    },
    {
        id: 3,
        name: 'test3',
    },
    {
        id: 4,
        name: '添加到最后的一条数据',
    },
]

на этот раз использоватьindexтак какkey, это не проблема, потому чтоindex1 добавляется сзади

Но если вставленные данные вставляются в середину, а не в конец,

const list = [
    {
        id: 1,
        name: 'test1',
    },
    {
        id: 4,
        name: '不甘落后跑到第二的的一条数据',
    }
    {
        id: 2,
        name: 'test2',
    },
    {
        id: 3,
        name: 'test3',
    },
]

В это время произойдет ситуация:

之前的数据                         之后的数据

key: 0  index: 0 name: test1     key: 0  index: 0 name: test1
key: 1  index: 1 name: test2     key: 1  index: 1 name: 不甘落后跑到第二的的一条数据
key: 2  index: 2 name: test3     key: 2  index: 2 name: test2
                                 key: 3  index: 3 name: test3

Таким образом, после добавления данных в дополнение к первым данным можно就地复用, последние три строки должны быть перерисованы, что, очевидно, не является тем результатом, который нам нужен.

использоватьуникальный ключулучшить:

На этот раз мы поставилиkeyПривязать к уникальному идентификатору id

之前的数据                         之后的数据

key: 1  id: 1 index: 0 name: test1   key: 1  id: 1 index: 0  name: test1
key: 2  id: 2 index: 1 name: test2   key: 4  id: 4 index: 1  name: 不甘落后的一条数据
key: 3  id: 3 index: 2 name: test3   key: 2  id: 2 index: 2  name: test2
                                     key: 3  id: 3 index: 3  name: test3

Помимо добавленияid为4的不甘落后的数据Он добавлен недавно, а остальные повторно используют предыдущий dom, потому что он передается здесь唯一keyсвязывать и не перерисовывать при изменении порядка.

Таким образом, нам нужно использовать ключ, чтобы сделать уникальный идентификатор для каждого узла, алгоритм Vue Diff может правильно идентифицировать этот узел, найти правильную область местоположения для вставки нового узла, поэтому в одном предложении:Роль ключа в основном заключается в эффективном обновлении виртуального DOM.

Soul Painter онлайн:

Видно, что когда наши старые данные преобразуются в новые данные [a,b,c,d] --> [a,e,b,c,d]

Если мы не используем правильныйkey, возможно, в дополнение к данным, которые можно использовать повторно, необходимо повторно отобразить следующие четыре данных.

тогда как если правильныйkeyКогда вы можете понять, что есть только одно место, которое нужно изменить, то есть новые данные e, а остальные будут продолжать использоваться соответственно, как показано стрелками.

vue中的key