溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Vue虛擬Dom到真實Dom如何轉(zhuǎn)換

發(fā)布時間:2022-10-13 11:44:08 來源:億速云 閱讀:172 作者:iii 欄目:開發(fā)技術(shù)

本篇內(nèi)容主要講解“Vue虛擬Dom到真實Dom如何轉(zhuǎn)換”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學(xué)習(xí)“Vue虛擬Dom到真實Dom如何轉(zhuǎn)換”吧!

再有一顆樹形結(jié)構(gòu)的Javascript對象后, 我們需要做的就是講這棵樹跟真實Dom樹形成映射關(guān)系。我們先回顧之前的mountComponnet 方法:

export function mountComponent(vm, el) {
  vm.$el = el
  ...
  callHook(vm, "beforeMount")
  ...
  const updateComponent = function () {
    vm._update(vm._render())
  }
  ...
}

我們已經(jīng)執(zhí)行完了vm._render 方法拿到了VNode, 現(xiàn)在將它作為參數(shù)傳給vm._update 方法并執(zhí)行。 vm._update這個方法的作用就是將VNode 轉(zhuǎn)為真實的Dom, 不過它有兩個執(zhí)行時機:

首次渲染

當(dāng)執(zhí)行new Vue 到此時就是首次渲染了, 會將傳入的Vnode對象映射為真實的Dom。

更新頁面

數(shù)據(jù)變化會驅(qū)動頁面發(fā)生變化, 這也是vue最獨特的特性之一, 數(shù)據(jù)改變之前和之后生成兩份VNode進行比較, 而怎么樣在舊的VNode上做最小的改動去渲染頁面,這樣一個diff算法還是挺復(fù)雜的。 如果再沒有先說清楚數(shù)據(jù)響應(yīng)式是怎么回事之前,直接將diff對理解vue 的整體流程不太好。 所以這章分析首次渲染后, 下一章就是數(shù)據(jù)響應(yīng)式, 之后才是diff比較。

先來看看vm._update方法的定義:

Vue.prototype._update = function(vnode) {
  ... 首次渲染
  vm.$el = vm.__patch__(vm.$el, vnode)  // 覆蓋原來的vm.$el
  ...
}

這里的 vm. e l 是 之 前 在 = = m o u n t C o m p o n e n t = = 方 法 內(nèi) 就 掛 載 的 , 一 個 真 實 的 = = D o m = = 元 素 。 首 次 渲 染 會 傳 入 v m . el 是之前在 ==mountComponent== 方法內(nèi)就掛載的, 一個真實的==Dom==元素。 首次渲染會傳入 vm. el是之前在==mountComponent==方法內(nèi)就掛載的,一個真實的==Dom==元素。首次渲染會傳入vm.el 以及得到的VNode, 所以看下vm.patch 定義:

Vue.prototype.__patch__ = createPatchFunction({ nodeOps, modules })

patch 是createPatchFunction 方法內(nèi)部返回的一個方法, 它接受一個對象:

nodeOps屬性:封裝了操作原生Dom 的一些方法的集合, 如:創(chuàng)建、插入,移除這些, 我們到使用的地方咋詳解。

modules 屬性: 創(chuàng)建真實Dom 也需要生成它的如class/attrs/style 等屬性。 modules 是一個數(shù)組集合,數(shù)組的每一項都是這些屬性對應(yīng)的鉤子方法, 這些屬性的創(chuàng)建,更新,銷毀等都有對應(yīng)鉤子方法。 當(dāng)某一時刻需要做某件事,執(zhí)行對應(yīng)的鉤子即可。 比如它們都有create 這個鉤子方法, 如將這些create 鉤子收集到一個數(shù)組內(nèi), 需要在真實Dom上創(chuàng)建這些屬性時,依次執(zhí)行數(shù)組的每一項,也就是依次創(chuàng)建了它們。

PS: 這里modules 屬性內(nèi)的鉤子方法是區(qū)分平臺的, web, weex 以及 SSR 它們調(diào)用VNode 方法方式并不相同, 所以vue在這里又使用了函數(shù)柯里化這個騷操作, 在createPatchFunction 內(nèi)將平臺的差異化磨平, 從而 patch 方法只用接收新舊node即可。

生成Dom

