再有一顆樹形結構的Javascript對象後, 我們需要做的就是講這棵樹跟真實Dom樹形成映射關係。我們先回顧之前的mountComponnet 方法:
export function mountComponent(vm, el) {
vm.$el = el
...
callHook(vm, 'beforeMount')
...
const updateComponent = function () {
vm._update(vm._render())
}
...
}
我們已經執行完了vm._render 方法拿到了VNode, 現在將它作爲參數傳給vm._update 方法並執行。 vm._update這個方法的作用就是將VNode 轉爲真實的Dom, 不過它有兩個執行時機:
首次渲染
- 當執行new Vue 到此時就是首次渲染了, 會將傳入的Vnode對象映射爲真實的Dom。
更新頁面
- 數據變化會驅動頁面發生變化, 這也是vue最獨特的特性之一, 數據改變之前和之後生成兩份VNode進行比較, 而怎麼樣在舊的VNode上做最小的改動去渲染頁面,這樣一個diff算法還是挺複雜的。 如果再沒有先說清楚數據響應式是怎麼回事之前,直接將diff對理解vue 的整體流程不太好。 所以這章分析首次渲染後, 下一章就是數據響應式, 之後纔是diff比較。
先來看看vm._update方法的定義:
Vue.prototype._update = function(vnode) {
... 首次渲染
vm.$el = vm.__patch__(vm.$el, vnode) // 覆蓋原來的vm.$el
...
}
這裏的 vm.el 以及得到的VNode, 所以看下vm.patch 定義:
Vue.prototype.__patch__ = createPatchFunction({ nodeOps, modules })
patch 是 createPatchFunction 方法內部返回的一個方法, 它接受一個對象:
nodeOps屬性:封裝了操作原生Dom 的一些方法的集合, 如:創建、插入,移除這些, 我們到使用的地方咋詳解。
modules 屬性: 創建真實Dom 也需要生成它的如class/attrs/style 等屬性。 modules 是一個數組集合,數組的每一項都是這些屬性對應的鉤子方法, 這些屬性的創建,更新,銷燬等都有對應鉤子方法。 當某一時刻需要做某件事,執行對應的鉤子即可。 比如它們都有create 這個鉤子方法, 如將這些create 鉤子收集到一個數組內, 需要在真實Dom上創建這些屬性時,依次執行數組的每一項,也就是依次創建了它們。
PS: 這裏modules 屬性內的鉤子方法是區分平臺的, web, weex 以及 SSR 它們調用VNode 方法方式並不相同, 所以vue在這裏又使用了函數柯里化這個騷操作, 在createPatchFunction 內將平臺的差異化磨平, 從而 patch 方法只用接收新舊node即可。
生成Dom
這裏大家記住一句話即可, 無論VNode 是什麼類型的節點, 只有三種類型的節點會被創建並插入到Dom中: 元素節點,註釋節點, 和文本節點。
我們接着看下createPatchFunction 它返回一個怎樣的方法:
export function createPatchFunction(backend) {
...
const { modules, nodeOps } = backend // 解構出傳入的集合
return function (oldVnode, vnode) { // 接收新舊vnode
...
const isRealElement = isDef(oldVnode.nodeType) // 是否是真實Dom
if(isRealElement) { // $el是真實Dom
oldVnode = emptyNodeAt(oldVnode) // 轉爲VNode格式覆蓋自己
}
...
}
}
首次渲染時沒有oldVnode, oldVnode 就是 $el, 一個真實的dom, 經過emptyNodeAt(odVnode) 方法包裝:
function emptyNodeAt(elm) {
return new VNode(
nodeOps.tagName(elm).toLowerCase(), // 對應tag屬性
{}, // 對應data
[], // 對應children
undefined, //對應text
elm // 真實dom賦值給了elm屬性
)
}
包裝後的:
{
tag: 'div',
elm: '<div id="app"></div>' // 真實dom
}
-------------------------------------------------------
nodeOps:
export function tagName (node) { // 返回節點的標籤名
return node.tagName
}
在將傳入的==$el== 屬性轉爲了VNode 格式之後,我們繼續:
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) // 首次父節點爲<body></body>
createElm( // 創建真實Dom
vnode, // 第二個參數
insertedVnodeQueue, // 空數組
parentElm, // <body></body>
nodeOps.nextSibling(oldElm) // 下一個節點
)
return vnode.elm // 返回真實Dom覆蓋vm.$el
}
}
------------------------------------------------------
nodeOps:
export function parentNode (node) { // 獲取父節點
return node.parentNode
}
export function nextSibling(node) { // 獲取下一個節點
return node.nextSibing
}
createElm 方法開始生成真實的Dom, VNode 生成真實的Dom 的方式還是分爲元素節點和組件兩種方式, 所以我們使用上一章生成的VNode分別說明。
1. 元素節點生成Dom
{ // 元素節點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 // 如果是組件結果返回true,不會繼續,之後詳解createComponent
}
if(isDef(tag)) { // 元素節點
vnode.elm = nodeOps.createElement(tag) // 創建父節點
createChildren(vnode, children, insertedVnodeQueue) // 創建子節點
insert(parentElm, vnode.elm, refElm) // 插入
} else if(isTrue(vnode.isComment)) { // 註釋節點
vnode.elm = nodeOps.createComment(vnode.text) // 創建註釋節點
insert(parentElm, vnode.elm, refElm); // 插入到父節點
} else { // 文本節點
vnode.elm = nodeOps.createTextNode(vnode.text) // 創建文本節點
insert(parentElm, vnode.elm, refElm) // 插入到父節點
}
...
}
------------------------------------------------------------------
nodeOps:
export function createElement(tagName) { // 創建節點
return document.createElement(tagName)
}
export function createComment(text) { //創建註釋節點
return document.createComment(text)
}
export function createTextNode(text) { // 創建文本節點
return document.createTextNode(text)
}
function insert (parent, elm, ref) { //插入dom操作
if (isDef(parent)) { // 有父節點
if (isDef(ref)) { // 有參考節點
if (ref.parentNode === parent) { // 參考節點的父節點等於傳入的父節點
nodeOps.insertBefore(parent, elm, ref) // 在父節點內的參考節點之前插入elm
}
} else {
nodeOps.appendChild(parent, elm) // 添加elm到parent內
}
} // 沒有父節點什麼都不做
}
這算一個比較重要的方法,因爲很多地方會用到。
依次判斷是否是元素節點, 註釋節點,文本節點, 分別創建它們然後插入到父節點裏面, 這裏主要介紹創建元素節點, 另外兩個並沒有複雜的邏輯。 我們接下來看下: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, // 不是根節點插入
children,
i
)
}
} else if(isPrimitive(vnode.text)) { //typeof爲string/number/symbol/boolean之一
nodeOps.appendChild( // 創建並插入到父節點
vnode.elm,
nodeOps.createTextNode(String(vnode.text))
)
}
}
-------------------------------------------------------------------------------
nodeOps:
export default appendChild(node, child) { // 添加子節點
node.appendChild(child)
}
開始創建子節點, 遍歷VNode 的每一項, 每一項還是使用之前的createElm方法創建Dom。 如果某一項又是數組,繼續調用createChild創建某一項的子節點; 如果某一項不是數組, 創建文本節點並將它添加到父節點內。 像這樣使用遞歸的形式將嵌套的VNode全部創建爲真實的Dom。
在看一遍流程圖, 應該就能減少大家很多疑惑了(這裏先借用網上一章圖):
簡單來說就是由裏向外的挨個創建出真實的Dom, 然後插入到它的父節點內,最後將創建好的Dom插入到body內, 完成創建的過程, 元素節點的創建還是比較簡單的, 接下來看下組件式怎麼創建的。
組件VNode生成Dom
{ // 組件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>
首先看張簡易流程圖, 留個影響即可,方便理清之後的邏輯順序(這裏借用網上一張圖):
使用上一章組件生成VNode , 看下在createElm 內創建組件Dom分支邏輯是怎麼樣的:
function createElm(vnode, insertedVnodeQueue, parentElm, refElm) {
...
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { // 組件分支
return
}
...
執行createComponent 方法, 如果是元素節點不會返回任何東西,所以是undefined , 會繼續走接下來的創建元節點的邏輯。 現在是組件, 我們看下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方法
}
...
}
}
首先會將組件的vnode.data賦值給i, 是否有這個屬性就能判斷是否是組件vnode。 之後的if(isDef(i = i.hook) && isDef(i = i.init)) 集判斷和賦值爲一體, if 內的i(vnode) 就是執行的組件init(vnode)方法。 這個時候我們來看下組件的init 鉤子方法做了什麼:
import activeInstance // 全局變量
const init = vnode => {
const child = vnode.componentInstance =
createComponentInstanceForVnode(vnode, activeInstance)
...
}
activeInstance 是一個全局的變量, 再update 方法內賦值爲當前實例, 再當前實例做 patch 的過程中作爲了組件的父實例傳入, 在子組件的initLifecycle時構建組件關係。 將createComponentInsanceForVnode 執行的結果賦值給了vnode.componentInstance, 所以看下它的返回的結果是什麼:
export createComponentInstanceForVnode(vnode, parent) { // parent爲全局變量activeInstance
const options = { // 組件的options
_isComponent: true, // 設置一個標記位,表明是組件
_parentVnode: vnode,
parent // 子組件的父vm實例,讓初始化initLifecycle可以建立父子關係
}
return new vnode.componentOptions.Ctor(options) // 子組件的構造函數定義爲Ctor
}
再組件的init 方法內首先執行craeeteComponentInstanceForVnode方法, 這個方法的內部就會將子組件的構造函數實例化, 因爲子組件的構造函數繼承了基類Vue的所有能力, 這個時候相當於執行new Vue({…}) , 接下來又會執行==_init方法進行一系列的子組件的初始化邏輯, 回到_init== 方法內, 因爲他們之間還是有些不同的地方:
Vue.prototype._init = function(options) {
if(options && options._isComponent) { // 組件的合併options,_isComponent爲之前定義的標記位
initInternalComponent(this, options) // 區分是因爲組件的合併項會簡單很多
}
initLifecycle(vm) // 建立父子關係
...
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
...
}
前面都還是執行的好好的, 最後卻因爲沒有el屬性, 所以沒有掛載,createComponentInstanceForVnode 方法執行完畢。 這個時候我們回到組件的init方法, 補全剩下的邏輯:
const init = vnode => {
const child = vnode.componentInstance = // 得到組件的實例
createComponentInstanceForVnode(vnode, activeInstance)
child.$mount(undefined) // 那就手動掛載唄
}
我們在init 方法內手動掛載這個組件, 接着又會執行組件的==render()== 方法得到組件內元素節點VNode , 然後執行vm._update(), 執行組件的 patch 方法, 因爲 $mount 方法傳入的是 undefined, oldVnode 也是 undefinned, 會執行__patch_ 內的這段邏輯:
return function patch(oldVnode, vnode) {
...
if (isUndef(oldVnode)) {
createElm(vnode, insertedVnodeQueue)
}
...
}
這次執行createElm 是沒有傳入第三個參數父節點的, 那組件創建好的Dom放哪生效了? 沒有父節點頁要生成Dom不是, 這個時候執行的是組件的 patch , 所以參數vnode 就是組件內元素節點的vnode了:
<template> // app組件內模板
<div>app text</div>
</template>
-------------------------
{ // app內元素vnode
tag: 'div',
children: [
{text: app text}
],
parent: { // 子組件_init時執行initLifecycle建立的關係
tag: 'vue-component-1-app',
componentOptions: {...}
}
}
很明顯這個時候不是組件了, 即使是組件也沒關係, 大不了還是執行一遍createComponent 創建組件的邏輯, 因爲總會有組件是由元素節點組成的。 這個時候我們執行一遍創建元素節點的邏輯, 因爲沒有第三個參數父節點, 所以組件的Dom雖然創建好了, 並不會在這裏插入。 請注意這個時候組件的init 已經完成, 但是組件的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已經完成
}
if (isDef(vnode.componentInstance)) { // 執行組件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
...
}
無論是嵌套多麼深的組件, 遇到組件後就執行 init, 在init 的 patch 過程中又遇到嵌套組件, 那就再執行嵌套組件的init, 嵌套組件完成 __patch__後將真是的Dom插入到它的父節點內, 接着執行完外層組件的 patch 又插入到它的父幾點內, 最後插入到body 內, 完成嵌套組件的創建過程, 總之還是一個由裏及外的過程。
在回過頭看這張圖, 相信會很好理解了:
再將本章最初的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 的類中, 這個類是幹嘛的,我們下一章在介紹。 接下來執行mounted 鉤子方法。 至此new vue 的整個流程就全部走完了。 我們回顧下從new Vue 開始執行的順序:
new Vue ==> vm._init() ==> vm.$mount(el) ==> vm._render() ==> vm.update(vnode)
最後我們以一個問題來結束本章的內容:
- 父子兩個組件同時定義了 beforeCreate, created, beforeMounte, mounted 四個鉤子, 它們的執行順序是怎樣的?
解答:
- 首先會執行父組件的初始化過程, 所以會依次執行beforeCreate, created, 在執行掛載前又會執行beforeMount鉤子, 不過在生成真實dom 的 __patch__過程中遇到嵌套子組件後又會轉爲去執行子組件的初始化鉤子beforeCreate, created, 子組件在掛載前會執行beforeMounte, 再完成子組件的Dom創建後執行 mounted。 這個父組件的 patch 過程纔算完成, 最後執行父組件的mounted 鉤子, 這就是它們的執行順序。 如下:
parent beforeCreate
parent created
parent beforeMounte
child beforeCreate
child created
child beforeMounte
child mounted
parent mounted