【深刻淺出】Vue3 虛擬 DOM

序言

首發在個人博客 深刻 Vue3 虛擬 DOMhtml

譯自:diving-into-the-vue-3s-virtual-dom-mediumvue

做者:Lachlan Millernode

此篇咱們將深刻 Vue3 虛擬 DOM,以及瞭解它是如何遍歷找到對應 vnode 的。git

多數狀況下咱們不須要考慮 Vue 組件內部是如何構成的。但有一些庫會幫助咱們理解,好比 Vue Test Utils 的 findComponent 函數。還有一個咱們都應該很熟悉的 Vue 開發工具 —— Vue DevTools,它顯示了應用的組件層次結構,而且咱們能夠對它進行編輯操做等。github

vuedevtool.png

咱們本篇要作的是:實現 Vue Test Utils API 的一部分,即 findComponent 函數。數組

設計 findComponent

首先,咱們都知道虛擬 DOM 是基於「提高性能」提出的,當數據發生變化時,Vue 會判斷此是否須要進行更新、或進行表達式的計算、或進行最終的 DOM 更新。markdown

好比這樣:app

- div 
  - span (show: true) 
    - 'Visible'
複製代碼

它的內部層次關係是:dom

HTMLDivElement -> HTMLSpanElement -> TextNode
複製代碼

若是 show 屬性變成 false。Vue 虛擬 DOM 會進行以下更新:async

- div 
  - span (show: false) 
    - 'Visible'
複製代碼

接着,Vue 會更新 DOM,移除'span' 元素。

那麼,咱們設想一下,findComponent 函數,它的調用可能會是相似這樣的結構:

const { createApp } = require('vue')

const App = {
  template: `
    <C>
      <B>
        <A />
      </B>
    </C>
  `
}

const app = createApp(App).mount('#app')

const component = findComponent(A, { within: app })

// 咱們經過 findComponent 方法找到了 <A/> 標籤。
複製代碼

打印 findComponent

接着,咱們先寫幾個簡單組件,以下:

// import jsdom-global. We need a global `document` for this to work.
require('jsdom-global')()
const { createApp, h } = require('vue')

// some components
const A = { 
  name: 'A',
  data() {
    return { msg: 'msg' }
  },
  render() {
    return h('div', 'A')
  }
}

const B = { 
  name: 'B',
  render() {
    return h('span', h(A))
  }
}

const C = { 
  name: 'C',
  data() {
    return { foo: 'bar' }
  },
  render() {
    return h('p', { id: 'a', foo: this.foo }, h(B))
  }
}

// mount the app!
const app = createApp(C).mount(document.createElement('div'))
複製代碼
  • 咱們須要在 Node.js v14+ 環境,由於咱們要用到 可選鏈。且須要安裝 Vue、jsdom 和 jsdom-global。

咱們能夠看到 A , B , C 三個組件,其中 A , C 組件有 data 屬性,它會幫助咱們深刻研究 VDOM。

你能夠打印試試:

console.log(app)
console.log(Object.keys(app))
複製代碼

結果爲 {},由於 Object.keys 只會顯示可枚舉的屬性。

咱們能夠嘗試打印隱藏的不可枚舉的屬性

console.log(app.$)
複製代碼

能夠獲得大量輸出信息:

