前言
各位道友大家好,我是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 創建添加過程是從子到父的,而組件的實例化是從父到子的。