前言
各位道友大家好,我是LSF,在上一篇博文 中,分析了Vue初始化的整體流程,最後到了 update 動態建立 DOM 階段。接下來這篇博文,會對這個流程進行分析,重點需要掌握 createElm 函式的執行邏輯。
一、_update 如何判斷是初始化還是更新操作?
_update 是在Vue例項化之前,通過prototype混入的一個例項方法。主要目的是將vnode轉化成真實DOM,它定義在 core/instance/lifecycle.js 檔案中。
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this // vm -> this
const prevEl = vm.$el
// 儲存上一個vnode。
const prevVnode = vm._vnode
// 設定 activeInstance 當前活動的vm,返回方法。
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode // 賦值 _vnode 屬性為新傳入的 vnode。
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render 初始化渲染,如果有子元件,會遞迴初始化
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates 更新
vm.$el = vm.__patch__(prevVnode, vnode)
}
// activeInstance 恢復到當前的vm
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
程式碼中可以看到,通過 prevVnode 是否為 null 來判斷的是否是初始化 patch。由於是初始化操作,開始的時候 vm._vnode 沒有被賦值成 vnode,從而 vm._vnode 為 null。所以程式碼的執行邏輯會走到初始化 patch。
二、patch
2.1 patch 定義
web端的 Vue.prototype.__patch__
方法,它定義的入口在 src/platforms/web/runtime/index.js 檔案中。
import { patch } from './patch'
...
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
// install platform patch function
// 安裝web端的 patch 方法。
Vue.prototype.__patch__ = inBrowser ? patch : noop
...
如果是瀏覽器環境下,被賦值為 patch 方法,該方法定義在 src/platforms/web/runtime/patch.js中。如果是非瀏覽器環境,patch 被賦值成一個空函式。
/* @flow */
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
// 呼叫 createPatchFunction 函式,返回 patch。
export const patch: Function = createPatchFunction({ nodeOps, modules })
通過程式碼可以看到,最終 vue 是呼叫了 createPatchFunction 函式,它定義在 src/core/vdom/patch.js 中。createPatchFunction 函式內部定義瞭如 emptyNodeAt、removeNode、createElement、createChildren 等一系列的輔助函式,通過這些輔助函式,完成了對 patch 函式的程式碼邏輯的封裝。
2.2 初始化的 patch
建立Vue例項,或者元件例項的,patch 都會被執行。
如果是建立vue例項執行 patch
isRealElement:判斷是否是真實的DOM節點。
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly):負責DOM的更新。
oldVnode = emptyNodeAt(oldVnode):對容器DOM進行vnode的轉化。
createElm():建立新節點,初始化建立需要重點關注的函式。
如果是建立元件例項執行的 patch
isInitialPatch:使用者判斷子元件否初次執行 patch,進行建立。
insertedVnodeQueue:新建立子元件節點,元件 vnode 會被push到這個佇列中。
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)。
具體程式碼註釋如下
export function createPatchFunction (backend) {
...
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// 如果新的 vnode 為空,呼叫 destory 鉤子,銷燬oldVnode
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
// 使用者判斷子元件否初次執行 patch,進行建立。
let isInitialPatch = false
// 新建立子元件節點,元件 vnode 會被push到這個佇列中
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
// 空掛載(可能作為元件),建立新的根元素
isInitialPatch = true
// 建立元件節點的子元素
createElm(vnode, insertedVnodeQueue)
} else {
// 1.作為判斷是否是真實的DOM節點條件
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
// 更新操作
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
...
// either not server-rendered, or hydration failed.
// create an empty node and replace it
// 2. 傳入的容器DOM(如 el: "#app"),會在這裡被轉化成 vnode。
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
// 3. 建立新節點
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// update parent placeholder node element, recursively
// 遞迴的更新父佔位節點元素。
if (isDef(vnode.parent)) {...
}
// destroy old node
// 銷燬舊節點
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
// 呼叫 insertedVnodeQueue 佇列中所有子元件的 insert 鉤子。
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
三、createElm 動態建立DOM
createElm
函式是動態建立 DOM 的核心,作用是通過 vnode 建立真實的 DOM,並插入到它的父 DOM 節點中。它定義在 src/core/vdom/patch.js
的 createPatchFunction 方法中。createElm 內部建立 DOM 的主要判斷邏輯,可以概括為下面幾種情況。
1、如果建立元件節點
如果碰到子元件標籤,走建立元件節點邏輯。
建立完成,插入到父親元素中。
2、如果建立標籤元素節點
如果 vnode.tag 不為空,先建立標籤元素, 賦值 vnode.elm 進行佔位。
呼叫
createChildren
建立子節點,最終這些子節點會 append 到 vnode.elm 標籤元素中。將 vnode.elm 標籤元素插入到父親元素中。
3、如果建立註釋節點
如果 vnode.isComment 不為空,建立註釋節點,賦值 vnode.elm。
將註釋節點插入到父親元素中。
4、如果建立文字節點
上面三種情況都不是,則建立文字節點,賦值 vnode.elm。
將文字節點插入到父親元素中。
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested // for transition enter check
// 1、如果碰到子元件標籤,走建立元件節點邏輯,插入父親節點。
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
// 2、如果是標籤標記,先建立標籤元素進行佔位。
// 呼叫 createChildren 建立子節點(遞迴呼叫createElm)。
// 將標籤元素,插入父親元素。
if (isDef(tag)) {
if (process.env.NODE_ENV !== 'production') {
if (data && data.pre) {
creatingElmInVPre++
}
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
// 通過上面的tag,建立標籤元素,賦值給 vnode.elm 進行佔位
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
// in Weex, the default insertion order is parent-first.
// List items can be optimized to use children-first insertion
// with append="tree".
const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
if (!appendAsTree) {
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
createChildren(vnode, children, insertedVnodeQueue)
if (appendAsTree) {
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
} else {
// 建立子節點
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// 將建立的標籤元素節點,插入父親元素
insert(parentElm, vnode.elm, refElm)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
} else if (isTrue(vnode.isComment)) {
// 3、建立註釋節點,插入到父親元素
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
// 4、建立文字節點,插入到父親元素
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
下面對動態建立的幾種情況分別進行說明。
3.1 建立元件節點
建立元件節點和 vue 的元件系統息息相關,這裡先不具體展開,之後的博文中單獨分析 vue 元件系統。只需要記住 vue 模板裡的子元件初始化建立,是在這一步進行即可。
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
...
// 建立元件節點
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
...
}
createComponent 這個方法也定義在 src/core/vdom/patch.js 的 createPatchFunction 的方法中,這裡先簡單的介紹一下這個方法的內部邏輯。
通過 vnode.data 中是否包含元件相關的 hook,來判斷當前 vnode 是否是子元件 vnode(元件的 vnode,會包含 init 等鉤子方法)。
呼叫 init,執行子元件的初始化流程,建立子元件例項,進行子元件掛載。
將生成的子元件 DOM 賦值給 vnode.elm。
通過 vnode.elm 將建立的子元件節點,插入到父親元素中。
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// 呼叫元件 init 鉤子後,會執行子元件的**初始化流程**
// 建立子元件例項,進行子元件掛載。
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
// 如果是元件例項,將建立 vnode.elm 佔位符
// 將生成的元件節點,插入到父親元素中
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
如果是建立元件節點,並且成功,createComponent 函式返回 true。createElm 函式執行到 return。
如果是其他型別的節點,createComponent 函式返回 undefined,createElm 函式,會向下執行建立其他型別節點(標籤元素、註釋、文字)的程式碼邏輯。
綜上所述,createElm 函式執行,只要碰到元件標籤,會遞迴的去初始化建立子元件,簡圖如下所示(綠色線路部分)。
再呼叫 insert(parentElm, vnode.elm, refElm),將生成的元件節點插入到父親元素中(遵從先子後父)。
3.2 建立標籤元素節點
createElm
判斷如果 vnode 不是元件的 vnode,它會判斷是否是標籤元素,從而進行建立標籤元素節點的程式碼邏輯, 主要邏輯分析如下。
vnode.tag 標籤屬性存在,通過 tag 建立對應的標籤元素,賦值給 vnode.elm 進行佔位。
呼叫 createChildren 建立子節點(遍歷子vnode,遞迴呼叫 createElm 函式)。
將建立的標籤元素節點,插入父親元素。
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
...
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
// 2、如果是標籤標記,先建立標籤元素進行佔位。
// 呼叫 createChildren 建立子節點(遍歷子vnode,遞迴呼叫 createElm 函式)。
// 將標籤元素,插入父親元素。
// 如果標籤屬性不為空
if (isDef(tag)) {
if (process.env.NODE_ENV !== 'production') {
if (data && data.pre) {
creatingElmInVPre++
}
// 不合法的標籤進行提示
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
// 通過上面的tag,建立標籤元素,賦值給 vnode.elm 進行佔位
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
...
} else {
// 建立子節點
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
// vnode.data 不為空,呼叫所有create的鉤子。
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// 將建立的標籤元素節點,插入父親元素
insert(parentElm, vnode.elm, refElm)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
...
}
createChildren
函式主要邏輯如下
如果 vnode.children 是子 vnode 陣列,遍歷 vnode.children 中的每個子 vnode,遞迴的呼叫了 createElm 函式,建立對應的子節點,並插入到父親元素中(此時的父親元素 parentElm 為 vnode.elm)。
如果 vnode.text 為空字串。就建立一個空文字節點,插入到 vnode.elm 元素中。
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(children)
}
// 遍歷子vnode陣列,遞迴呼叫 createElm
for (let i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
}
} else if (isPrimitive(vnode.text)) {
// 建立空文字節點,appendChildren 到 vnode.elm 中
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
}
}
上面已經建立完成子標籤節點,invokeCreateHooks 呼叫執行所有子元件相關的 create 鉤子。這個方法createElm、
initComponent 中都會被呼叫。如果在 initComponent 中呼叫,說明建立的子節點中有元件節點,還會將元件 vnode 新增到 insertedVnodeQueue 佇列中。
// createElm 中
if (isDef(data)) {
// vnode.data 不為空,呼叫所有create的鉤子。
invokeCreateHooks(vnode, insertedVnodeQueue)
}
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
// empty component root.
// skip all element-related modules except for ref (#3455)
registerRef(vnode)
// make sure to invoke the insert hook
insertedVnodeQueue.push(vnode)
}
}
function invokeCreateHooks (vnode, insertedVnodeQueue) {
// 所有元件相關的create鉤子都呼叫
// initComponent呼叫的話,還會將各個子元件的 vnode 新增到 insertedVnodeQueue 佇列中。
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
綜上所述,createElm 建立標籤節點內部通過 createChildren 實現了對 createElm 的遍歷遞迴呼叫,實現了深度優先遍歷,簡圖如下所示(藍色線路部分)。
再呼叫 insert(parentElm, vnode.elm, refElm),將生成的元素節點插入到父親元素中(遵從先子後父)。
3.3 建立註釋節點
如果不是建立元件節點和元素節點,vue 就通過 vnode.isComment 屬性判斷,是否建立註釋節點。建立完成之後,插入到父親元素中(遵從先子後父)
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
3.3 建立文字節點
如果不是建立元件節點、元素節點、註釋節點,vue 就建立文字節點,建立完成之後,插入到父親元素中(遵從先子後父)。
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
四、銷燬舊節點
通過前面章節的分析,知道了 patch 函式,主要通過 createElm 動態的建立好了 DOM,並且已經成功新增到了舊DOM的後面,所以下一步操作,就只需要將舊 DOM 進行刪除即可。
// destroy old node
// 銷燬舊的節點(如 el: "app" 這個DOM)
// 建立完成的整個dom會append到 el: "app", 的父親元素(如 parentElm 為 body)上
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
五、總結
vue 通過呼叫 patch 函式進行初始化 DOM 的建立。
patch 的關鍵是理解內部 createElm 這個函式,它會判斷元件、元素、註釋、文字這些型別的節點,來建立相應的DOM,完成之後新增到父元素。
vue 的元件系統實現,關鍵在於動態建立元件節點的邏輯當中。
新 DOM 建立新增過程是從子到父的,而元件的例項化是從父到子的。