- 資訊首頁(yè) > 開(kāi)發(fā)技術(shù) > web開(kāi)發(fā) > JavaScript >
- Vue虛擬Dom到真實(shí)Dom的轉換
再有一顆樹(shù)形結構的Javascript對象后, 我們需要做的就是講這棵樹(shù)跟真實(shí)Dom樹(shù)形成映射關(guān)系。我們先回顧之前的mountComponnet 方法:
export function mountComponent(vm, el) { vm.$el = el ... callHook(vm, 'beforeMount') ... const updateComponent = function () { vm._update(vm._render()) } ... }
我們已經(jīng)執行完了vm._render 方法拿到了VNode, 現在將它作為參數傳給vm._update 方法并執行。 vm._update這個(gè)方法的作用就是將VNode 轉為真實(shí)的Dom, 不過(guò)它有兩個(gè)執行時(shí)機:
當執行new Vue 到此時(shí)就是首次渲染了, 會(huì )將傳入的Vnode對象映射為真實(shí)的Dom。
數據變化會(huì )驅動(dòng)頁(yè)面發(fā)生變化, 這也是vue最獨特的特性之一, 數據改變之前和之后生成兩份VNode進(jìn)行比較, 而怎么樣在舊的VNode上做最小的改動(dòng)去渲染頁(yè)面,這樣一個(gè)diff算法還是挺復雜的。 如果再沒(méi)有先說(shuō)清楚數據響應式是怎么回事之前,直接將diff對理解vue 的整體流程不太好。 所以這章分析首次渲染后, 下一章就是數據響應式, 之后才是diff比較。
先來(lái)看看vm._update方法的定義:
Vue.prototype._update = function(vnode) { ... 首次渲染 vm.$el = vm.__patch__(vm.$el, vnode) // 覆蓋原來(lái)的vm.$el ... }
這里的 vm. e l 是 之 前 在 = = m o u n t C o m p o n e n t = = 方 法 內 就 掛 載 的 , 一 個(gè) 真 實(shí) 的 = = D o m = = 元 素 。 首 次 渲 染 會(huì ) 傳 入 v m . el 是之前在 ==mountComponent== 方法內就掛載的, 一個(gè)真實(shí)的==Dom==元素。 首次渲染會(huì )傳入 vm. el是之前在==mountComponent==方法內就掛載的,一個(gè)真實(shí)的==Dom==元素。首次渲染會(huì )傳入vm.el 以及得到的VNode, 所以看下vm.patch 定義:
Vue.prototype.__patch__ = createPatchFunction({ nodeOps, modules })
patch 是createPatchFunction 方法內部返回的一個(gè)方法, 它接受一個(gè)對象:
nodeOps屬性:封裝了操作原生Dom 的一些方法的集合, 如:創(chuàng )建、插入,移除這些, 我們到使用的地方咋詳解。
modules 屬性: 創(chuàng )建真實(shí)Dom 也需要生成它的如class/attrs/style 等屬性。 modules 是一個(gè)數組集合,數組的每一項都是這些屬性對應的鉤子方法, 這些屬性的創(chuàng )建,更新,銷(xiāo)毀等都有對應鉤子方法。 當某一時(shí)刻需要做某件事,執行對應的鉤子即可。 比如它們都有create 這個(gè)鉤子方法, 如將這些create 鉤子收集到一個(gè)數組內, 需要在真實(shí)Dom上創(chuàng )建這些屬性時(shí),依次執行數組的每一項,也就是依次創(chuàng )建了它們。
PS: 這里modules 屬性?xún)鹊你^子方法是區分平臺的, web, weex 以及 SSR 它們調用VNode 方法方式并不相同, 所以vue在這里又使用了函數柯里化這個(gè)騷操作, 在createPatchFunction 內將平臺的差異化磨平, 從而 patch 方法只用接收新舊node即可。
這里大家記住一句話(huà)即可, 無(wú)論VNode 是什么類(lèi)型的節點(diǎn), 只有三種類(lèi)型的節點(diǎn)會(huì )被創(chuàng )建并插入到Dom中: 元素節點(diǎn),注釋節點(diǎn), 和文本節點(diǎn)。
我們接著(zhù)看下createPatchFunction 它返回一個(gè)怎樣的方法:
export function createPatchFunction(backend) { ... const { modules, nodeOps } = backend // 解構出傳入的集合 return function (oldVnode, vnode) { // 接收新舊vnode ... const isRealElement = isDef(oldVnode.nodeType) // 是否是真實(shí)Dom if(isRealElement) { // $el是真實(shí)Dom oldVnode = emptyNodeAt(oldVnode) // 轉為VNode格式覆蓋自己 } ... } }
首次渲染時(shí)沒(méi)有oldVnode, oldVnode 就是 $el, 一個(gè)真實(shí)的dom, 經(jīng)過(guò)emptyNodeAt(odVnode) 方法包裝:
function emptyNodeAt(elm) { return new VNode( nodeOps.tagName(elm).toLowerCase(), // 對應tag屬性 {}, // 對應data [], // 對應children undefined, //對應text elm // 真實(shí)dom賦值給了elm屬性 ) } 包裝后的: { tag: 'div', elm: '<div id="app"></div>' // 真實(shí)dom } ------------------------------------------------------- nodeOps: export function tagName (node) { // 返回節點(diǎn)的標簽名 return node.tagName }
在將傳入的==$el== 屬性轉為了VNode 格式之后,我們繼續:
export function createPatchFunction(backend) { ... return function (oldVnode, vnode) { // 接收新舊vnode const insertedVnodeQueue = [] ... const oldElm = oldVnode.elm //包裝后的真實(shí)Dom <div id='app'></div> const parentElm = nodeOps.parentNode(oldElm) // 首次父節點(diǎn)為<body></body> createElm( // 創(chuàng )建真實(shí)Dom vnode, // 第二個(gè)參數 insertedVnodeQueue, // 空數組 parentElm, // <body></body> nodeOps.nextSibling(oldElm) // 下一個(gè)節點(diǎn) ) return vnode.elm // 返回真實(shí)Dom覆蓋vm.$el } } ------------------------------------------------------ nodeOps: export function parentNode (node) { // 獲取父節點(diǎn) return node.parentNode } export function nextSibling(node) { // 獲取下一個(gè)節點(diǎn) return node.nextSibing }
createElm 方法開(kāi)始生成真實(shí)的Dom, VNode 生成真實(shí)的Dom 的方式還是分為元素節點(diǎn)和組件兩種方式, 所以我們使用上一章生成的VNode分別說(shuō)明。
{ // 元素節點(diǎn)VNode tag: 'div', children: [{ tag: 'h1', children: [ {text: 'title h1'} ] }, { tag: 'h2', children: [ {text: 'title h2'} ] }, { tag: 'h3', children: [ {text: 'title h3'} ] } ] }
大家可以先看下這個(gè)流程圖有個(gè)印象即可, 再接下來(lái)看具體實(shí)現時(shí)思路會(huì )清晰很多(這里先借用網(wǎng)上的一張圖):
開(kāi)始Dom, 來(lái)看下它的定義:
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 // 如果是組件結果返回true,不會(huì )繼續,之后詳解createComponent } if(isDef(tag)) { // 元素節點(diǎn) vnode.elm = nodeOps.createElement(tag) // 創(chuàng )建父節點(diǎn) createChildren(vnode, children, insertedVnodeQueue) // 創(chuàng )建子節點(diǎn) insert(parentElm, vnode.elm, refElm) // 插入 } else if(isTrue(vnode.isComment)) { // 注釋節點(diǎn) vnode.elm = nodeOps.createComment(vnode.text) // 創(chuàng )建注釋節點(diǎn) insert(parentElm, vnode.elm, refElm); // 插入到父節點(diǎn) } else { // 文本節點(diǎn) vnode.elm = nodeOps.createTextNode(vnode.text) // 創(chuàng )建文本節點(diǎn) insert(parentElm, vnode.elm, refElm) // 插入到父節點(diǎn) } ... } ------------------------------------------------------------------ nodeOps: export function createElement(tagName) { // 創(chuàng )建節點(diǎn) return document.createElement(tagName) } export function createComment(text) { //創(chuàng )建注釋節點(diǎn) return document.createComment(text) } export function createTextNode(text) { // 創(chuàng )建文本節點(diǎn) return document.createTextNode(text) } function insert (parent, elm, ref) { //插入dom操作 if (isDef(parent)) { // 有父節點(diǎn) if (isDef(ref)) { // 有參考節點(diǎn) if (ref.parentNode === parent) { // 參考節點(diǎn)的父節點(diǎn)等于傳入的父節點(diǎn) nodeOps.insertBefore(parent, elm, ref) // 在父節點(diǎn)內的參考節點(diǎn)之前插入elm } } else { nodeOps.appendChild(parent, elm) // 添加elm到parent內 } } // 沒(méi)有父節點(diǎn)什么都不做 } 這算一個(gè)比較重要的方法,因為很多地方會(huì )用到。
依次判斷是否是元素節點(diǎn), 注釋節點(diǎn),文本節點(diǎn), 分別創(chuàng )建它們然后插入到父節點(diǎn)里面, 這里主要介紹創(chuàng )建元素節點(diǎn), 另外兩個(gè)并沒(méi)有復雜的邏輯。 我們接下來(lái)看下:createChild 方法定義:
function createChild(vnode, children, insertedVnodeQueue) { if(Array.isArray(children)) { // 是數組 for(let i = 0; i < children.length; ++i) { // 遍歷vnode每一項 createElm( // 遞歸調用 children[i], insertedVnodeQueue, vnode.elm, null, true, // 不是根節點(diǎn)插入 children, i ) } } else if(isPrimitive(vnode.text)) { //typeof為string/number/symbol/boolean之一 nodeOps.appendChild( // 創(chuàng )建并插入到父節點(diǎn) vnode.elm, nodeOps.createTextNode(String(vnode.text)) ) } } ------------------------------------------------------------------------------- nodeOps: export default appendChild(node, child) { // 添加子節點(diǎn) node.appendChild(child) }
開(kāi)始創(chuàng )建子節點(diǎn), 遍歷VNode 的每一項, 每一項還是使用之前的createElm方法創(chuàng )建Dom。 如果某一項又是數組,繼續調用createChild創(chuàng )建某一項的子節點(diǎn); 如果某一項不是數組, 創(chuàng )建文本節點(diǎn)并將它添加到父節點(diǎn)內。 像這樣使用遞歸的形式將嵌套的VNode全部創(chuàng )建為真實(shí)的Dom。
在看一遍流程圖, 應該就能減少大家很多疑惑了(這里先借用網(wǎng)上一章圖):
簡(jiǎn)單來(lái)說(shuō)就是由里向外的挨個(gè)創(chuàng )建出真實(shí)的Dom, 然后插入到它的父節點(diǎn)內,最后將創(chuàng )建好的Dom插入到body內, 完成創(chuàng )建的過(guò)程, 元素節點(diǎn)的創(chuàng )建還是比較簡(jiǎn)單的, 接下來(lái)看下組件式怎么創(chuàng )建的。
{ // 組件VNode tag: 'vue-component-1-app', context: {...}, componentOptions: { Ctor: function(){...}, // 子組件構造函數 propsData: undefined, children: undefined, tag: undefined }, data: { on: undefined, // 原生事件 hook: { // 組件鉤子 init: function(){...}, insert: function(){...}, prepatch: function(){...}, destroy: function(){...} } } } ------------------------------------------- <template> // app組件內模板 <div>app text</div> </template>
首先看張簡(jiǎn)易流程圖, 留個(gè)影響即可,方便理清之后的邏輯順序(這里借用網(wǎng)上一張圖):
使用上一章組件生成VNode , 看下在createElm 內創(chuàng )建組件Dom分支邏輯是怎么樣的:
function createElm(vnode, insertedVnodeQueue, parentElm, refElm) { ... if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { // 組件分支 return } ...
執行createComponent 方法, 如果是元素節點(diǎn)不會(huì )返回任何東西,所以是undefined , 會(huì )繼續走接下來(lái)的創(chuàng )建元節點(diǎn)的邏輯。 現在是組件, 我們看下createComponent 的實(shí)現:
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方法 } ... } }
首先會(huì )將組件的vnode.data賦值給i, 是否有這個(gè)屬性就能判斷是否是組件vnode。 之后的if(isDef(i = i.hook) && isDef(i = i.init)) 集判斷和賦值為一體, if 內的i(vnode) 就是執行的組件init(vnode)方法。 這個(gè)時(shí)候我們來(lái)看下組件的init 鉤子方法做了什么:
import activeInstance // 全局變量 const init = vnode => { const child = vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance) ... }
activeInstance 是一個(gè)全局的變量, 再u(mài)pdate 方法內賦值為當前實(shí)例, 再當前實(shí)例做 patch 的過(guò)程中作為了組件的父實(shí)例傳入, 在子組件的initLifecycle時(shí)構建組件關(guān)系。 將createComponentInsanceForVnode 執行的結果賦值給了vnode.componentInstance, 所以看下它的返回的結果是什么:
export createComponentInstanceForVnode(vnode, parent) { // parent為全局變量activeInstance const options = { // 組件的options _isComponent: true, // 設置一個(gè)標記位,表明是組件 _parentVnode: vnode, parent // 子組件的父vm實(shí)例,讓初始化initLifecycle可以建立父子關(guān)系 } return new vnode.componentOptions.Ctor(options) // 子組件的構造函數定義為Ctor }
再組件的init 方法內首先執行craeeteComponentInstanceForVnode方法, 這個(gè)方法的內部就會(huì )將子組件的構造函數實(shí)例化, 因為子組件的構造函數繼承了基類(lèi)Vue的所有能力, 這個(gè)時(shí)候相當于執行new Vue({…}) , 接下來(lái)又會(huì )執行==_init方法進(jìn)行一系列的子組件的初始化邏輯, 回到_init== 方法內, 因為他們之間還是有些不同的地方:
Vue.prototype._init = function(options) { if(options && options._isComponent) { // 組件的合并options,_isComponent為之前定義的標記位 initInternalComponent(this, options) // 區分是因為組件的合并項會(huì )簡(jiǎn)單很多 } initLifecycle(vm) // 建立父子關(guān)系 ... callHook(vm, 'created') if (vm.$options.el) { // 組件是沒(méi)有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 ... }
前面都還是執行的好好的, 最后卻因為沒(méi)有el屬性, 所以沒(méi)有掛載,createComponentInstanceForVnode 方法執行完畢。 這個(gè)時(shí)候我們回到組件的init方法, 補全剩下的邏輯:
const init = vnode => { const child = vnode.componentInstance = // 得到組件的實(shí)例 createComponentInstanceForVnode(vnode, activeInstance) child.$mount(undefined) // 那就手動(dòng)掛載唄 }
我們在init 方法內手動(dòng)掛載這個(gè)組件, 接著(zhù)又會(huì )執行組件的==render()== 方法得到組件內元素節點(diǎn)VNode , 然后執行vm._update(), 執行組件的 patch 方法, 因為 $mount 方法傳入的是 undefined, oldVnode 也是undefinned, 會(huì )執行__patch_ 內的這段邏輯:
return function patch(oldVnode, vnode) { ... if (isUndef(oldVnode)) { createElm(vnode, insertedVnodeQueue) } ... }
這次執行createElm 是沒(méi)有傳入第三個(gè)參數父節點(diǎn)的, 那組件創(chuàng )建好的Dom放哪生效了? 沒(méi)有父節點(diǎn)頁(yè)要生成Dom不是, 這個(gè)時(shí)候執行的是組件的 patch , 所以參數vnode 就是組件內元素節點(diǎn)的vnode了:
<template> // app組件內模板 <div>app text</div> </template> ------------------------- { // app內元素vnode tag: 'div', children: [ {text: app text} ], parent: { // 子組件_init時(shí)執行initLifecycle建立的關(guān)系 tag: 'vue-component-1-app', componentOptions: {...} } }
很明顯這個(gè)時(shí)候不是組件了, 即使是組件也沒(méi)關(guān)系, 大不了還是執行一遍createComponent 創(chuàng )建組件的邏輯, 因為總會(huì )有組件是由元素節點(diǎn)組成的。 這個(gè)時(shí)候我們執行一遍創(chuàng )建元素節點(diǎn)的邏輯, 因為沒(méi)有第三個(gè)參數父節點(diǎn), 所以組件的Dom雖然創(chuàng )建好了, 并不會(huì )在這里插入。 請注意這個(gè)時(shí)候組件的init 已經(jīng)完成, 但是組件的createComponent 方法并沒(méi)有完成, 我們補全它的邏輯:
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)) { // 執行組件init時(shí)被賦值 initComponent(vnode) // 賦值真實(shí)dom給vnode.elm insert(parentElm, vnode.elm, refElm) // 組件Dom在這里插入 ... return true // 所以會(huì )直接return } } } ----------------------------------------------------------------------- function initComponent(vnode) { ... vnode.elm = vnode.componentInstance.$el // __patch__返回的真實(shí)dom ... }
無(wú)論是嵌套多么深的組件, 遇到組件后就執行 init, 在init 的 patch 過(guò)程中又遇到嵌套組件, 那就再執行嵌套組件的init, 嵌套組件完成 __patch__后將真是的Dom插入到它的父節點(diǎn)內, 接著(zhù)執行完外層組件的 patch 又插入到它的父幾點(diǎn)內, 最后插入到body 內, 完成嵌套組件的創(chuàng )建過(guò)程, 總之還是一個(gè)由里及外的過(guò)程。
在回過(guò)頭看這張圖, 相信會(huì )很好理解了:
再將本章最初的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 }
接下來(lái)會(huì )將 updateComponent 傳入到一個(gè)Watcher 的類(lèi)中, 這個(gè)類(lèi)是干嘛的,我們下一章在介紹。 接下來(lái)執行mounted 鉤子方法。 至此new vue 的整個(gè)流程就全部走完了。 我們回顧下從new Vue 開(kāi)始執行的順序:
new Vue ==> vm._init() ==> vm.$mount(el) ==> vm._render() ==> vm.update(vnode)
最后我們以一個(gè)問(wèn)題來(lái)結束本章的內容:
父子兩個(gè)組件同時(shí)定義了 beforeCreate, created, beforeMounte, mounted 四個(gè)鉤子, 它們的執行順序是怎樣的?
首先會(huì )執行父組件的初始化過(guò)程, 所以會(huì )依次執行beforeCreate, created, 在執行掛載前又會(huì )執行beforeMount鉤子, 不過(guò)在生成真實(shí)dom 的 __patch__過(guò)程中遇到嵌套子組件后又會(huì )轉為去執行子組件的初始化鉤子beforeCreate, created, 子組件在掛載前會(huì )執行beforeMounte, 再完成子組件的Dom創(chuàng )建后執行 mounted。 這個(gè)父組件的 patch 過(guò)程才算完成, 最后執行父組件的mounted 鉤子, 這就是它們的執行順序。 如下:
parent beforeCreate parent created parent beforeMounte child beforeCreate child created child beforeMounte child mounted parent mounted
到此這篇關(guān)于Vue虛擬Dom到真實(shí)Dom的轉換的文章就介紹到這了,更多相關(guān)Vue虛擬Dom到真實(shí)Dom內容請搜索腳本之家以前的文章或繼續瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng )、來(lái)自本網(wǎng)站內容采集于網(wǎng)絡(luò )互聯(lián)網(wǎng)轉載等其它媒體和分享為主,內容觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如侵犯了原作者的版權,請告知一經(jīng)查實(shí),將立刻刪除涉嫌侵權內容,聯(lián)系我們QQ:712375056,同時(shí)歡迎投稿傳遞力量。
Copyright ? 2009-2022 56dr.com. All Rights Reserved. 特網(wǎng)科技 特網(wǎng)云 版權所有 特網(wǎng)科技 粵ICP備16109289號
域名注冊服務(wù)機構:阿里云計算有限公司(萬(wàn)網(wǎng)) 域名服務(wù)機構:煙臺帝思普網(wǎng)絡(luò )科技有限公司(DNSPod) CDN服務(wù):阿里云計算有限公司 百度云 中國互聯(lián)網(wǎng)舉報中心 增值電信業(yè)務(wù)經(jīng)營(yíng)許可證B2
建議您使用Chrome、Firefox、Edge、IE10及以上版本和360等主流瀏覽器瀏覽本網(wǎng)站