Vue中的虛擬DOM如何構建

這篇文章主要介紹了Vue中的虛擬DOM如何構建的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇Vue中的虛擬DOM如何構建文章都會有所收獲,下面我們一起來看看吧。

梅州ssl適用于網站、小程序/APP、API接口等需要進行數(shù)據(jù)傳輸應用場景,ssl證書未來市場廣闊!成為成都創(chuàng)新互聯(lián)的ssl證書銷售渠道,可以享受市場價格4-6折優(yōu)惠!如果有意向歡迎電話聯(lián)系或者加微信:13518219792(備注:SSL證書合作)期待與您的合作!

Vue中的虛擬DOM如何構建

虛擬DOM技術使得我們的頁面渲染的效率更高,減輕了節(jié)點的操作從而提高性能。

一、真實DOM和其解析流程

      本節(jié)我們主要介紹真實   DOM 的解析過程,通過介紹其解析過程以及存在的問題,從而引出為什么需要虛擬DOM。一圖勝千言,如下圖為 webkit 渲染引擎工作流程圖

Vue中的虛擬DOM如何構建

      所有的瀏覽器渲染引擎工作流程大致分為5步:創(chuàng)建        DOM 樹 —> 創(chuàng)建 Style Rules -> 構建 Render 樹 —> 布局 Layout -—> 繪制 Painting。

  • 第一步,構建 DOM 樹:用 HTML 分析器,分析 HTML 元素,構建一棵 DOM 樹;

  • 第二步,生成樣式表:用 CSS 分析器,分析 CSS 文件和元素上的 inline 樣式,生成頁面的樣式表;

  • 第三步,構建 Render 樹:將 DOM 樹和樣式表關聯(lián)起來,構建一棵 Render 樹(Attachment)。每個 DOM 節(jié)點都有 attach 方法,接受樣式信息,返回一個 render 對象(又名 renderer),這些 render 對象最終會被構建成一棵 Render 樹;

  • 第四步,確定節(jié)點坐標:根據(jù) Render 樹結構,為每個 Render 樹上的節(jié)點確定一個在顯示屏上出現(xiàn)的精確坐標;

  • 第五步,繪制頁面:根據(jù) Render 樹和節(jié)點顯示坐標,然后調用每個節(jié)點的 paint 方法,將它們繪制出來。

注意點:

1、DOM 樹的構建是文檔加載完成開始的?構建 DOM 樹是一個漸進過程,為達到更好的用戶體驗,渲染引擎會盡快將內容顯示在屏幕上,它不必等到整個 HTML 文檔解析完成之后才開始構建 render 樹和布局。

2、Render 樹是 DOM 樹和 CSS 樣式表構建完畢后才開始構建的?這三個過程在實際進行的時候并不是完全獨立的,而是會有交叉,會一邊加載,一邊解析,以及一邊渲染。

3、CSS 的解析注意點?CSS 的解析是從右往左逆向解析的,嵌套標簽越多,解析越慢。

4、JS 操作真實 DOM 的代價?用我們傳統(tǒng)的開發(fā)模式,原生 JSJQ 操作 DOM 時,瀏覽器會從構建 DOM 樹開始從頭到尾執(zhí)行一遍流程。在一次操作中,我需要更新 10 個 DOM 節(jié)點,瀏覽器收到第一個 DOM 請求后并不知道還有 9 次更新操作,因此會馬上執(zhí)行流程,最終執(zhí)行10 次。例如,第一次計算完,緊接著下一個 DOM 更新請求,這個節(jié)點的坐標值就變了,前一次計算為無用功。計算 DOM 節(jié)點坐標值等都是白白浪費的性能。即使計算機硬件一直在迭代更新,操作 DOM 的代價仍舊是昂貴的,頻繁操作還是會出現(xiàn)頁面卡頓,影響用戶體驗

二、Virtual-DOM 基礎

2.1、虛擬 DOM 的好處

      虛擬 DOM 就是為了解決瀏覽器性能問題而被設計出來的。如前,若一次操作中有 10 次更新 DOM 的動作,虛擬 DOM 不會立即操作 DOM,而是將這 10 次更新的 diff 內容保存到本地一個 JS 對象中,最終將這個 JS 對象一次性 attchDOM 樹上,再進行后續(xù)操作,避免大量無謂的計算量。所以,用 JS 對象模擬 DOM 節(jié)點的好處是,頁面的更新可以先全部反映在 JS 對象(虛擬 DOM )上,操作內存中的 JS 對象的速度顯然要更快,等更新完成后,再將最終的 JS 對象映射成真實的 DOM,交由瀏覽器去繪制。

2.2、算法實現(xiàn)

2.2.1、用 JS 對象模擬 DOM

(1)如何用 JS 對象模擬 DOM

例如一個真實的 DOM 節(jié)點如下:

