Углубленный анализ: виртуальный DOM в основе Vue

Vue.js

предисловие

использоватьVueЯ работаю над проектом уже два года, да.VueизapiТак же удобнее в использовании, хотя даVueНекоторые принципы реализации, такие как виртуальныйDOM,flow, data-driven, принципы маршрутизации и т. д., но я намеренно не исследовал основы этих принципов иVueКак исходный код использует эти принципы для реализации фреймворка, так что используйте свободное время для выполненияVueТехнические принципы иVueЗавершение бетонной реализации каркаса. если ты правVueЕсли вам интересен принцип реализации, то вы можете начать читать этот цикл статей, который откроетVueБазовые мировые врата , и более подробно рассмотрим детали их реализации. Эта статьяVirtual DOMтехнические принципы иVueКонкретная реализация фреймворка.

Я усердно работал над написанием в течение долгого времени, и я надеюсь вручную поставить лайк и поощрить~

Адрес гитхаба:GitHub.com/ревматизм123/…, который обобщает все статьи в блоге автора, если вам нравится или вдохновляет, пожалуйста, помогите дать звезду ~, что также является поощрением для автора.

1. РеальныйDOMи процесс его разбора

В этом разделе мы в основном представляем реальныйDOMПроцесс синтаксического анализа, представляя свой процесс синтаксического анализа и существующие проблемы, приводит к тому, почему виртуальныеDOM. Картинка стоит тысячи слов, как показано нижеwebkitРабочий процесс движка рендеринга

Все рабочие процессы браузерного движка рендеринга примерно разделены на 5 шагов: созданиеDOMдерево —> создатьStyle Rules-> построитьRenderдерево —> макетLayout--> рисоватьPainting.

  • Первым шагом является построение дерева DOM: используйте анализатор HTML для анализа элементов HTML и построения дерева DOM;
  • Второй шаг — создать таблицу стилей: используйте анализатор CSS для анализа встроенных стилей в файлах и элементах CSS и создайте таблицу стилей для страницы;
  • Третий шаг, построение дерева рендеринга: Свяжите дерево DOM с таблицей стилей, чтобы построить дерево рендеринга (вложение). У каждого узла DOM есть метод attach, который принимает информацию о стиле и возвращает объект рендеринга (также известный как средство рендеринга), который в конечном итоге будет сконструирован в дереве рендеринга;
  • На четвертом этапе определите координаты узла: в соответствии со структурой дерева рендеринга определите точную координату, появляющуюся на экране дисплея, для каждого узла в дереве рендеринга;
  • Пятый шаг, отрисовка страницы: отобразите координаты в соответствии с деревом рендеринга и узлами, а затем вызовите метод рисования каждого узла, чтобы отрисовать их.

будь осторожен:

1,DOMПостроение дерева начинается при загрузке документа?ПостроитьDOMДерево представляет собой постепенный процесс, чтобы добиться лучшего взаимодействия с пользователем, механизм рендеринга будет отображать содержимое на экране как можно скорее, ему не нужно ждать, пока весьHTMLСборка после завершения синтаксического анализа документаrenderДерево и макет.

2,RenderдеревоDOMдерево иCSSНачинается ли сборка после создания таблицы стилей?Эти три процесса не являются полностью независимыми, когда они происходят на самом деле, но будут пересечения, которые будут загружаться, анализироваться и отображаться одновременно.

3.CSSАнализ точек внимания? CSSСинтаксический анализ выполняется в обратном порядке справа налево, чем больше вложенных тегов, тем медленнее парсинг.

4.JSдействующий реальныйDOMЦена?Используя нашу традиционную модель разработки, нативныеJSилиJQдействоватьDOM, браузер выполнит процесс от начала до конца, начиная с построения DOM-дерева. За одну операцию мне нужно обновить 10DOMузел, браузер получает первыйDOMПосле запроса он не знает, что есть 9 операций обновления, поэтому процесс будет выполнен сразу, и в итоге будет выполнено 10 раз. Например, после первого расчета следующийDOMКогда делается запрос на обновление, значение координаты этого узла изменяется, и предыдущий расчет бесполезен. рассчитатьDOMЗначения координат узлов и т. д. - все это потраченная впустую производительность. Несмотря на то, что компьютерное оборудование неоднократно обновлялось, операционнаяDOMЦена по-прежнему высока, а частые операции по-прежнему будут вызывать застревание страниц, что повлияет на работу пользователей.

два,Virtual-DOMОснование

2.1 ВиртуальныйDOMпреимущества

