再有一棵樹形結構的JavaScript
對象後,咱們如今須要作的就是將這棵樹跟真實的Dom
樹造成映射關係,首先簡單回顧以前遇到的mountComponent
方法:vue
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
,不過它有兩個執行的時機:node
首次渲染
new Vue
到此時就是首次渲染了,會將傳入的VNode
對象映射爲真實的Dom
。更新頁面
vue
最獨特的特性之一,數據改變以前和以後會生成兩份VNode
進行比較,而怎麼樣在舊的VNode
上作最小的改動去渲染頁面,這樣一個diff
算法仍是挺複雜的。如再沒有先說清楚數據響應式是怎麼回事以前,而直接講diff
對理解vue
的總體流程並不太好。因此咱們這章分析完首次渲染後,下一章就是數據響應式,以後纔是diff
比對,如此排序,萬望理解。咱們如今先來看下vm._update
方法的定義:web
Vue.prototype._update = function(vnode) {
... 首次渲染
vm.$el = vm.__patch__(vm.$el, vnode) // 覆蓋原來的vm.$el
...
}
複製代碼
這裏的vm.$el
是以前在mountComponent
方法內就掛載的,一個真實Dom
元素。首次渲染會傳入vm.$el
以及獲得的VNode
,因此看下vm.__patch__
定義:面試
Vue.prototype.__patch__ = createPatchFunction({ nodeOps, modules })
複製代碼
__patch__
是createPatchFunction
方法內部返回的一個方法,它接受一個對象:算法
nodeOps
屬性:封裝了操做原生Dom
的一些方法的集合,如建立、插入、移除這些,再使用到的地方再詳解。數組
modules
屬性:建立真實Dom
也須要生成它的如class
/attrs
/style
等屬性。modules
是一個數組集合,數組的每一項都是這些屬性對應的鉤子方法,這些屬性的建立、更新、銷燬等都有對應鉤子方法,當某一時刻須要作某件事,執行對應的鉤子便可。好比它們都有create
這個鉤子方法,如將這些create
鉤子收集到一個數組內,須要在真實Dom
上建立這些屬性時,依次執行數組的每一項,也就是依次建立了它們。bash
Ps: 這裏
modules
屬性內的鉤子方法是區分平臺的,web
、weex
以及SSR
它們調用VNode
方法方式並不相同,因此vue
在這裏又使用了函數柯里化這個騷操做,在createPatchFunction
內將平臺的差別化抹平,從而__patch__
方法只用接收新舊node
便可。weex
這裏你們記住一句話便可,不管VNode
是什麼類型的節點,只有三種類型的節點會被建立並插入到的Dom
中:元素節點、註釋節點、和文本節點。app
咱們接着來看下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(oldVnode)
方法包裝:
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
內,完成建立的過程,元素節點的建立仍是比較簡單的,咱們接下來看下組件是怎麼建立的。
2. 組件VNode生成Dom
{ // 組件VNode
tag: 'vue-component-1-app',
context: {...},
componentOptions: {
Ctor: function(){...}, // 子組件構造函數
propsData: undefined,
children: undefined,
tag: undefined,
children: 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
時構建組件關係。將createComponentInstanceForVnode
執行的結果賦值給了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
方法內首先執行createComponentInstanceForVnode
方法,這個方法的內部就會將子組件的構造函數實例化,由於子組件的構造函數繼承了基類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
也是undefined
,會執行__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)
複製代碼
最後咱們仍是以一道vue
可能會被問到的面試題做爲本章的結束吧~
面試官微笑而又不失禮貌的問道:
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
複製代碼
順手點個贊或關注唄,找起來也方便~