<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 對象來表示 DOM 節(jié)點,使用對象的屬性記錄節(jié)點的類型、屬性、子節(jié)點等。

element.js 中表示節(jié)點對象代碼如下:

/**
 * Element virdual-dom 對象定義
 * @param {String} tagName - dom 元素名稱
 * @param {Object} props - dom 屬性
 * @param {Array<Element|String>} - 子節(jié)點
 */
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++
    })
    // 子元素個數(shù)
    this.count = count
}

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

module.exports = createElement;

根據(jù) 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'])
])

現(xiàn)在 ul 就是我們用  JavaScript 對象表示的 DOM 結構,我們輸出查看 ul 對應的數(shù)據(jù)結構如下:

Vue中的虛擬DOM如何構建

(2)渲染用 JS 表示的 DOM 對象

但是頁面上并沒有這個結構,下一步我們介紹如何將 ul 渲染成頁面上真實的 DOM 結構,相關渲染函數(shù)如下:

/**
 * render 將virdual-dom 對象渲染為實際 DOM 元素
 */
Element.prototype.render = function () {
    var el = document.createElement(this.tagName)
    var props = this.props
    // 設置節(jié)點的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() // 如果子節(jié)點也是虛擬DOM,遞歸構建DOM節(jié)點
            : document.createTextNode(child) // 如果字符串,只構建文本節(jié)點
        el.appendChild(childEl)
    })
    return el
}

我們通過查看以上 render 方法,會根據(jù)  tagName 構建一個真正的 DOM 節(jié)點,然后設置這個節(jié)點的屬性,最后遞歸地把自己的子節(jié)點也構建起來。

我們將構建好的 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)深度優(yōu)先遍歷,記錄差異

在實際的代碼中,會對新舊兩棵樹進行一個深度優(yōu)先的遍歷,這樣每個節(jié)點都會有一個唯一的標記:

Vue中的虛擬DOM如何構建

在深度優(yōu)先遍歷的時候,每遍歷到一個節(jié)點就把該節(jié)點和新的的樹進行對比。如果有差異的話就記錄到一個對象里面。

// diff 函數(shù),對比兩棵樹
function diff(oldTree, newTree) {
  var index = 0 // 當前節(jié)點的標志
  var patches = {} // 用來記錄每個節(jié)點差異的對象
  dfsWalk(oldTree, newTree, index, patches)
  return patches
}

// 對兩棵樹進行深度優(yōu)先遍歷
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) {
    // 節(jié)點相同,比較屬性
    var propsPatches = diffProps(oldNode, newNode)
    if (propsPatches) {
      currentPatch.push({ type: patch.PROPS, props: propsPatches })
    }
    // 比較子節(jié)點,如果子節(jié)點有'ignore'屬性,則不需要比較
    if (!isIgnoreChildren(newNode)) {
      diffChildren(
        oldNode.children,
        newNode.children,
        index,
        patches,
        currentPatch
      )
    }
  } else if(newNode !== null){
    // 新節(jié)點和舊節(jié)點不同,用 replace 替換
    currentPatch.push({ type: patch.REPLACE, node: newNode })
  }

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

從以上可以得出,patches[1] 表示 p ,patches[3] 表示 ul ,以此類推。

(2)差異類型

DOM 操作導致的差異類型包括以下幾種:

  • 節(jié)點替換:節(jié)點改變了,例如將上面的 div 換成 h2;

  • 順序互換:移動、刪除、新增子節(jié)點,例如上面 div 的子節(jié)點,把 pul 順序互換;

  • 屬性更改:修改了節(jié)點的屬性,例如把上面 liclass 樣式類刪除;

  • 文本改變:改變文本節(jié)點的文本內容,例如將上面 p 節(jié)點的文本內容更改為 “Real Dom”;

以上描述的幾種差異類型在代碼中定義如下所示:

var REPLACE = 0 // 替換原先的節(jié)點
var REORDER = 1 // 重新排序
var PROPS = 2 // 修改了節(jié)點的屬性
var TEXT = 3 // 文本內容改變

(3)列表對比算法

      子節(jié)點的對比算法,例如      p, ul, div 的順序換成了 div, p, ul。這個該怎么對比?如果按照同層級進行順序對比的話,它們都會被替換掉。如 pdivtagName 不同,p 會被 div 所替代。最終,三個節(jié)點都會被替換,這樣 DOM 開銷就非常大。而實際上是不需要替換節(jié)點,而只需要經過節(jié)點移動就可以達到,我們只需知道怎么進行移動。

      將這個問題抽象出來其實就是字符串的最小編輯距離問題(Edition Distance),最常見的解決方法是 Levenshtein Distance , Levenshtein Distance 是一個度量兩個字符序列之間差異的字符串度量標準,兩個單詞之間的 Levenshtein Distance 是將一個單詞轉換為另一個單詞所需的單字符編輯(插入、刪除或替換)的最小數(shù)量。Levenshtein Distance 是1965年由蘇聯(lián)數(shù)學家 Vladimir Levenshtein 發(fā)明的。Levenshtein Distance 也被稱為編輯距離(Edit Distance),通過動態(tài)規(guī)劃求解,時間復雜度為 O(M*N)。

