再有一棵樹形結(jié)構(gòu)的JavaScript對象后,我們現(xiàn)在需要做的就是將這棵樹跟真實的Dom樹形成映射關(guān)系,首先簡單回顧之前遇到的mountComponent方法:
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í)行的時機(jī):
首次渲染
- 當(dāng)執(zhí)行
new Vue到此時就是首次渲染了,會將傳入的VNode對象映射為真實的Dom。
更新頁面
- 數(shù)據(jù)變化會驅(qū)動頁面發(fā)生變化,這也是
vue最獨(dú)特的特性之一,數(shù)據(jù)改變之前和之后會生成兩份VNode進(jìn)行比較,而怎么樣在舊的VNode上做最小的改動去渲染頁面,這樣一個diff算法還是挺復(fù)雜的。如再沒有先說清楚數(shù)據(jù)響應(yīng)式是怎么回事之前,而直接講diff對理解vue的整體流程并不太好。所以我們這章分析完首次渲染后,下一章就是數(shù)據(jù)響應(yīng)式,之后才是diff比對,如此排序,萬望理解。
我們現(xiàn)在先來看下vm._update方法的定義:
Vue.prototype._update = function(vnode) {
... 首次渲染
vm.$el = vm.__patch__(vm.$el, vnode) // 覆蓋原來的vm.$el
...
}
這里的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(oldVnode)方法包裝:
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é)點的標(biāo)簽名
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'}
]
}
]
}
大家可以先看下這個流程圖有一個印象即可,接下來再看具體實現(xiàn)時相信思路會清晰很多:
開始創(chuàng)建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。
再看一遍流程圖,相信大家疑惑已經(jīng)減少很多:
簡單來說就是由里向外的挨個創(chuàng)建出真實的
Dom,然后插入到它的父節(jié)點內(nèi),最后將創(chuàng)建好的Dom插入到body內(nèi),完成創(chuàng)建的過程,元素節(jié)點的創(chuàng)建還是比較簡單的,我們接下來看下組件是怎么創(chuàng)建的。
2. 組件VNode生成Dom
{ // 組件VNode
tag: 'vue-component-1-app',
context: {...},
componentOptions: {
Ctor: function(){...}, // 子組件構(gòu)造函數(shù)
propsData: undefined,
children: undefined,
tag: undefined,
children: 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é)點的邏輯?,F(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)系。將createComponentInstanceForVnode執(zhí)行的結(jié)果賦值給了vnode.componentInstance,所以看下它的返回的結(jié)果是什么:
export createComponentInstanceForVnode(vnode, parent) { // parent為全局變量activeInstance
const options = { // 組件的options
_isComponent: true, // 設(shè)置一個標(biāo)記位,表明是組件
_parentVnode: vnode,
parent // 子組件的父vm實例,讓初始化initLifecycle可以建立父子關(guān)系
}
return new vnode.componentOptions.Ctor(options) // 子組件的構(gòu)造函數(shù)定義為Ctor
}
再組件的init方法內(nèi)首先執(zhí)行createComponentInstanceForVnode方法,這個方法的內(nèi)部就會將子組件的構(gòu)造函數(shù)實例化,因為子組件的構(gòu)造函數(shù)繼承了基類Vue的所有能力,這個時候相當(dāng)于執(zhí)行new Vue({...}),接下來又會執(zhí)行_init方法進(jìn)行一系列的子組件的初始化邏輯,我們回到_init方法內(nèi),因為它們之間還是有些不同的地方:
Vue.prototype._init = function(options) {
if(options && options._isComponent) { // 組件的合并options,_isComponent為之前定義的標(biāo)記位
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方法,補(bǔ)全剩下的邏輯:
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也是undefined,會執(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方法并沒有完成,我們補(bǔ)全它的邏輯:
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__又插入到它的父節(jié)點內(nèi),最后插入到body內(nèi),完成嵌套組件的創(chuàng)建過程,總之還是一個由里及外的過程。
再回過頭來看這張圖,相信會好理解很多~
我們再將本章最初的
mountComponent之后的邏輯補(bǔ)充完整:
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)
最后我們還是以一道vue可能會被問到的面試題作為本章的結(jié)束吧~
面試官微笑而又不失禮貌的問道:
- 父子兩個組件同時定義了
beforeCreate、created、beforeMounte、mounted四個鉤子,它們的執(zhí)行順序是怎么樣的?
懟回去:
- 如果大家看完前面的章節(jié),相信這個問題已經(jīng)了然于胸了。首先會執(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í)行順序。執(zhí)行順序如下:
parent beforeCreate
parent created
parent beforeMounte
child beforeCreate
child created
child beforeMounte
child mounted
parent mounted
順手點個贊或關(guān)注唄,找起來也方便~
分享一個筆者自己寫的組件庫,哪天可能會用的上了 ~ ↓