виртуальныйDOMОн предназначен для решения проблем с производительностью браузера. Как и раньше, если в одной операции 10 обновленийDOMдействие, виртуальныйDOMсразу не получитсяDOM, вместо 10 обновленийdiffСохраните содержимое в локальномJSобъект, и в конечном итоге этоJSобъект одноразовыйattchприбытьDOMНа дереве выполняются последующие операции, чтобы избежать большого количества ненужных вычислений. Итак, используйтеJSмоделирование объектовDOMПреимущество узла в том, что обновление страницы можно полностью отразить в первую очередь.JSобъект (виртуальныйDOM), манипулировать в памятиJSСкорость объекта явно выше, а после завершения обновления окончательныйJSОбъекты сопоставляются с реальнымиDOM, переданный браузеру для рисования.

2.2 Реализация алгоритма

2.2.1, используйтеJSмоделирование объектовDOMДерево

(1) Как использоватьJSмоделирование объектовDOMДерево

например настоящийDOMУзлы следующие:

<div id="virtual-dom">
<p>Virtual DOM</p>
<ul id="list">
  <li class="item">Item 1</li>
  <li class="item">Item 2</li>
  <li class="item">Item 3</li>
</ul>
<div>Hello World</div>
</div> 

мы используемJavaScriptобъект для представленияDOMNode, используйте свойства объекта для записи типа, свойств, дочерних узлов и т. д. узла.

element.jsКод объекта узла, представленного в середине, выглядит следующим образом:

/**
 * Element virdual-dom 对象定义
 * @param {String} tagName - dom 元素名称
 * @param {Object} props - dom 属性
 * @param {Array<Element|String>} - 子节点
 */
function Element(tagName, props, children) {
    this.tagName = tagName
    this.props = props
    this.children = children
    // dom 元素的 key 值,用作唯一标识符
    if(props.key){
       this.key = props.key
    }
    var count = 0
    children.forEach(function (child, i) {
        if (child instanceof Element) {
            count += child.count
        } else {
            children[i] = '' + child
        }
        count++
    })
    // 子元素个数
    this.count = count
}

function createElement(tagName, props, children){
 return new Element(tagName, props, children);
}

module.exports = createElement;

согласно сelementустановка объекта, вышеDOMСтруктура может быть просто выражена как:

var el = require("./element.js");
var ul = el('div',{id:'virtual-dom'},[
  el('p',{},['Virtual DOM']),
  el('ul', { id: 'list' }, [
	el('li', { class: 'item' }, ['Item 1']),
	el('li', { class: 'item' }, ['Item 2']),
	el('li', { class: 'item' }, ['Item 3'])
  ]),
  el('div',{},['Hello World'])
]) 

Сейчасulчто мы используемJavaScriptобъект представленDOMструктура, мы выводим представлениеulСоответствующая структура данных выглядит следующим образом:

(2) Для рендерингаJSуказаноDOMобъект

Но такой структуры на странице нет.Далее мы представим, какulвизуализировать как реальный на страницеDOMСтруктура, связанные функции рендеринга следующие:

/**
 * render 将virdual-dom 对象渲染为实际 DOM 元素
 */
Element.prototype.render = function () {
    var el = document.createElement(this.tagName)
    var props = this.props
    // 设置节点的DOM属性
    for (var propName in props) {
        var propValue = props[propName]
        el.setAttribute(propName, propValue)
    }

    var children = this.children || []
    children.forEach(function (child) {
        var childEl = (child instanceof Element)
            ? child.render() // 如果子节点也是虚拟DOM,递归构建DOM节点
            : document.createTextNode(child) // 如果字符串,只构建文本节点
        el.appendChild(childEl)
    })
    return el
} 

Глядя на вышеизложенноеrenderметод, согласноtagNameпостроить настоящийDOMузел, затем задайте свойства этого узла и, наконец, рекурсивно создайте свои собственные дочерние узлы.

мы построимDOMДобавить структуру на страницуbodyвыше, следующим образом:

ulRoot = ul.render();
document.body.appendChild(ulRoot); 

Таким образом, страницаbodyесть настоящийDOMструктура, эффект показан на следующем рисунке:

2.2.2 Сравните два виртуальных дереваDOMразница деревьев —diffалгоритм

diffАлгоритм сравнения двух деревьевVirtual DOMРазность деревьев, если требуется полное сравнение двух деревьев, тоdiffВременная сложность алгоритмаO(n^3). Но во внешнем интерфейсе вы редко перемещаетесь по слоямDOMэлемент, поэтомуVirtual DOMБудут сравниваться только элементы одного уровня, как показано на следующем рисунке.divтолько на том же уровнеdivНапротив, второй уровень будет сравниваться только со вторым уровнем, так что сложность алгоритма может быть достигнута.O(n).

(1) Обход в глубину, запись различий

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

dfs-walk

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

// diff 函数,对比两棵树
function diff(oldTree, newTree) {
  var index = 0 // 当前节点的标志
  var patches = {} // 用来记录每个节点差异的对象
  dfsWalk(oldTree, newTree, index, patches)
  return patches
}

// 对两棵树进行深度优先遍历
function dfsWalk(oldNode, newNode, index, patches) {
  var currentPatch = []
  if (typeof (oldNode) === "string" && typeof (newNode) === "string") {
    // 文本内容改变
    if (newNode !== oldNode) {
      currentPatch.push({ type: patch.TEXT, content: newNode })
    }
  } else if (newNode!=null && oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
    // 节点相同,比较属性
    var propsPatches = diffProps(oldNode, newNode)
    if (propsPatches) {
      currentPatch.push({ type: patch.PROPS, props: propsPatches })
    }
    // 比较子节点,如果子节点有'ignore'属性,则不需要比较
    if (!isIgnoreChildren(newNode)) {
      diffChildren(
        oldNode.children,
        newNode.children,
        index,
        patches,
        currentPatch
      )
    }
  } else if(newNode !== null){
    // 新节点和旧节点不同,用 replace 替换
    currentPatch.push({ type: patch.REPLACE, node: newNode })
  }

  if (currentPatch.length) {
    patches[index] = currentPatch
  }
} 

Из вышеизложенного можно сделать вывод, чтоpatches[1]выражатьp,patches[3]выражатьul, и так далее.

(2) Тип разницы

DOMК видам расхождений, вызванных операциями, относятся следующие:

  • Замена узла: Меняется узел, например, указанный вышеdivзаменитьh1;
  • Обмен последовательностями: перемещение, удаление, добавление дочерних узлов, как указано выше.divдочерний узел, поставитьpа такжеulобмен заказами;
  • Изменение атрибута: изменены атрибуты узла, например добавление вышеуказанногоliизclassудаление класса стиля;
  • Изменение текста: измените текстовое содержимое текстового узла, например, как указано выше.pИзменить текстовое содержимое узла is "Real Dom"

Некоторые из описанных выше типов различий определены в коде следующим образом:

var REPLACE = 0 // 替换原先的节点
var REORDER = 1 // 重新排序
var PROPS = 2 // 修改了节点的属性
var TEXT = 3 // 文本内容改变 

(3) Алгоритм сравнения списка

Алгоритм сравнения дочерних узлов, напримерp, ul, divизменил порядокdiv, p, ul. Как это следует сравнивать? Если их сравнивать по порядку одного уровня, то все они будут заменены. Такие какpа такжеdivизtagNameразные,pБудетdivзаменены. В конце концов, все три узла будут заменены, так чтоDOMСтоимость очень большая. На самом деле нет необходимости заменять узел, а нужно только перемещаться по узлу, чтобы достичь, нам нужно только знать, как двигаться.

Абстрагирование этой проблемы на самом деле является проблемой минимального расстояния редактирования строк (Edition Distance), наиболее распространенным обходным решением являетсяLevenshtein Distance , Levenshtein Distanceпредставляет собой строку метрики, которая измеряет разницу между двумя последовательностями символов, разницей между двумя словамиLevenshtein Distanceминимальное количество односимвольных правок (вставок, удалений или замен), необходимых для преобразования одного слова в другое.Levenshtein DistanceОн был изобретен в 1965 году советским математиком Владимиром Левенштейном.Levenshtein DistanceТакже известно как расстояние редактирования (Edit Distance),пройти черездинамическое программированиеРешите, временная сложностьO(M*N).

Определение: для двух строкa、b, то ихLevenshtein Distanceдля:

Пример: строкаaа такжеb,a=“abcde” ,b=“cabef”, согласно формуле расчета, приведенной выше, ихLevenshtein DistanceПроцесс расчета выглядит следующим образом:

этой статьиdemoИспользуйте плагиныlist-diff2Алгоритмы сравниваются, временная сложность алгоритма великаO(n*m), хотя этот алгоритм не является оптимальным, он используется дляdomДостаточно обычных манипуляций с элементами. Конкретный процесс реализации алгоритма здесь подробно описываться не будет.GitHub.com/Ли ВО РАН/История…

(4) Примеры вывода

два виртуальныхDOMОбъект показан на рисунке ниже, гдеul1представляет собой исходный виртуальныйDOMДерево,ul2представляет измененный виртуальныйDOMДерево

var ul1 = el('div',{id:'virtual-dom'},[
  el('p',{},['Virtual DOM']),
  el('ul', { id: 'list' }, [
	el('li', { class: 'item' }, ['Item 1']),
	el('li', { class: 'item' }, ['Item 2']),
	el('li', { class: 'item' }, ['Item 3'])
  ]),
  el('div',{},['Hello World'])
]) 
var ul2 = el('div',{id:'virtual-dom'},[
  el('p',{},['Virtual DOM']),
  el('ul', { id: 'list' }, [
	el('li', { class: 'item' }, ['Item 21']),
	el('li', { class: 'item' }, ['Item 23'])
  ]),
  el('p',{},['Hello World'])
]) 
var patches = diff(ul1,ul2);
console.log('patches:',patches);

Смотрим на вывод двух манекеновDOMОбъект различия между объектами показан на рисунке ниже, мы можем получить его через объект различия, два виртуальныхDOMКакие изменения были сделаны между объектами и, таким образом, в соответствии с этим объектом различия (patches), чтобы изменить исходное реальноеDOMструктуру, чтобы страницаDOMизменения структуры.

2.2.3, два виртуальныхDOMРазличия, применяемые к реальному объектуDOMДерево

(1) Обход в глубинуDOMДерево

​ Поскольку шаг 1 создаетJavaScriptдерево объектов иrenderвыходи настоящимDOM树的信息、结构是一样的。 Так что мы можемDOMТакже при обходе дерева в глубину обход, сгенерированный из двух шаговpatchesНайдите отличие текущего пройденного узла в объекте, как показано в следующем связанном коде:

function patch (node, patches) {
  var walker = {index: 0}
  dfsWalk(node, walker, patches)
}

function dfsWalk (node, walker, patches) {
  // 从patches拿出当前节点的差异
  var currentPatches = patches[walker.index]

  var len = node.childNodes
    ? node.childNodes.length
    : 0
  // 深度遍历子节点
  for (var i = 0; i < len; i++) {
    var child = node.childNodes[i]
    walker.index++
    dfsWalk(child, walker, patches)
  }
  // 对当前节点进行DOM操作
  if (currentPatches) {
    applyPatches(node, currentPatches)
  }
} 

(2) Для оригиналаDOMдерево нестиDOMдействовать

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

function applyPatches (node, currentPatches) {
  currentPatches.forEach(currentPatch => {
    switch (currentPatch.type) {
      case REPLACE:
        var newNode = (typeof currentPatch.node === 'string')
          ? document.createTextNode(currentPatch.node)
          : currentPatch.node.render()
        node.parentNode.replaceChild(newNode, node)
        break
      case REORDER:
        reorderChildren(node, currentPatch.moves)
        break
      case PROPS:
        setProps(node, currentPatch.props)
        break
      case TEXT:
        node.textContent = currentPatch.content
        break
      default:
        throw new Error('Unknown patch type ' + currentPatch.type)
    }
  })
} 

(3) Изменения структуры DOM

Объединив два полученных в разделе 2.2.2DOMРазница между объектами, примененная к первому (оригинальному)DOMструктура, мы можем видетьDOMСтруктура изменится, как и ожидалось, как показано на следующем изображении:

2.3 Заключение

Соответствующая реализация кода размещена на github. Заинтересованные студенты могут клонировать для запуска эксперимента. Адрес github:GitHub.com/ревматизм123/…

Virtual DOMАлгоритм в основном реализует три вышеуказанных шага для достижения:

  • использоватьJSмоделирование объектовDOMДерево -element.js

    <div id="virtual-dom">
    <p>Virtual DOM</p>
    <ul id="list">
      <li class="item">Item 1</li>
      <li class="item">Item 2</li>
      <li class="item">Item 3</li>
    </ul>
    <div>Hello World</div>
    </div> 
    
  • Сравните два виртуальных дереваDOMразница деревьев —diff.js

  • два виртуальныхDOMОтличие объекта применяется к реальномуDOMДерево -patch.js

    function applyPatches (node, currentPatches) {
      currentPatches.forEach(currentPatch => {
        switch (currentPatch.type) {
          case REPLACE:
            var newNode = (typeof currentPatch.node === 'string')
              ? document.createTextNode(currentPatch.node)
              : currentPatch.node.render()
            node.parentNode.replaceChild(newNode, node)
            break
          case REORDER:
            reorderChildren(node, currentPatch.moves)
            break
          case PROPS:
            setProps(node, currentPatch.props)
            break
          case TEXT:
            node.textContent = currentPatch.content
            break
          default:
            throw new Error('Unknown patch type ' + currentPatch.type)
        }
      })
    } 
    

три,Vueисходный кодVirtual-DOMКраткий анализ

Начнем со второй главы (Virtual-DOMосновы) уже освоилVirtual DOMсделать как реальныйDOMна самом деле испытатьVNodeОпределение,diff,patchи так далее, поэтому эта главаVueАнализ исходного кода также кратко анализируется в соответствии с этими процессами.

3.1,VNodeмоделированиеDOMДерево

3.1.1,VNodeКласс Анализ

существуетVue.jsсередина,Virtual DOMиспользуетсяVNodeэтоClassописать, он определяется вsrc/core/vdom/vnode.js, как видно из следующего блока кодаVue.jsсерединаVirtual DOMОпределение немного сложнее, потому что оно содержит многоVue.jsхарактеристики. ФактическиVue.jsсерединаVirtual DOMзаимствовано из библиотеки с открытым исходным кодомsnabbdomреализации, а затем добавить некоторыеVue.jsнекоторые характеристики.

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  devtoolsMeta: ?Object; // used to store functional render context for devtools
  fnScopeId: ?string; // functional scope id support

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }
}

Не здесь, потому чтоVNodeЕсли вас пугает такой атрибут, или вы скрипите зубами, чтобы узнать значение каждого атрибута, на самом деле мы в основном понимаем ключевые атрибуты его ядра, такие как:

  • tagатрибут этоvnodeатрибут тега
  • dataСвойство содержит окончательный рендер как реальныйdomПосле узла, на узлеclass,attribute,styleи связанные события
  • childrenсобственностьvnodeдочерний узел
  • textатрибут является текстовым атрибутом
  • elmатрибут для этогоvnodeсоответствующая реальностьdomузел
  • keyсобственностьvnodeОн отмечает, вdiffпроцесс может улучшитьdiffэффективность

3.1.2, создание исходного кодаVNodeОбработать

(1) Инициализировать vue

мы создаем экземплярvueэкземпляр, то естьnew Vue( )при фактическом выполненииsrc/core/instance/index.jsопределено вFunctionфункция.

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

просмотревVueизfunction,мы знаемVueтолько черезnewинициализация ключевого слова, затем вызовитеthis._initметод, который находится вsrc/core/instance/init.jsопределено в .

  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
      
    // 省略一系列其它初始化的代码
      
    if (vm.$options.el) {
      console.log('vm.$options.el:',vm.$options.el);
      vm.$mount(vm.$options.el)
    }
  }

(2)Vueмонтирование экземпляра

Vueчерез$mountметод экземпляра для монтированияdom, ниже анализируемcompilerверсияmountРеализация, соответствующий исходный код находится в каталогеsrc/platforms/web/entry-runtime-with-compiler.jsОпределено в файле: .

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  
   // 省略一系列初始化以及逻辑判断代码  
 
  return mount.call(this, el, hydrating)
}

Мы обнаружили, что в итоге мы вызвали исходный прототип с помощью$mountСпособ крепления на исходном прототипе$mountметод вsrc/platforms/web/runtime/index.jsОпределение определено.

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

мы обнаруживаем$mountметод действительно вызоветmountComponentметод, который определен вsrc/core/instance/lifecycle.jsв файле

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // 省略一系列其它代码
  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      // 生成虚拟 vnode   
      const vnode = vm._render()
      // 更新 DOM
      vm._update(vnode, hydrating)
     
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // 实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法  
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  return vm
}

Как видно из приведенного выше кода,mountComponentЯдро состоит в том, чтобы сначала создать экземпляр рендерингаWatcher, в своей функции обратного вызова вызоветupdateComponentметод, в этом вызове методаvm._renderМетод сначала создает виртуальный узел и, наконец, вызываетvm._updateвозобновитьDOM.

(3) Создайте виртуальный узел