<ref *1> { 
  uid: 0, 
  vnode: {
    __v_isVNode: true, 
    __v_skip: true, 
    type: { 
      name: 'C', 
      data: [Function: data], 
      render: [Function: render], 
      __props: [] 
  }, // hundreds of lines ...
複製代碼

再打印:

console.log(Object.keys(app.$))
複製代碼

輸出:

Press ENTER or type command to continue 
[ 
'uid', 'vnode', 'type', 'parent', 'appContext', 'root', 'next', 'subTree', 'update', 'render', 'proxy', 'withProxy', 'effects', 'provides', 'accessCache', 'renderCache', 'ctx', 'data', 'props', 'attrs', 'slots', 'refs', 'setupState', 'setupContext', 'suspense', 'asyncDep', 'asyncResolved', 'isMounted', 'isUnmounted', 'isDeactivated', 'bc', 'c', 'bm', 'm', 'bu', 'u', 'um', 'bum', 'da', 'a', 'rtg', 'rtc', 'ec', 'emit', 'emitted' 
]
複製代碼

咱們能夠看到一些很熟悉的屬性:好比 slotsdatasuspense 是一個新特性,emit 無需多言。還有好比 attrsbccbm 這些是生命週期鉤子:bcbeforeCreate, ccreated。也有一些內部惟一的生命週期鉤子,如 rtg,也就是 renderTriggered, 當 propsdata 發生變化時,用於更新操做,從而再渲染。

本篇咱們須要特別關注的是:vnodesubTreecomponenttypechildren

匹配 findComponent

來先看 vnode,它有不少屬性,咱們須要關注的是 typecomponent 這兩個。

// 打印 console.log(app.$.vnode.component)

console.log(app.$.vnode.component) 
<ref *1> { 
  uid: 0, 
  vnode: { 
    __v_isVNode: true, 
    __v_skip: true, 
    type: { 
      name: 'C', 
      data: [Function: data], 
      render: [Function: render], 
      __props: [] 
  }, // ... many more things ... } }
複製代碼

type 頗有意思!它與咱們以前定義的 C 組件同樣,咱們能夠看到它也有 [Function: data](咱們在前面定義了一個 msg 數據是咱們的查找目標)。實際上咱們嘗試能夠做如下打印:

console.log(C === app.$.vnode.component.type) //=> true
複製代碼

天吶!兩者居然是相等的!😮

console.log(C === app.$.vnode.type) //=> true
複製代碼

這樣也是相等的!😮

(你是否會疑問這兩個屬性爲何會指向同一個對象?這裏先暫且按下不表、自行探索。)

不管如何,咱們算是獲得了尋找到組件的途徑。

經過這裏的找尋過程,咱們還能再進一步獲得如下相等關係:

console.log( 
  app.$
  .subTree.children[0].component
  .subTree.children[0].component.type === A) //=> true
複製代碼

在本例中,div 節點的 subTree.children 數組長度是 2 。咱們知道了虛擬 DOM 的遞歸機制,就能夠沿着這個方向:subTree -> children -> component 來給出咱們的遞歸解決方案。

實現 findComponent

咱們首先實現 matches 函數,用於判斷是當前 vnode 節點和目標是否相等。

function matches(vnode, target) { 
  return vnode?.type === target
}
複製代碼

而後是 findComponent 函數,它是咱們調用並查找內部遞歸函數的公共 API。

function findComponent(comp, { within }) { 
  const result = find([within.$], comp) 
  if (result) { 
    return result 
  } 
}
複製代碼

此處的 find 方法的實現是咱們要重點討論的

咱們知道寫遞歸,最重要的是判斷何時結束 loop,因此 find 函數應該先是這樣的:

function find(vnodes, target) { 
  if (!Array.isArray(vnodes)) { 
    return 
  } 
}
複製代碼

而後,在遍歷 vnode 時,若是找到匹配的組件,則將其返回。若是找不到匹配的組件,則可能須要檢查 vnode.subTree.children 是否已定義,從而更深層次的查詢及匹配。最後,若是都沒有,咱們則返回累加器 acc。因此,代碼以下:

function find(vnodes, target) {
  if (!Array.isArray(vnodes)) {
    return 
  }

  return vnodes.reduce((acc, vnode) => {
    if (matches(vnode, target)) {
      return vnode
    }

    if (vnode?.subTree?.children) {
      return find(vnode.subTree.children, target)
    }

    return acc
  }, {})
}
複製代碼

若是你在 if (vnode?.subTree?.children) { 這裏進行一個打印 console.log,你能找到 B 組件,可是咱們的目標 A 組件的路徑以下:

app.$ 
  .subTree.children[0].component 
  .subTree.children[0].component.type === A) //=> true
複製代碼

因此咱們再次調用了 find 方法:find(vnode.subTree.children, target),在下一次迭代中查找的第一個參數將是app.$.subTree.children,它是 vnode 的數組。咱們不只須要檢查vnode.subTree.children,還須要檢查vnode.component.subTree

因此,最後 find 方法以下:

function find(vnodes, target) {
  if (!Array.isArray(vnodes)) {
    return 
  }

  return vnodes.reduce((acc, vnode) => {
    if (matches(vnode, target)) {
      return vnode
    }

    if (vnode?.subTree?.children) {
      return find(vnode.subTree.children, target)
    }

    if (vnode?.component?.subTree) {
      return find(vnode.component.subTree.children, target)
    }

    return acc
  }, {})
}
複製代碼

而後咱們再調用它:

const result = findComponent(A, { within: app })

console.log( result.component.proxy.msg ) // => 'msg'
複製代碼

咱們成功了!經過 findComponent,找到了 msg!

若是你之前使用過 Vue Test Utils,可能見過相似的東西 wrapper.vm.msg,它其實是在內部訪問 proxy(對於Vue 3)或 vm(對於Vue 2)。

小結

本篇的實現並不是完美,現實實現上還須要執行更多檢查。例如,若是使用 templateSuspense組件時,須要做更多判斷。不過這些你能夠在 Vue Test Utils 源碼 中能夠看到,但願能幫助你進一步理解虛擬 DOM。

本篇 源碼地址,小手一動、一下就懂~

好啦,以上就是本次分享~

若是喜歡,點贊關注👍👍👍~我是掘金安東尼,關注公衆號【掘金安東尼】,持續輸出ing!

相關文章
相關標籤/搜索