這里大家記住一句話即可, 無論VNode 是什么類型的節(jié)點, 只有三種類型的節(jié)點會被創(chuàng)建并插入到Dom中: 元素節(jié)點,注釋節(jié)點, 和文本節(jié)點。

我們接著看下createPatchFunction 它返回一個怎樣的方法:

export function createPatchFunction(backend) {
  ...
  const { modules, nodeOps } = backend  // 解構(gòu)出傳入的集合
  
  return function (oldVnode, vnode) {  // 接收新舊vnode
    ...
    const isRealElement = isDef(oldVnode.nodeType) // 是否是真實Dom
    if(isRealElement) {  // $el是真實Dom
      oldVnode = emptyNodeAt(oldVnode)  // 轉(zhuǎn)為VNode格式覆蓋自己
    }
    ...
  }
}

首次渲染時沒有oldVnode, oldVnode 就是 $el, 一個真實的dom, 經(jīng)過emptyNodeAt(odVnode) 方法包裝:

function emptyNodeAt(elm) {
  return new VNode(
    nodeOps.tagName(elm).toLowerCase(), // 對應(yīng)tag屬性
    {},  // 對應(yīng)data
    [],   // 對應(yīng)children
    undefined,  //對應(yīng)text
    elm  // 真實dom賦值給了elm屬性
  )
}

包裝后的:
{
  tag: "div",
  elm: "<div id="app"></div>" // 真實dom
}

-------------------------------------------------------

nodeOps:
export function tagName (node) {  // 返回節(jié)點的標簽名
  return node.tagName  
}

在將傳入的==$el== 屬性轉(zhuǎn)為了VNode 格式之后,我們繼續(xù):

export function createPatchFunction(backend) { 
  ...
  
  return function (oldVnode, vnode) {  // 接收新舊vnode
  
    const insertedVnodeQueue = []
    ...
    const oldElm = oldVnode.elm  //包裝后的真實Dom <div id="app"></div>
    const parentElm = nodeOps.parentNode(oldElm)  // 首次父節(jié)點為<body></body>
  	
    createElm(  // 創(chuàng)建真實Dom
      vnode, // 第二個參數(shù)
      insertedVnodeQueue,  // 空數(shù)組
      parentElm,  // <body></body>
      nodeOps.nextSibling(oldElm)  // 下一個節(jié)點
    )
    
    return vnode.elm // 返回真實Dom覆蓋vm.$el
  }
}
                                              
------------------------------------------------------

nodeOps:
export function parentNode (node) {  // 獲取父節(jié)點
  return node.parentNode 
}

export function nextSibling(node) {  // 獲取下一個節(jié)點
  return node.nextSibing  
}

createElm 方法開始生成真實的Dom, VNode 生成真實的Dom 的方式還是分為元素節(jié)點和組件兩種方式, 所以我們使用上一章生成的VNode分別說明。

1. 元素節(jié)點生成Dom

{  // 元素節(jié)點VNode
  tag: "div",
  children: [{
      tag: "h1",
      children: [
        {text: "title h1"}
      ]
    }, {
      tag: "h2",
      children: [
        {text: "title h2"}
      ]
    }, {
      tag: "h3",
      children: [
        {text: "title h3"}
      ]
    }
  ]
}

開始Dom, 來看下它的定義:

function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) { 
  ...
  const children = vnode.children  // [VNode, VNode, VNode]
  const tag = vnode.tag  // div
  
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return  // 如果是組件結(jié)果返回true,不會繼續(xù),之后詳解createComponent
  }
  
  if(isDef(tag)) {  // 元素節(jié)點
    vnode.elm = nodeOps.createElement(tag)  // 創(chuàng)建父節(jié)點
    createChildren(vnode, children, insertedVnodeQueue)  // 創(chuàng)建子節(jié)點
    insert(parentElm, vnode.elm, refElm)  // 插入
    
  } else if(isTrue(vnode.isComment)) {  // 注釋節(jié)點
    vnode.elm = nodeOps.createComment(vnode.text)  // 創(chuàng)建注釋節(jié)點
    insert(parentElm, vnode.elm, refElm); // 插入到父節(jié)點
    
  } else {  // 文本節(jié)點
    vnode.elm = nodeOps.createTextNode(vnode.text)  // 創(chuàng)建文本節(jié)點
    insert(parentElm, vnode.elm, refElm)  // 插入到父節(jié)點
  }
  
  ...
}

