ホームページ  >  記事  >  ウェブフロントエンド  >  Vue での仮想 DOM の詳細な分析

Vue での仮想 DOM の詳細な分析

青灯夜游
青灯夜游転載
2022-08-08 19:07:031575ブラウズ

Vue での仮想 DOM の詳細な分析

仮想 DOM テクノロジーにより、ページのレンダリングがより効率的になり、ノード操作が軽減され、パフォーマンスが向上します。この記事では、vueVirtual DOM の技術原則と、Vue フレームワークの具体的な実装について詳しく分析します。 (学習ビデオ共有: vue ビデオ チュートリアル )

1. RealDOM とその解析プロセス

このセクションでは、主に実 DOM の解析プロセスを紹介し、その解析プロセスと既存の問題点を紹介することで、仮想 DOM が必要な理由につながります。百聞は一見に如かず。以下に示すのは webkit レンダリング エンジンのワークフロー図です

Vue での仮想 DOM の詳細な分析

## すべてのブラウザ レンダリング エンジンのワークフローは、大きく 5 つのステップに分かれています。

DOM ツリーの作成—> 作成スタイル ルール -> ビルドレンダリング ツリー—> レイアウトレイアウト --> 描画# # #絵画###。 最初のステップは DOM ツリーを構築することです: HTML アナライザーを使用して HTML 要素を分析し、DOM ツリーを構築します;

    2 番目のステップはスタイル シートを生成することです: CSS を使用しますアナライザー、CSS ファイルと要素のインライン スタイルを分析し、ページのスタイル シートを生成します。
  • 3 番目のステップは、レンダー ツリーを構築することです。DOM ツリーとスタイル シートを関連付けて、レンダーツリー(添付ファイル)。各 DOM ノードには、スタイル情報を受け取り、レンダー オブジェクト (レンダラとも呼ばれる) を返す Attach メソッドがあります。これらのレンダー オブジェクトは、最終的にレンダー ツリーに構築されます。
  • 4 番目のステップは、ノードを決定することです。座標: レンダー ツリーの構造に従って、レンダー ツリー上の各ノードのディスプレイに表示される正確な座標を決定します。
  • 5 番目のステップは、ページを描画することです。レンダー ツリーとノードに従って座標を表示します。 、各ノードのペイント メソッドを呼び出して描画します。
  • 注:

1.

DOM

ツリーの構築はドキュメントがロードされたときに開始されますか? DOM ツリーの構築は段階的なプロセスです。より良いユーザー エクスペリエンスを実現するために、レンダリング エンジンはできるだけ早くコンテンツを画面に表示します。全体が # 完了するまで待つ必要はありません。 ##HTML ドキュメントが解析され、その後初めて render ツリーとレイアウトの構築が開始されます。 2. Render

ツリーは、

DOM ツリーと CSS スタイル シートが構築された後に構築されますか? これら 3 つのプロセスは、実際に実行されるときは完全に独立しているわけではありませんが、ロード、解析、レンダリングが同時に行われ、重複します。 3. CSS

を解析するときに注意すべき点は何ですか?

CSS は右から左に逆に解析されます。ネストされたタグが多いほど、解析は遅くなります。 4. JS

実際の

DOM の運用コストはいくらですか? 従来の開発モデルであるネイティブ JS または JQ を使用して DOM を操作する場合、ブラウザは、 DOM ツリー。 1 回の操作で 10 個の DOM ノードを更新する必要があります。ブラウザは最初の DOM リクエストを受信した後、あと 9 個の更新操作があることを認識していないため、すぐに処理し、最終的に10回実行します。たとえば、最初の計算後、次の DOM 更新リクエストの直後に、このノードの座標値が変化し、前の計算は役に立ちません。 DOM ノードの座標値などの計算はパフォーマンスの無駄です。コンピューターのハードウェアが繰り返し更新されている場合でも、DOM の運用コストは依然として高価であり、頻繁な操作により依然としてページのフリーズが発生し、ユーザー エクスペリエンスに影響を及ぼします2。 Virtual -DOM

基本

2.1、仮想の利点DOM

VirtualDOM はブラウザのパフォーマンスの問題に対処するように設計されています。前と同様、1 回の操作で

DOM

