>  기사  >  웹 프론트엔드  >  Vue의 가상 DOM에 대한 심층 분석

Vue의 가상 DOM에 대한 심층 분석

青灯夜游
青灯夜游앞으로
2022-08-08 19:07:031647검색

Vue의 가상 DOM에 대한 심층 분석

Virtual DOM 기술을 사용하면 페이지 렌더링의 효율성이 향상되어 노드 작업이 줄어들고 성능이 향상됩니다. 이 글에서는 vueVirtual DOM 기술 원리와 Vue 프레임워크의 구체적인 구현에 대한 심층 분석을 제공합니다. (학습 동영상 공유: vue 동영상 튜토리얼Virtual DOM的技术原理和 Vue 框架的具体实现。(学习视频分享:vue视频教程

一、真实DOM和其解析流程

        本节我们主要介绍真实   DOM 的解析过程,通过介绍其解析过程以及存在的问题,从而引出为什么需要虚拟DOM。一图胜千言,如下图为 webkit 渲染引擎工作流程图

Vue의 가상 DOM에 대한 심층 분석

        所有的浏览器渲染引擎工作流程大致分为5步:创建        DOM 树 —> 创建 Style Rules -> 构建 Render 树 —> 布局 Layout -—> 绘制 Painting

  • 第一步,构建 DOM 树:用 HTML 分析器,分析 HTML 元素,构建一棵 DOM 树;
  • 第二步,生成样式表:用 CSS 分析器,分析 CSS 文件和元素上的 inline 样式,生成页面的样式表;
  • 第三步,构建 Render 树:将 DOM 树和样式表关联起来,构建一棵 Render 树(Attachment)。每个 DOM 节点都有 attach 方法,接受样式信息,返回一个 render 对象(又名 renderer),这些 render 对象最终会被构建成一棵 Render 树;
  • 第四步,确定节点坐标:根据 Render 树结构,为每个 Render 树上的节点确定一个在显示屏上出现的精确坐标;
  • 第五步,绘制页面:根据 Render 树和节点显示坐标,然后调用每个节点的 paint 方法,将它们绘制出来。

注意点:

1、DOM 树的构建是文档加载完成开始的? 构建 DOM 树是一个渐进过程,为达到更好的用户体验,渲染引擎会尽快将内容显示在屏幕上,它不必等到整个 HTML 文档解析完成之后才开始构建 render 树和布局。

2、Render 树是 DOM 树和 CSS 样式表构建完毕后才开始构建的? 这三个过程在实际进行的时候并不是完全独立的,而是会有交叉,会一边加载,一边解析,以及一边渲染。

3、CSS 的解析注意点? CSS 的解析是从右往左逆向解析的,嵌套标签越多,解析越慢。

4、JS 操作真实 DOM 的代价? 用我们传统的开发模式,原生 JSJQ 操作 DOM 时,浏览器会从构建 DOM 树开始从头到尾执行一遍流程。在一次操作中,我需要更新 10 个 DOM 节点,浏览器收到第一个 DOM 请求后并不知道还有 9 次更新操作,因此会马上执行流程,最终执行10 次。例如,第一次计算完,紧接着下一个 DOM 更新请求,这个节点的坐标值就变了,前一次计算为无用功。计算 DOM 节点坐标值等都是白白浪费的性能。即使计算机硬件一直在迭代更新,操作 DOM 的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户体验

二、Virtual-DOM 基础

2.1、虚拟 DOM 的好处

        虚拟 DOM 就是为了解决浏览器性能问题而被设计出来的。如前,若一次操作中有 10 次更新 DOM 的动作,虚拟 DOM 不会立即操作 DOM,而是将这 10 次更新的 diff 内容保存到本地一个 JS 对象中,最终将这个 JS 对象一次性 attchDOM 树上,再进行后续操作,避免大量无谓的计算量。所以,用 JS 对象模拟 DOM 节点的好处是,页面的更新可以先全部反映在 JS 对象(虚拟 DOM )上,操作内存中的 JS 对象的速度显然要更快,等更新完成后,再将最终的 JS 对象映射成真实的 DOM)

1. 실제 DOM 및 해당 구문 분석 프로세스

🎜 이 섹션에서는 주로 실제 를 소개합니다. >DOM 의 구문 분석 프로세스는 구문 분석 프로세스와 기존 문제를 소개함으로써 가상 DOM이 필요한 이유를 알려줍니다. 천 마디 말보다 한 장의 사진이 좋습니다. 아래 그림은 webkit 렌더링 엔진🎜

Vue의 가상 DOM에 대한 심층 분석🎜🎜 모든 브라우저 렌더링 엔진 워크플로는 대략 5단계로 나뉩니다. DOM 트리 —> 스타일 규칙 생성 -> 렌더링 트리 빌드 —> 그림을 그립니다. 🎜

  • 첫 번째 단계, DOM 트리 구축: HTML 파서를 사용하여 HTML 요소를 분석하고 DOM 트리 구축
  • 두 번째 단계, 스타일 시트 생성: CSS 파서 사용, CSS 분석 페이지의 스타일 시트를 생성하기 위한 요소의 파일 및 인라인 스타일
  • 세 번째 단계는 렌더 트리를 구축하는 것입니다. DOM 트리를 스타일 시트와 연결하여 렌더 트리(첨부 파일)를 구축합니다. 각 DOM 노드에는 스타일 정보를 받아들이고 렌더 객체(렌더러라고도 함)를 반환하는 연결 메소드가 있습니다. 이러한 렌더 객체는 결국 렌더 트리로 구성됩니다.
  • 네 번째 단계는 노드 좌표: 렌더 트리 구조에 따라 렌더 트리의 각 노드에 대한 디스플레이에 나타나는 정확한 좌표를 결정합니다.
  • 다섯 번째 단계는 페이지를 그리는 것입니다. 렌더 트리에 따라 좌표를 표시합니다. 트리와 노드를 호출한 다음 각각을 호출합니다. 노드의 페인트 메소드가 이를 그립니다.
🎜참고: 🎜🎜1. 문서가 로드될 때 DOM 트리 구성이 시작됩니까? DOM 트리 구축은 더 나은 사용자 경험을 달성하기 위해 점진적인 프로세스로, 렌더링 엔진은 가능한 한 빨리 콘텐츠를 화면에 표시합니다. 전체 HTML 렌더링 트리 및 레이아웃 구성은 문서 구문 분석이 완료된 후에만 시작됩니다. 🎜🎜2. DOM 트리와 CSS 스타일 시트가 빌드된 후에 렌더링 트리가 빌드됩니다. 이 세 가지 프로세스는 실제 구현에서 완전히 독립적이지는 않지만 동시에 중첩, 로드, 구문 분석 및 렌더링됩니다. 🎜🎜3. CSS를 구문 분석할 때 주의해야 할 점은 무엇인가요? CSS는 오른쪽에서 왼쪽으로 역으로 구문 분석됩니다. 중첩된 태그가 많을수록 구문 분석 속도가 느려집니다. 🎜🎜4. JS로 실제 DOM을 운영하는 데 드는 비용은 얼마인가요? 기존 개발 모델인 기본 JS 또는 JQ를 사용하여 DOM을 작동하는 경우 브라우저는 DOM 트리 구축부터 시작합니다. 스크래치를 사용하여 프로세스를 한 번 실행합니다. 한 작업에서 10개의 DOM 노드를 업데이트해야 합니다. 브라우저가 첫 번째 DOM 요청을 받은 후에는 9개의 업데이트 작업이 더 있다는 것을 알지 못합니다. 즉시 프로세스를 실행하고 최종적으로 10번 실행합니다. 예를 들어 첫 번째 계산 후 다음 DOM 업데이트 요청 직후 이 노드의 좌표 값이 변경되므로 이전 계산은 쓸모가 없습니다. DOM 노드 좌표 값 등을 계산하는 것은 성능 낭비입니다. 컴퓨터 하드웨어가 반복적으로 업데이트되었음에도 불구하고 DOM 운영 비용은 여전히 ​​높으며 빈번한 작업으로 인해 페이지가 정지되어 사용자 경험에 영향을 미치게 됩니다🎜

2. 가상 DOM 기본

2.1. 가상 DOM 이점

🎜 가상 DOM은 브라우저 성능 문제를 해결하도록 설계되었습니다. 이전과 마찬가지로 한 번의 작업으로 DOM을 업데이트하는 작업이 10개 있으면 가상 DOMDOM을 즉시 작동시키지 않고 10개를 업데이트합니다. times 업데이트된 diff 콘텐츠는 로컬 JS 개체에 저장되고 마지막으로 이 JS 개체는 attch에 저장됩니다. code>DOM 트리를 확인한 다음 불필요한 계산을 피하기 위해 후속 작업을 수행합니다. 따라서 JS 개체를 사용하여 DOM 노드를 시뮬레이션하면 모든 페이지 업데이트가 JS 개체(가상 DOM ), 메모리에서 <code>JS 객체를 작동하는 속도가 확실히 더 빨라졌습니다. 업데이트가 완료된 후 최종 JS 객체가 DOM 객체에 매핑됩니다. 실제 DOM은 브라우저에 맡겨져 그려집니다. 🎜

2.2. 알고리즘 구현

2.2.1. JS 객체를 사용하여 DOM 트리JS 对象模拟 DOM

(1)如何用 JS 对象模拟 DOM

例如一个真实的 DOM 节点如下:

<div>
<p>Virtual DOM</p>
<ul>
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul>
<div>Hello World</div>
</div>

我们用 JavaScript 对象来表示 DOM 节点,使用对象的属性记录节点的类型、属性、子节点等。

element.js 中表示节点对象代码如下:

/**
 * Element virdual-dom 对象定义
 * @param {String} tagName - dom 元素名称
 * @param {Object} props - dom 属性
 * @param {Array<element>} - 子节点
 */
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>

根据 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 对应的数据结构如下:

Vue의 가상 DOM에 대한 심층 분석

(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 结构,效果如下图所示:

Vue의 가상 DOM에 대한 심층 분석

2.2.2、比较两棵虚拟 DOM 树的差异 — diff 算法

diff 算法用来比较两棵 Virtual DOM 树的差异,如果需要两棵树的完全比较,那么 diff 算法的时间复杂度为O(n^3)。但是在前端当中,你很少会跨越层级地移动 DOM 元素,所以 Virtual DOM 只会对同一个层级的元素进行对比,如下图所示, div 只会和同一层级的 div 对比,第二层级的只会跟第二层级对比,这样算法复杂度就可以达到 O(n)

Vue의 가상 DOM에 대한 심층 분석

(1)深度优先遍历,记录差异

在实际的代码中,会对新旧两棵树进行一个深度优先的遍历,这样每个节点都会有一个唯一的标记:

Vue의 가상 DOM에 대한 심층 분석

在深度优先遍历的时候,每遍历到一个节点就把该节点和新的的树进行对比。如果有差异的话就记录到一个对象里面。

// 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] 表示 ppatches[3] 表示 ul ,以此类推。

(2)差异类型

DOM 操作导致的差异类型包括以下几种:

  • 节点替换:节点改变了,例如将上面的 div 换成 h1;
  • 顺序互换:移动、删除、新增子节点,例如上面 div 的子节点,把 pul 顺序互换;
  • 属性更改:修改了节点的属性,例如把上面 liclass 样式类删除;
  • 文本改变:改变文本节点的文本内容,例如将上面 p 节点的文本内容更改为 “Real Dom”;

以上描述的几种差异类型在代码中定义如下所示:

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

(3)列表对比算法

        子节点的对比算法,例如      p, ul, div 的顺序换成了 div, p, ul。这个该怎么对比?如果按照同层级进行顺序对比的话,它们都会被替换掉。如 pdivtagName 不同,p 会被 div 所替代。最终,三个节点都会被替换,这样 DOM

를 시뮬레이션합니다. JS 개체는 DOM 트리를 시뮬레이션합니다. 🎜🎜

예를 들어 실제 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);

우리는 JavaScript code> 객체는 DOM 노드를 나타내며 객체의 속성을 사용하여 노드의 유형, 속성, 하위 노드 등을 기록합니다. 🎜

element.js는 노드 개체 코드를 다음과 같이 나타냅니다. 🎜

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 <p><code>element</code> 개체의 설정에 따라 위의 <code>DOM 구조는 다음과 같이 간단하게 표현할 수 있습니다. 🎜<pre class="brush:php;toolbar:false">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)
    }
  })
}

이제 ulJavaScript 개체로 표현되는 DOM 구조입니다. 출력 및 보기ul

의 해당 데이터 구조는 다음과 같습니다. 🎜

Vue의 가상 DOM에 대한 심층 분석🎜

🎜 (2) DOM로 표현 >JS 객체 🎜🎜

그러나 페이지에는 그러한 구조가 없습니다. 다음으로 ul을 실제 DOM 구조로 렌더링하는 방법을 소개하겠습니다. 페이지 관련 렌더링 기능은 다음과 같습니다. 🎜

<div>
<p>Virtual DOM</p>
<ul>
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul>
<div>Hello World</div>
</div>

위의 render 메소드를 살펴보면 를 기반으로 실제 <code>DOM 노드를 구축해보겠습니다. tagName을 선택한 다음 이 노드의 속성을 설정하고 마지막으로 자체 하위 노드를 재귀적으로 구축합니다. 🎜

다음과 같이 구성된 DOM 구조를 페이지 body에 추가합니다. 🎜

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)
    }
  })
}

이런 방식으로 body 실제 <code>DOM 구조가 있고 그 효과는 아래와 같습니다: 🎜

Vue의 가상 DOM에 대한 심층 분석🎜

🎜2.2. 2. 두 트리 비교 가상 DOM 트리의 차이점 - diff 알고리즘 🎜

diff 알고리즘은 두 diff 알고리즘을 사용하여 두 code>가상 DOM 트리 차이, 두 트리의 완전한 비교가 필요한 경우 <code>diff 알고리즘의 시간 복잡도는 O(n^3)입니다. . 그러나 프런트엔드에서는 DOM 요소를 레벨 간에 이동하는 경우가 거의 없으므로 Virtual DOM은 아래 그림과 같이 동일한 레벨의 요소만 비교합니다. div는 동일한 수준의 div와만 비교되고, 두 번째 수준은 두 번째 수준하고만 비교되므로 알고리즘 복잡도는 O(n)에 도달할 수 있습니다. ). 🎜<p style="text-align: center;"><img src="https://img.php.cn/upload/image/750/596/636/165995588713430Vue%EC%9D%98%20%EA%B0%80%EC%83%81%20DOM%EC%97%90%20%EB%8C%80%ED%95%9C%20%EC%8B%AC%EC%B8%B5%20%EB%B6%84%EC%84%9D" title="165995588713430Vue의 가상 DOM에 대한 심층 분석" alt="Vue의 가상 DOM에 대한 심층 분석">🎜</p> <p>🎜(1) 깊이 우선 순회, 기록 차이🎜🎜</p> <p>실제 코드에서는 이전 트리와 새 트리에 대해 깊이 우선 순회가 수행되므로 각 노드에는 고유한 태그가 있습니다: 🎜</p> <p style="text-align: center;"><img src="https://img.php.cn/upload/image/836/195/375/1659955899825790%20.png%20" title="1659955899825790.png" alt="Vue의 가상 DOM에 대한 심층 분석">🎜</p> <p>깊이 우선 순회 중에 노드를 순회할 때마다 해당 노드는 새 트리와 비교됩니다. 차이점이 있으면 객체에 기록됩니다. 🎜</p> <pre class="brush:php;toolbar:false">export default class VNode {   tag: string | void;   data: VNodeData | void;   children: ?Array&lt;vnode&gt;;   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&lt;vnode&gt;,     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 &amp;&amp; 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   } }&lt;/vnode&gt;&lt;/vnode&gt;</pre> <p>위에서 <code>patches[1]p를 나타내고 patches[3]ul을 나타낸다는 결론을 내릴 수 있습니다. 코드> 등. 🎜