------------------------------------------------------------------

nodeOps:
export function createElement(tagName) {  // 創(chuàng)建節(jié)點
  return document.createElement(tagName)
}

export function createComment(text) {  //創(chuàng)建注釋節(jié)點
  return document.createComment(text)
}

export function createTextNode(text) {  // 創(chuàng)建文本節(jié)點
  return document.createTextNode(text)
}

function insert (parent, elm, ref) {  //插入dom操作
  if (isDef(parent)) {  // 有父節(jié)點
    if (isDef(ref)) { // 有參考節(jié)點
      if (ref.parentNode === parent) {  // 參考節(jié)點的父節(jié)點等于傳入的父節(jié)點
        nodeOps.insertBefore(parent, elm, ref)  // 在父節(jié)點內(nèi)的參考節(jié)點之前插入elm
      }
    } else {
      nodeOps.appendChild(parent, elm)  //  添加elm到parent內(nèi)
    }
  }  // 沒有父節(jié)點什么都不做
}
這算一個比較重要的方法,因為很多地方會用到。

依次判斷是否是元素節(jié)點, 注釋節(jié)點,文本節(jié)點, 分別創(chuàng)建它們?nèi)缓蟛迦氲礁腹?jié)點里面, 這里主要介紹創(chuàng)建元素節(jié)點, 另外兩個并沒有復(fù)雜的邏輯。 我們接下來看下:createChild 方法定義:

function createChild(vnode, children, insertedVnodeQueue) {
  if(Array.isArray(children)) {  // 是數(shù)組
    for(let i = 0; i < children.length; ++i) {  // 遍歷vnode每一項
      createElm(  // 遞歸調(diào)用
        children[i], 
        insertedVnodeQueue, 
        vnode.elm, 
        null, 
        true, // 不是根節(jié)點插入
        children, 
        i
      )
    }
  } else if(isPrimitive(vnode.text)) {  //typeof為string/number/symbol/boolean之一
    nodeOps.appendChild(  // 創(chuàng)建并插入到父節(jié)點
      vnode.elm, 
      nodeOps.createTextNode(String(vnode.text))
    )
  }
}

-------------------------------------------------------------------------------

nodeOps:
export default appendChild(node, child) {  // 添加子節(jié)點
  node.appendChild(child)
}

開始創(chuàng)建子節(jié)點, 遍歷VNode 的每一項, 每一項還是使用之前的createElm方法創(chuàng)建Dom。 如果某一項又是數(shù)組,繼續(xù)調(diào)用createChild創(chuàng)建某一項的子節(jié)點; 如果某一項不是數(shù)組, 創(chuàng)建文本節(jié)點并將它添加到父節(jié)點內(nèi)。 像這樣使用遞歸的形式將嵌套的VNode全部創(chuàng)建為真實的Dom。

簡單來說就是由里向外的挨個創(chuàng)建出真實的Dom, 然后插入到它的父節(jié)點內(nèi),最后將創(chuàng)建好的Dom插入到body內(nèi), 完成創(chuàng)建的過程, 元素節(jié)點的創(chuàng)建還是比較簡單的, 接下來看下組件式怎么創(chuàng)建的。

組件VNode生成Dom

{  // 組件VNode
  tag: "vue-component-1-app",
  context: {...},
  componentOptions: {
    Ctor: function(){...},  // 子組件構(gòu)造函數(shù)
    propsData: undefined,
    children: undefined,
    tag: undefined
  },
  data: {
    on: undefined,  // 原生事件
    hook: {  // 組件鉤子
      init: function(){...},
      insert: function(){...},
      prepatch: function(){...},
      destroy: function(){...}
    }
  }
}

-------------------------------------------

<template>  // app組件內(nèi)模板
  <div>app text</div>
</template>

生成VNode , 看下在createElm 內(nèi)創(chuàng)建組件Dom分支邏輯是怎么樣的:

function createElm(vnode, insertedVnodeQueue, parentElm, refElm) { 
  ...
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { // 組件分支
    return  
  }
  ...

執(zhí)行createComponent 方法, 如果是元素節(jié)點不會返回任何東西,所以是undefined , 會繼續(xù)走接下來的創(chuàng)建元節(jié)點的邏輯。 現(xiàn)在是組件, 我們看下createComponent 的實現(xiàn):

function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if(isDef(i)) {
    if(isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode)  // 執(zhí)行init方法
    }
    ...
  }
}