定義:對于兩個字符串 a、b,則他們的 Levenshtein Distance 為:

Vue中的虛擬DOM如何構建

示例:字符串 aba=“abcde” ,b=“cabef”,根據(jù)上面給出的計算公式,則他們的 Levenshtein Distance 的計算過程如下:

Vue中的虛擬DOM如何構建

本文的 demo 使用插件 list-diff2 算法進行比較,該算法的時間復雜度偉 O(n*m),雖然該算法并非最優(yōu)的算法,但是用于對于 dom 元素的常規(guī)操作是足夠的。該算法具體的實現(xiàn)過程這里不再詳細介紹,該算法的具體介紹可以參照:github.com/livoras/lis…

(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 對象之間進行了哪些變化,從而根據(jù)這個差異對象(patches)更改原先的真實 DOM 結構,從而將頁面的 DOM 結構進行更改。

Vue中的虛擬DOM如何構建

2.2.3、將兩個虛擬 DOM 對象的差異應用到真正的 DOM

(1)深度優(yōu)先遍歷 DOM

      因為步驟一所構建的         JavaScript 對象樹和 render 出來真正的 DOM 樹的信息、結構是一樣的。所以我們可以對那棵 DOM 樹也進行深度優(yōu)先的遍歷,遍歷的時候從步驟二生成的 patches 對象中找出當前遍歷的節(jié)點差異,如下相關代碼所示:

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

function dfsWalk (node, walker, patches) {
  // 從patches拿出當前節(jié)點的差異
  var currentPatches = patches[walker.index]

  var len = node.childNodes
    ? node.childNodes.length
    : 0
  // 深度遍歷子節(jié)點
  for (var i = 0; i < len; i++) {
    var child = node.childNodes[i]
    walker.index++
    dfsWalk(child, walker, patches)
  }
  // 對當前節(jié)點進行DOM操作
  if (currentPatches) {
    applyPatches(node, currentPatches)
  }
}

(2)對原有 DOM 樹進行 DOM 操作

我們根據(jù)不同類型的差異對當前節(jié)點進行不同的 DOM 操作 ,例如如果進行了節(jié)點替換,就進行節(jié)點替換 DOM 操作;如果節(jié)點文本發(fā)生了改變,則進行文本替換的 DOM 操作;以及子節(jié)點重排、屬性改變等 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、結語

相關代碼實現(xiàn)已經放到 github 上面,有興趣的同學可以clone運行實驗,github地址為:github.com/fengshi123/…

Virtual DOM 算法主要實現(xiàn)上面三個步驟來實現(xiàn):

  • 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

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 簡析

我們從第二章節(jié)(Virtual-DOM 基礎)中已經掌握 Virtual DOM 渲染成真實的 DOM 實際上要經歷 VNode 的定義、diff、patch 等過程,所以本章節(jié) 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 的實現(xiàn),然后加入了一些 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節(jié)點后,節(jié)點上的class,attributestyle以及綁定的事件

  • children 屬性是vnode的子節(jié)點

  • text 屬性是文本屬性

  • elm 屬性為這個vnode對應的真實dom節(jié)點

  • key 屬性是vnode的標記,在diff過程中可以提高diff的效率

3.1.2、源碼創(chuàng)建 VNode 過程

(1)初始化vue

我們在實例化一個 vue 實例,也即 new Vue( ) 時,實際上是執(zhí)行 src/core/instance/index.js  中定義的 Function 函數(shù)。

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 實現(xiàn),相關源碼在目錄 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)
}

我們發(fā)現(xiàn)最終還是調用用原先原型上的 $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)
}

我們發(fā)現(xiàn)$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,在它的回調函數(shù)中會調用 updateComponent 方法  
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  return vm
}

從上面的代碼可以看到,mountComponent 核心就是先實例化一個渲染Watcher,在它的回調函數(shù)中會調用 updateComponent 方法,在此方法中調用 vm._render 方法先生成虛擬 Node,最終調用 vm._update 更新 DOM。

(3)創(chuàng)建虛擬 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 方法創(chuàng)建 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 函數(shù)不是編譯生成的
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    // 場景是 render 函數(shù)是編譯生成的
    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)) {
      // 創(chuàng)建虛擬 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 個參數(shù),context 表示 VNode 的上下文環(huán)境,它是 Component 類型;tag表示標簽,它可以是一個字符串,也可以是一個 Component;data 表示 VNode 的數(shù)據(jù),它是一個 VNodeData 類型,可以在 flow/vnode.js 中找到它的定義;children 表示當前 VNode 的子節(jié)點,它是任意類型的,需要被規(guī)范為標準的 VNode 數(shù)組;

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 表示:

Vue中的虛擬DOM如何構建

3.2、diff 過程

3.2.1、Vue.js 源碼的 diff 調用邏輯

Vue.js 源碼實例化了一個 watcher,這個 ~ 被添加到了在模板當中所綁定變量的依賴當中,一旦 model 中的響應式的數(shù)據(jù)發(fā)生了變化,這些響應式的數(shù)據(jù)所維護的 dep 數(shù)組便會調用 dep.notify() 方法完成所有依賴遍歷執(zhí)行的工作,這包括視圖的更新,即 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,在它的回調函數(shù)中會調用 updateComponent 方法  
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  return vm
}

完成視圖的更新工作事實上就是調用了vm._update方法,這個方法接收的第一個參數(shù)是剛生成的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) {
      // 第一個參數(shù)為真實的node節(jié)點,則為初始化
      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 過程并根據(jù)需要操作的 vdom 節(jié)點打 patch,最后生成新的真實 dom 節(jié)點并完成視圖的更新工作。

接下來,讓我們看下 vm.__patch__的邏輯過程, vm.__patch__ 方法定義在 src/core/vdom/patch.js 中。

function patch (oldVnode, vnode, hydrating, removeOnly) {
    ......
    if (isUndef(oldVnode)) {
      // 當oldVnode不存在時,創(chuàng)建新的節(jié)點
      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 不存在時,會創(chuàng)建新的節(jié)點;另一種則是已經存在 oldVnode ,那么會對 oldVnodevnode 進行 diffpatch 的過程。其中 patch 過程中會調用 sameVnode 方法來對對傳入的2個 vnode 進行基本屬性的比較,只有當基本屬性相同的情況下才認為這個2個vnode 只是局部發(fā)生了更新,然后才會對這2個 vnode 進行 diff,如果2個 vnode 的基本屬性存在不一致的情況,那么就會直接跳過 diff 的過程,進而依據(jù) vnode 新建一個真實的 dom,同時刪除老的 dom節(jié)點。

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沒有文本節(jié)點
    if (isUndef(vnode.text)) {
      // 如果oldVnode的children屬性存在且vnode的children屬性也存在  
      if (isDef(oldCh) && isDef(ch)) {
        // updateChildren,對子節(jié)點進行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有子節(jié)點,而vnode沒有,那么就清空這個節(jié)點  
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      // 如果oldVnode和vnode文本屬性不同,那么直接更新真是dom節(jié)點的文本元素
      nodeOps.setTextContent(elm, vnode.text)
    }
    ......
  }

從以上代碼得知,

diff 過程中又分了好幾種情況,oldCholdVnode的子節(jié)點,chVnode的子節(jié)點:

  • 首先進行文本節(jié)點的判斷,若 oldVnode.text !== vnode.text,那么就會直接進行文本節(jié)點的替換;

  • vnode  沒有文本節(jié)點的情況下,進入子節(jié)點的 diff;

  • oldChch 都存在且不相同的情況下,調用 updateChildren 對子節(jié)點進行 diff;

  • oldCh不存在,ch 存在,首先清空 oldVnode 的文本節(jié)點,同時調用 addVnodes 方法將 ch 添加到elm真實 dom 節(jié)點當中;

  • oldCh存在,ch不存在,則刪除 elm 真實節(jié)點下的 oldCh 子節(jié)點;

  • oldVnode 有文本節(jié)點,而 vnode 沒有,那么就清空這個文本節(jié)點。

3.2.2、子節(jié)點 diff 流程分析

(1)Vue.js 源碼

      這里著重分析下updateChildren方法,它也是整個 diff 過程中最重要的環(huán)節(jié),以下為 Vue.js 的源碼過程,為了更形象理解 diff 過程,我們給出相關的示意圖來講解。

  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    // 為oldCh和newCh分別建立索引,為之后遍歷的依據(jù)
    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被遍歷完后跳出循環(huán)
    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 前,首先給 oldChnewCh 分別分配一個 startIndexendIndex 來作為遍歷的索引,當oldCh 或者 newCh 遍歷完后(遍歷完的條件就是 oldCh 或者 newCh 標題名稱:Vue中的虛擬DOM如何構建
網址分享:http://bm7419.com/article6/ggooog.html

成都網站建設公司_創(chuàng)新互聯(lián),為您提供網頁設計公司、品牌網站設計ChatGPT、響應式網站、電子商務品牌網站制作

廣告

聲明:本網站發(fā)布的內容(圖片、視頻和文字)以用戶投稿、用戶轉載內容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內容未經允許不得轉載,或轉載時需注明來源: 創(chuàng)新互聯(lián)