🎜(2) 차이 유형🎜🎜

DOM 작업으로 인해 발생하는 차이 유형은 다음과 같습니다. 🎜

  • 노드 교체: 노드가 변경됩니다. 예를 들어, 위의 divh1로 대체됩니다.
  • 시퀀스 교환: 위의 div와 같은 하위 노드 이동, 삭제, 추가 의 하위 노드는 pul의 순서를 교환합니다.
  • 속성 변경: 노드의 속성을 수정합니다. , 위의 li의 <code>class 스타일 클래스를 삭제합니다.
  • 텍스트 변경: 텍스트 노드의 텍스트 내용을 변경합니다. 위의 p 노드의 텍스트 콘텐츠를 "Real Dom"으로;

위에 설명된 여러 가지 차이점 유형은 코드에서 다음과 같이 정의됩니다. 다음: 🎜

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)
}

🎜(3) 목록 비교 알고리즘 🎜🎜

하위 노드에 대한 비교 알고리즘(예: p, ul, div 순서가 div로 대체됨) , p, ul. 이것을 어떻게 비교하나요? 동일한 레벨에서 순차적으로 비교하면 모두 교체됩니다. pdivtagName이 다른 경우 pdiv로 대체됩니다. >. 결국 세 개의 노드가 모두 교체되므로 DOM 오버헤드가 매우 높습니다. 사실 노드를 교체할 필요는 없고 노드만 이동하면 됩니다. 🎜

        将这个问题抽象出来其实就是字符串的最小编辑距离问题(Edition Distance),最常见的解决方法是 Levenshtein Distance , Levenshtein Distance 是一个度量两个字符序列之间差异的字符串度量标准,两个单词之间的 Levenshtein Distance 是将一个单词转换为另一个单词所需的单字符编辑(插入、删除或替换)的最小数量。Levenshtein Distance 是1965年由苏联数学家 Vladimir Levenshtein 发明的。Levenshtein Distance 也被称为编辑距离(Edit Distance),通过动态规划求解,时间复杂度为 O(M*N)