を更新するアクションが 10 回ある場合、仮想 DOM はすぐに DOM を操作せず、10 回更新します。 #diff コンテンツをローカルの JS オブジェクトに保存し、最後に attch この JS オブジェクトを DOM ツリーに一度に保存します大量の不要な計算を避けるために、後続の操作を実行するまでの時間を短縮します。したがって、JS オブジェクトを使用して DOM ノードをシミュレートする利点は、すべてのページ更新を最初に JS オブジェクト (仮想 DOM#) に反映できることです。 ##) 一方、メモリ上で JS オブジェクトを操作する速度は明らかに速く、更新が完了すると、最後の JS オブジェクトが実際のオブジェクトにマッピングされます。 DOM.ブラウザに描画させます。 <h3 data-id="heading-4"><strong>2.2. アルゴリズムの実装</strong></h3> <h4 data-id="heading-5"> <strong>2.2.1. <code>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'])
])
Now

ul は、JavaScript オブジェクトを使用する DOM 構造です。表現するには、出力して表示しますul 対応するデータ構造は次のとおりです。

Vue での仮想 DOM の詳細な分析

(2) DOM## をレンダリングします。 JS # オブジェクト で表されますが、ページ上にそのような構造はありません。次に、

ul

を実際の にレンダリングする方法を紹介します。ページ上の DOM 構造と関連するレンダリング関数は次のとおりです。 <pre class="brush:php;toolbar:false">/**  * 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 }</pre> 上記の

render

メソッドを参照して、実際の DOM ノード ベースを構築します。 tagName で、このノードのプロパティを設定し、最後に独自の子ノードを再帰的に構築します。 構築した

DOM

構造をページ body に次のように追加します。 <pre class="brush:php;toolbar:false">ulRoot = ul.render(); document.body.appendChild(ulRoot);</pre>このようにして、ページ

body

内部には実際の DOM 構造があり、その効果は次の図に示すとおりです:

Vue での仮想 DOM の詳細な分析#2.2.2. 比較2 つの仮想

DOM

ツリーの違い - diff アルゴリズム diff

このアルゴリズムは、2 つの

間の違いを比較するために使用されます。仮想 DOM ツリー: 2 つのツリーの完全な比較が必要な場合、diff アルゴリズムの時間計算量は O(n^3) です。ただし、フロントエンドでは、DOM 要素をレベル間で移動することはほとんどないため、Virtual DOM は、次の図 div に示すように、同じレベルの要素のみを比較します。 は同じレベルの div とのみ比較され、2 番目のレベルは 2 番目のレベルとのみ比較されるため、アルゴリズムの複雑さは O(n) に達する可能性があります。 。

Vue での仮想 DOM の詳細な分析(1) 深さ優先トラバーサル、違いを記録します

実際のコードでは、古いツリーと新しいツリーは次のようになります。深さ優先トラバーサル。各ノードに一意のマークが付けられます。

深さ優先トラバーサルでは、ノードがトラバースされるたびに、ノードが新しいノードに追加されます。 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]

p を表し、patches[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

    。これをどう比較すればいいでしょうか?同じレベルで連続して比較すると、すべて置き換えられます。

    p

    div

    tagName

    が異なる場合、pdiv に置き換えられます。最終的には 3 つのノードすべてが置き換えられるため、DOM オーバーヘッドが非常に大きくなります。実際には、ノードを置き換える必要はなく、ノードを移動するだけでよく、移動方法さえわかれば十分です。 <p>        将这个问题抽象出来其实就是字符串的最小编辑距离问题(<code>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 <p><strong>(2)对原有 <code>DOM</code> 树进行 <code>DOM</code> 操作</strong></p><p>我们根据不同类型的差异对当前节点进行不同的 <code>DOM</code> 操作 ,例如如果进行了节点替换,就进行节点替换 <code>DOM</code> 操作;如果节点文本发生了改变,则进行文本替换的 <code>DOM</code> 操作;以及子节点重排、属性改变等 <code>DOM</code> 操作,相关代码如 <code>applyPatches</code> 所示 :</p><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)
        }
      })
    }

    (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 中国語 Web サイトの他の関連記事を参照してください。

    声明:
    この記事はjuejin.cnで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。