Vueиз _renderМетод является закрытым методом экземпляра, который используется для рендеринга экземпляра как виртуального.Node. Это определено вsrc/core/instance/render.jsВ файле:

 Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options
    let vnode
    try {
      // 省略一系列代码  
      currentRenderingInstance = vm
      // 调用 createElement 方法来返回 vnode
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`){}
    }
    // set parent
    vnode.parent = _parentVnode
    console.log("vnode...:",vnode);
    return vnode
  }

Vue.jsиспользовать_createElementсоздание методаVNode, который определен вsrc/core/vdom/create-elemenet.js середина:

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
    
  // 省略一系列非主线代码
  
  if (normalizationType === ALWAYS_NORMALIZE) {
    // 场景是 render 函数不是编译生成的
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    // 场景是 render 函数是编译生成的
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // 创建虚拟 vnode
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

_createElementМетод имеет 5 параметров,contextПредставляет контекст VNode, которыйComponent Типы;tagПредставляет метку, которая может быть строкой илиComponent;dataПредставляет данные VNode, который являетсяVNodeDataтип, который можно найти вflow/vnode.jsНайдите его определение в ;childrenПредставляет собой детский узел текущего Vnode, который имеет любой тип и должен быть стандартизирован как стандартныйVNodeмножество;

3.1.3. Просмотр экземпляра

Для того, чтобы более интуитивно видеть то, что мы обычно пишемVueКак использовать кодVNodeКласс для представления, у нас есть более глубокое понимание через преобразование экземпляра.

Например, создать экземплярVueПример:

  var app = new Vue({
    el: '#app',
    render: function (createElement) {
      return createElement('div', {
        attrs: {
          id: 'app',
          class: "class_box"
        },
      }, this.message)
    },
    data: {
      message: 'Hello Vue!'
    }
  })

Мы распечатываем соответствующийVNodeВыражать:

3.2,diffОбработать

3.2.1,Vue.jsисходный кодdiffлогика вызова

Vue.jsИсходный код создает экземплярwatcher, ~ добавляется к зависимостям переменных, связанных в шаблоне, послеmodelОтветные данные в ответных данных изменились, ответные данные сохраненыdepмассив вызоветdep.notify()Метод выполняет всю работу, выполняемую обходом зависимостей, включая обновление представления, т.е.updateComponentвызов метода.watcherа такжеupdateComponentМетод определен вsrc/core/instance/lifecycle.jsв файле.

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // 省略一系列其它代码
  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      // 生成虚拟 vnode   
      const vnode = vm._render()
      // 更新 DOM
      vm._update(vnode, hydrating)
     
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // 实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法  
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  return vm
}

Для завершения обновления представления на самом деле нужно вызватьvm._updateметод, первый параметр, полученный этим методом, просто генерируетсяVnode, называетсяvm._updateМетод определен вsrc/core/instance/lifecycle.jsсередина.

  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    if (!prevVnode) {
      // 第一个参数为真实的node节点,则为初始化
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // 如果需要diff的prevVnode存在,那么对prevVnode和vnode进行diff
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
  }

Самое главное в этом методеvm.__patch__метод, который также является целымvirtual-domСреди них самый основной метод в основном завершаетprevVnodeа такжеvnodeизdiffобрабатывать и работать по мере необходимостиvdomпопадание в узелpatch, и, наконец, создать новый реальныйdomnode и завершите обновление представления.

Далее давайте посмотримvm.__patch__логический процессvm.__patch__метод определен вsrc/core/vdom/patch.jsсередина.

function patch (oldVnode, vnode, hydrating, removeOnly) {
    ......
    if (isUndef(oldVnode)) {
      // 当oldVnode不存在时,创建新的节点
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      // 对oldVnode和vnode进行diff,并对oldVnode打patch  
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } 
	......
  }
}

существуетpatchВ методе мы видим, что будет два случая, один, когдаoldVnodeКогда его нет, будет создан новый узел, другой уже будет существоватьoldVnode, тоoldVnodeа такжеvnodeпровестиdiffа такжеpatchпроцесс. вpatchпроцесс вызоветsameVnodeспособ сопряжения входящих 2vnodeСравнение основных свойств считается равным 2 только тогда, когда основные свойства совпадают.vnodeПроисходит только частичное обновление, а потом эти дваvnodeпровестиdiff, если 2vnodeЕсть несоответствие в основных свойствах , тогда оно будет пропущено напрямуюdiffпроцесса, а затем на основеvnodeсоздать настоящийdom, при удалении старогоdomузел.

function sameVnode (a, b) {
  return (
    a.key === b.key &&
    a.tag === b.tag &&
    a.isComment === b.isComment &&
    isDef(a.data) === isDef(b.data) &&
    sameInputType(a, b)
  )
}

diffПроцесс в основном осуществляется путем вызоваpatchVnodeметод:

  function patchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
    ...... 
    const elm = vnode.elm = oldVnode.elm
    const oldCh = oldVnode.children
    const ch = vnode.children
    // 如果vnode没有文本节点
    if (isUndef(vnode.text)) {
      // 如果oldVnode的children属性存在且vnode的children属性也存在  
      if (isDef(oldCh) && isDef(ch)) {
        // updateChildren,对子节点进行diff  
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(ch)
        }
        // 如果oldVnode的text存在,那么首先清空text的内容,然后将vnode的children添加进去  
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // 删除elm下的oldchildren
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        // oldVnode有子节点,而vnode没有,那么就清空这个节点  
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      // 如果oldVnode和vnode文本属性不同,那么直接更新真是dom节点的文本元素
      nodeOps.setTextContent(elm, vnode.text)
    }
    ......
  }

Из приведенного выше кода

diffВ процессе бывает несколько ситуаций.oldChдляoldVnodeдочерние узлы ,chдляVnodeдочерние узлы:

  • Сначала оцените текстовый узел, еслиoldVnode.text !== vnode.text, то текстовый узел будет заменен напрямую;
  • существуетvnodeВ случае отсутствия текстового узла введите дочерний узелdiff;
  • когдаoldChа такжеchЕсли оба существуют и не совпадают, вызовитеupdateChildrenна дочерних узлахdiff;
  • подобноoldChне существует,chсуществует, очистить сначалаoldVnodeтекстовый узел при вызовеaddVnodesметод будетchдобавить вelmреальностьdomсреди узлов;
  • подобноoldChсуществует,chне существует, удалитеelmпод настоящим узломoldChдочерний узел;
  • подобноoldVnodeимеет текстовые узлы, аvnodeНет, тогда очистите текстовый узел.

3.2.2, дочерние узлыdiffАнализ процесса

(1)Vue.jsисходный код

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

  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    // 为oldCh和newCh分别建立索引,为之后遍历的依据
    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, idxInOld, vnodeToMove, refElm

    // 直到oldCh或者newCh被遍历完后跳出循环
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }

в начале обходаdiffпрежде, сначала дайтеoldChа такжеnewChВыделите одинstartIndexа такжеendIndexкак индекс обхода, когдаoldChилиnewChПосле обхода (условие обходаoldChилиnewChизstartIndex >= endIndex), остановкаoldChа такжеnewChизdiffОбработать. Далее давайте посмотрим на всеdiffПроцесс (без свойств узлаkeyСлучай).

(2) НетkeyизdiffОбработать

Мы объясним приведенный выше процесс кода с помощью следующей схематической диаграммы:

(2.1) Сначала начните сравнение с первого узла, будь тоoldChещеnewChЗапуск или завершающий узел не существуетsameVnode, при этом атрибут узла не сkeyотмечено, поэтому первый раундdiffПосле завершения,newChизstartVnodeбыл добавлен вoldStartVnodeнапротивnewStartIndexдвигаться вперед на один;

图片描述

(2.2) Второй раундdiffсредний, доволенsameVnode(oldStartVnode, newStartVnode), поэтому для этих 2vnodeпровестиdiff, и наконецpatchударoldStartVnodeна, в то же времяoldStartVnodeа такжеnewStartIndexпродвинуться на одну позицию вперед;

图片描述

(2.3) Третий раундdiffсредний, доволенsameVnode(oldEndVnode, newStartVnode), то сначалаoldEndVnodeа такжеnewStartVnodeпровестиdiffи кoldEndVnodeпровестиpatch, и завершитьoldEndVnodeсменная операция и, наконец,newStartIndexдвигаться вперед на одно место,oldStartVnodeдвигаться назад на один;

图片描述

(2.4) Четвертый раундdiff, процесс такой же, как шаг 3;

图片描述

(2.5) Пятый раундdiff, то же, что процесс 1;

图片描述

(2.6) После завершения процесса обходаnewStartIdx > newEndIdx, что свидетельствует о том, что в это времяoldChЕсли есть избыточные узлы, то, наконец, эти избыточные узлы необходимо удалить.

图片描述

(3) Даkeyизdiffобработать

существуетvnodeБезkeyслучае, каждый раундdiffво время процесса起始а также结束узлы сравниваются до тех пор, покаoldChилиnewChбыл пройден. в то время как вvnodeпредставлятьkeyатрибут, в каждом раундеdiffпроцесс, когда起始а также结束узел не найденsameVnodeКогда тогда определяетnewStartVnodeЕсть лиkey, и будь тоoldKeyToIndxНайдите соответствующий узел в:

  • если этого не существуетkey, затем установите этоnewStartVnodeСоздан как новый узел и вставлен в исходныйrootв дочернем узле ;
  • если это существуетkey, затем выньтеoldChсуществование этогоkeyизvnode, а затем перейдите кdiffнад;

Благодаря приведенному выше анализуvdomдобавитьkeyПосле свойства повторитеdiffпроцесс, когдаотправная точка,конечная точкаизпоиска такжеdiffКогда совпадения по-прежнему нет, оно будет использованоkeyв качестве уникального идентификатора для выполненияdiff, чтобы он мог увеличиватьсяdiffэффективность.

с участиемKeyатрибутvnodeизdiffПроцесс можно увидеть на следующем рисунке:

(3.1) Сначала начните сравнение с первого узла, будь тоoldChещеnewChни начальный, ни конечный узел не существуетsameVnode, но атрибут узла сkeyотмечено, то вoldKeyToIndxНайдите соответствующий узел в первом раундеdiffпослеoldChВверхB节点был удален, ноnewChВверхB节点начальствоelmправо собственностиoldChначальствоB节点изelmЦитировать.

图片描述

(3.2) Второй раундdiffсредний, доволенsameVnode(oldStartVnode, newStartVnode), поэтому для этих 2vnodeпровестиdiff, и наконецpatchударoldStartVnodeна, в то же времяoldStartVnodeа такжеnewStartIndexпродвинуться на одну позицию вперед;

图片描述

(3.3) Третий раундdiffсредний, доволенsameVnode(oldEndVnode, newStartVnode), то сначалаoldEndVnodeа такжеnewStartVnodeпровестиdiff, и кoldEndVnodeпровестиpatch, и завершитьoldEndVnodeсменная операция и, наконец,newStartIndexдвигаться вперед на одно место,oldStartVnodeдвигаться назад на один;

图片描述

(3.4) Четвертый раундdiff, процесс такой же, как шаг 2;

图片描述

(3.5) Пятый раундdiff, потому что в это времяoldStartIndexбыло больше, чемoldEndIndex, поэтому оставшиесяVnodeОчередь вставляется в конец очереди.

图片描述

3.3,patchОбработать

введено через главу 3.2diffВ процессе мы увидимnodeOpsМетод корреляции с реальнымDOMструктура для работы,nodeOpsопределено вsrc/platforms/web/runtime/node-ops.js, что является основнымDOMПодробно операция здесь не описывается.

export function createElementNS (namespace: string, tagName: string): Element {
  return document.createElementNS(namespaceMap[namespace], tagName)
}

export function createTextNode (text: string): Text {
  return document.createTextNode(text)
}

export function createComment (text: string): Comment {
  return document.createComment(text)
}

export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
  parentNode.insertBefore(newNode, referenceNode)
}

export function removeChild (node: Node, child: Node) {
  node.removeChild(child)
}

3.4, резюме

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

4. Резюме

Эта статья из введения реальногоDOMЕго структура и процесс решения проблем, что приводит к тому, почему виртуальныйDOM; затем проанализируйте виртуальныйDOMПреимущества , а также реализация некоторых его теоретических основ и базовых алгоритмов, наконец, на основе освоенных нами базовых знаний мы проверим его шаг за шагом.Vue.jsКак реализован исходный код. От существующих проблем -> теоретическая база -> конкретная практика, шаг за шагом, чтобы помочь каждому лучше понять, что естьVirtual DOM, зачем нужноVirtual DOM,так же какVirtual DOMКонкретная реализация, я надеюсь, что эта статья будет вам полезна.

** Я много работал над написанием в течение долгого времени. Если это будет полезно для вас, я надеюсь вручную лайкнуть и поощрить ~~~~~~**

Адрес гитхаба:GitHub.com/ревматизм123/…, который обобщает все статьи в блоге автора, если вам нравится или вдохновляет, пожалуйста, помогите дать звезду ~, что также является поощрением для автора.

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

1. Технология Vue показала:США ТБ Huang Yi.GitHub.IO/v UE-Anarias…

2. Углубленный анализ: как реализовать алгоритм Virtual DOM:сегмент fault.com/ah/119000000…

3. Виртуальный DOM (vdom) ядра vue:woo woo Краткое описание.com/afraid/Afan 0 no 39860…

4. Краткий анализ виртуального дома (реализация Vue):сегмент fault.com/ah/119000001…