首先會將組件的vnode.data賦值給i, 是否有這個屬性就能判斷是否是組件vnode。 之后的if(isDef(i = i.hook) && isDef(i = i.init)) 集判斷和賦值為一體, if 內(nèi)的i(vnode) 就是執(zhí)行的組件init(vnode)方法。 這個時候我們來看下組件的init 鉤子方法做了什么:

import activeInstance  // 全局變量

const init = vnode => {
  const child = vnode.componentInstance = 
    createComponentInstanceForVnode(vnode, activeInstance)
  ...
}

activeInstance 是一個全局的變量, 再update 方法內(nèi)賦值為當(dāng)前實例, 再當(dāng)前實例做 patch 的過程中作為了組件的父實例傳入, 在子組件的initLifecycle時構(gòu)建組件關(guān)系。 將createComponentInsanceForVnode 執(zhí)行的結(jié)果賦值給了vnode.componentInstance, 所以看下它的返回的結(jié)果是什么:

export  createComponentInstanceForVnode(vnode, parent) {  // parent為全局變量activeInstance
  const options = {  // 組件的options
    _isComponent: true,  // 設(shè)置一個標記位,表明是組件
    _parentVnode: vnode, 
    parent  // 子組件的父vm實例,讓初始化initLifecycle可以建立父子關(guān)系
  }
  
  return new vnode.componentOptions.Ctor(options)  // 子組件的構(gòu)造函數(shù)定義為Ctor
}

再組件的init 方法內(nèi)首先執(zhí)行craeeteComponentInstanceForVnode方法, 這個方法的內(nèi)部就會將子組件的構(gòu)造函數(shù)實例化, 因為子組件的構(gòu)造函數(shù)繼承了基類Vue的所有能力, 這個時候相當(dāng)于執(zhí)行new Vue({…}) , 接下來又會執(zhí)行==_init方法進行一系列的子組件的初始化邏輯, 回到_init== 方法內(nèi), 因為他們之間還是有些不同的地方:

Vue.prototype._init = function(options) {
  if(options && options._isComponent) {  // 組件的合并options,_isComponent為之前定義的標記位
    initInternalComponent(this, options)  // 區(qū)分是因為組件的合并項會簡單很多
  }
  
  initLifecycle(vm)  // 建立父子關(guān)系
  ...
  callHook(vm, "created")
  
  if (vm.$options.el) { // 組件是沒有el屬性的,所以到這里咋然而止
    vm.$mount(vm.$options.el)
  }
}

----------------------------------------------------------------------------------------

function initInternalComponent(vm, options) {  // 合并子組件options
  const opts = vm.$options = Object.create(vm.constructor.options)
  opts.parent = options.parent  // 組件init賦值,全局變量activeInstance
  opts._parentVnode = options._parentVnode  // 組件init賦值,組件的vnode 
  ...
}

前面都還是執(zhí)行的好好的, 最后卻因為沒有el屬性, 所以沒有掛載,createComponentInstanceForVnode 方法執(zhí)行完畢。 這個時候我們回到組件的init方法, 補全剩下的邏輯:

const init = vnode => {
  const child = vnode.componentInstance = // 得到組件的實例
    createComponentInstanceForVnode(vnode, activeInstance)
    
  child.$mount(undefined)  // 那就手動掛載唄
}

我們在init 方法內(nèi)手動掛載這個組件, 接著又會執(zhí)行組件的==render()== 方法得到組件內(nèi)元素節(jié)點VNode , 然后執(zhí)行vm._update(), 執(zhí)行組件的 patch 方法, 因為 $mount 方法傳入的是 undefined, oldVnode 也是undefinned, 會執(zhí)行__patch_ 內(nèi)的這段邏輯:

return function patch(oldVnode, vnode) {
  ...
  if (isUndef(oldVnode)) {
    createElm(vnode, insertedVnodeQueue)
  }
  ...
}