定义:对于两个字符串 a、b,则他们的 Levenshtein Distance 为:

Vue의 가상 DOM에 대한 심층 분석

示例:字符串 aba=“abcde” ,b=“cabef”,根据上面给出的计算公式,则他们的 Levenshtein Distance 的计算过程如下:

Vue의 가상 DOM에 대한 심층 분석

本文的 demo 使用插件 list-diff2 算法进行比较,该算法的时间复杂度伟 O(n*m),虽然该算法并非最优的算法,但是用于对于 dom 元素的常规操作是足够的。

该算法具体的实现过程这里不再详细介绍,该算法的具体介绍可以参照:https://github.com/livoras/list-diff

(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 结构进行更改。

Vue의 가상 DOM에 대한 심층 분석

2.2.3、将两个虚拟 DOM 对象的差异应用到真正的 DOM

(1)深度优先遍历 DOM

        因为步骤一所构建的         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 

(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.2 得到的两个 DOM 对象之间的差异,应用到第一个(原先)DOM 结构中,我们可以看到 DOM 结构进行了预期的变化,如下图所示:

Vue의 가상 DOM에 대한 심층 분석

2.3、结语

相关代码实现已经放到 github 上面,有兴趣的同学可以clone运行实验,github地址为:https://github.com/fengshi123/virtual-dom-example%E3%80%82

Virtual DOM 算法主要实现上面三个步骤来实现:

  • JS 对象模拟 DOM 树 — element.js

    <div>
    <p>Virtual DOM</p>
    <ul>
      <li>Item 1</li>
      <li>Item 2</li>
      <li>Item 3</li>
    </ul>
    <div>Hello World</div>
    </div>
  • 比较两棵虚拟 DOM 树的差异 — diff.js

Vue의 가상 DOM에 대한 심층 분석

  • 将两个虚拟 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 的定义、diffpatch 等过程,所以本章节 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.jsVirtual 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></vnode>

这里千万不要因为 VNode 的这么属性而被吓到,或者咬紧牙去摸清楚每个属性的意义,其实,我们主要了解其几个核心的关键属性就差不多了,例如:

  • tag 属性即这个vnode的标签属性
  • data 属性包含了最后渲染成真实dom节点后,节点上的classattributestyle以及绑定的事件
  • 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)
}

通过查看 Vuefunction,我们知道 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 方法先生成虚拟 Node,最终调用 vm._update 更新 DOM

(3)创建虚拟 Node

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()
  }
}</vnode></component>

_createElement 方法有 5 个参数,context 表示 VNode 的上下文环境,它是 Component 类型;tag表示标签,它可以是一个字符串,也可以是一个 Componentdata 表示 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 表示:

1Vue의 가상 DOM에 대한 심층 분석

3.2、diff 过程

3.2.1、Vue.js 源码的 diff 调用逻辑

Vue.js 源码实例化了一个 watcher,这个 ~ 被添加到了在模板当中所绑定变量的依赖当中,一旦 model 中的响应式的数据发生了变化,这些响应式的数据所维护的 dep 数组便会调用 dep.notify() 方法完成所有依赖遍历执行的工作,这包括视图的更新,即 updateComponent 方法的调用。watcherupdateComponent方法定义在  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 当中最为核心的方法,主要完成了prevVnodevnodediff 过程并根据需要操作的 vdom 节点打 patch,最后生成新的真实 dom 节点并完成视图的更新工作。

接下来,让我们看下 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 ,那么会对 oldVnodevnode 进行 diffpatch 的过程。其中 patch 过程中会调用 sameVnode 方法来对对传入的2个 vnode 进行基本属性的比较,只有当基本属性相同的情况下才认为这个2个vnode 只是局部发生了更新,然后才会对这2个 vnode 进行 diff,如果2个 vnode 的基本属性存在不一致的情况,那么就会直接跳过 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 过程中又分了好几种情况,oldCholdVnode的子节点,chVnode的子节点:

  • 首先进行文本节点的判断,若 oldVnode.text !== vnode.text,那么就会直接进行文本节点的替换;
  • vnode  没有文本节点的情况下,进入子节点的 diff
  • oldChch 都存在且不相同的情况下,调用 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) {
      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 前,首先给 oldChnewCh 分别分配一个 startIndexendIndex 来作为遍历的索引,当oldCh 或者 newCh 遍历完后(遍历完的条件就是 oldCh 或者 newChstartIndex >= endIndex ),就停止oldChnewChdiff 过程。接下来通过实例来看下整个 diff 的过程(节点属性中不带 key 的情况)。

(2)无 keydiff 过程

我们通过以下示意图对以上代码过程进行讲解:

(2.1)首先从第一个节点开始比较,不管是 oldCh 还是 newCh 的起始或者终止节点都不存在 sameVnode ,同时节点属性中是不带 key标记的,因此第一轮的 diff 完后,newChstartVnode 被添加到 oldStartVnode的前面,同时 newStartIndex前移一位;

1Vue의 가상 DOM에 대한 심층 분석

(2.2)第二轮的 diff中,满足 sameVnode(oldStartVnode, newStartVnode),因此对这2个 vnode 进行diff,最后将 patch 打到 oldStartVnode 上,同时 oldStartVnodenewStartIndex 都向前移动一位 ;

1Vue의 가상 DOM에 대한 심층 분석

(2.3)第三轮的 diff 中,满足 sameVnode(oldEndVnode, newStartVnode),那么首先对  oldEndVnodenewStartVnode 进行 diff,并对 oldEndVnode进行 patch,并完成  oldEndVnode 移位的操作,最后newStartIndex前移一位,oldStartVnode 后移一位;

1Vue의 가상 DOM에 대한 심층 분석

(2.4)第四轮的 diff中,过程同步骤3;

1Vue의 가상 DOM에 대한 심층 분석

(2.5)第五轮的 diff 中,同过程1;

1Vue의 가상 DOM에 대한 심층 분석

(2.6)遍历的过程结束后,newStartIdx > newEndIdx,说明此时 oldCh 存在多余的节点,那么最后就需要将这些多余的节点删除。

1Vue의 가상 DOM에 대한 심층 분석

(3)有 keydiff 流程

vnode 不带 key 的情况下,每一轮的 diff 过程当中都是起始结束节点进行比较,直到 oldCh 或者newCh 被遍历完。而当为 vnode 引入 key 属性后,在每一轮的 diff 过程中,当起始结束节点都没有找到sameVnode 时,然后再判断在 newStartVnode 的属性中是否有 key,且是否在 oldKeyToIndx 中找到对应的节点 :

  • 如果不存在这个 key,那么就将这个 newStartVnode作为新的节点创建且插入到原有的 root 的子节点中;
  • 如果存在这个 key,那么就取出 oldCh 中的存在这个 keyvnode,然后再进行 diff 的过;

通过以上分析,给vdom上添加 key属性后,遍历 diff 的过程中,当起始点结束点搜寻diff 出现还是无法匹配的情况下时,就会用 key 来作为唯一标识,来进行 diff,这样就可以提高 diff 效率。

带有 Key属性的 vnodediff 过程可见下图:

(3.1)首先从第一个节点开始比较,不管是 oldCh 还是 newCh 的起始或者终止节点都不存在 sameVnode,但节点属性中是带 key 标记的, 然后在 oldKeyToIndx 中找到对应的节点,这样第一轮 diff 过后 oldCh 上的B节点被删除了,但是 newCh 上的B节点elm 属性保持对 oldChB节点elm引用。

1Vue의 가상 DOM에 대한 심층 분석

(3.2)第二轮的 diff 中,满足 sameVnode(oldStartVnode, newStartVnode),因此对这2个 vnode 进行diff,最后将 patch 打到 oldStartVnode上,同时 oldStartVnodenewStartIndex 都向前移动一位 ;

1Vue의 가상 DOM에 대한 심층 분석

(3.3)第三轮的 diff中,满足 sameVnode(oldEndVnode, newStartVnode),那么首先对 oldEndVnodenewStartVnode 进行 diff,并对 oldEndVnode 进行 patch,并完成 oldEndVnode 移位的操作,最后newStartIndex 前移一位,oldStartVnode后移一位;

Vue의 가상 DOM에 대한 심층 분석

(3.4)第四轮的diff中,过程同步骤2;

2Vue의 가상 DOM에 대한 심층 분석

(3.5)第五轮的diff中,因为此时 oldStartIndex 已经大于 oldEndIndex,所以将剩余的 Vnode 队列插入队列最后。

2Vue의 가상 DOM에 대한 심층 분석

3.3、patch 过程

通过3.2章节介绍的 diff 过程中,我们会看到 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 到最终渲染的整个过程。

2Vue의 가상 DOM에 대한 심층 분석

四、总结

本文从通过介绍真实 DOM 结构其解析过程以及存在的问题,从而引出为什么需要虚拟 DOM;然后分析虚拟DOM 的好处,以及其一些理论基础和基础算法的实现;最后根据我们已经掌握的基础知识,再一步步去查看Vue.js 的源码如何实现的。从存在问题 —> 理论基础 —> 具体实践,一步步深入,帮助大家更好的了解什么是Virtual DOM、为什么需要 Virtual DOM、以及 Virtual DOM的具体实现,希望本文对您有帮助。

(学习视频分享:web前端开发编程基础视频

위 내용은 Vue의 가상 DOM에 대한 심층 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 juejin.cn에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제