這次執(zhí)行createElm 是沒有傳入第三個參數(shù)父節(jié)點的, 那組件創(chuàng)建好的Dom放哪生效了? 沒有父節(jié)點頁要生成Dom不是, 這個時候執(zhí)行的是組件的 patch , 所以參數(shù)vnode 就是組件內(nèi)元素節(jié)點的vnode了:

<template> // app組件內(nèi)模板
  <div>app text</div>
</template>

-------------------------

{  // app內(nèi)元素vnode
  tag: "div",
  children: [
    {text: app text}
  ],
  parent: {  // 子組件_init時執(zhí)行initLifecycle建立的關(guān)系
    tag: "vue-component-1-app",
    componentOptions: {...}
  }
}

很明顯這個時候不是組件了, 即使是組件也沒關(guān)系, 大不了還是執(zhí)行一遍createComponent 創(chuàng)建組件的邏輯, 因為總會有組件是由元素節(jié)點組成的。 這個時候我們執(zhí)行一遍創(chuàng)建元素節(jié)點的邏輯, 因為沒有第三個參數(shù)父節(jié)點, 所以組件的Dom雖然創(chuàng)建好了, 并不會在這里插入。 請注意這個時候組件的init 已經(jīng)完成, 但是組件的createComponent 方法并沒有完成, 我們補全它的邏輯:

function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data;
  if (isDef(i)) {
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode)  // init已經(jīng)完成
    }
    
    if (isDef(vnode.componentInstance)) {  // 執(zhí)行組件init時被賦值
    
      initComponent(vnode)  // 賦值真實dom給vnode.elm
      
      insert(parentElm, vnode.elm, refElm)  // 組件Dom在這里插入
      ...
      return true  // 所以會直接return
    }
  }
}

-----------------------------------------------------------------------

function initComponent(vnode) {
  ...
  vnode.elm = vnode.componentInstance.$el  // __patch__返回的真實dom
  ...
}

無論是嵌套多么深的組件, 遇到組件后就執(zhí)行 init, 在init 的 patch 過程中又遇到嵌套組件, 那就再執(zhí)行嵌套組件的init, 嵌套組件完成 __patch__后將真是的Dom插入到它的父節(jié)點內(nèi), 接著執(zhí)行完外層組件的 patch 又插入到它的父幾點內(nèi), 最后插入到body 內(nèi), 完成嵌套組件的創(chuàng)建過程, 總之還是一個由里及外的過程。

mountComponent 之后的邏輯補全:

export function mountComponent(vm, el) {
  ...
  const updateComponent = () => {
    vm._update(vm._render())
  }
  
  new Watcher(vm, updateComponent, noop, {
    before() {
      if(vm._isMounted) {
        callHook(vm, "beforeUpdate")
      }
    }   
  }, true)
  
  ...
  callHook(vm, "mounted")
  
  return vm
}

接下來會將 updateComponent 傳入到一個Watcher 的類中, 這個類是干嘛的,我們下一章在介紹。 接下來執(zhí)行mounted 鉤子方法。 至此new vue 的整個流程就全部走完了。 我們回顧下從new Vue 開始執(zhí)行的順序:

new Vue ==> vm._init() ==> vm.$mount(el) ==> vm._render()  ==> vm.update(vnode)

父子兩個組件同時定義了 beforeCreate, created, beforeMounte, mounted 四個鉤子, 它們的執(zhí)行順序是怎樣的?

解答:

首先會執(zhí)行父組件的初始化過程, 所以會依次執(zhí)行beforeCreate, created, 在執(zhí)行掛載前又會執(zhí)行beforeMount鉤子, 不過在生成真實dom 的 __patch__過程中遇到嵌套子組件后又會轉(zhuǎn)為去執(zhí)行子組件的初始化鉤子beforeCreate, created, 子組件在掛載前會執(zhí)行beforeMounte, 再完成子組件的Dom創(chuàng)建后執(zhí)行 mounted。 這個父組件的 patch 過程才算完成, 最后執(zhí)行父組件的mounted 鉤子, 這就是它們的執(zhí)行順序。 如下:

parent beforeCreate
parent created
parent beforeMounte
    child beforeCreate
    child created
    child beforeMounte
    child mounted
parent mounted

到此,相信大家對“Vue虛擬Dom到真實Dom如何轉(zhuǎn)換”有了更深的了解,不妨來實際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進入相關(guān)頻道進行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI