經過Vue 源碼探祕(九)(createComponent)的分析咱們知道,當咱們經過 createComponent
建立了組件 VNode
,接下來會走到 vm._update
,執行 vm.__patch__
把 VNode
轉換成真正的 DOM
節點。以前分析的是針對一個普通的 VNode
節點,接下來咱們來看看組件的 VNode
會有哪些不同的地方。vue
patch
函數的核心步驟是調用 createElm
函數來建立節點,這一節咱們再次回顧這個函數,看它是怎麼處理組件的 VNode
的。node
這一節咱們依然圍繞上一節的例子來分析:react
import Vue from "vue";
import App from "./App.vue";
var app = new Vue({
el: "#app",
// 這裏的 h 是 createElement 方法
render: h => h(App)
});
複製代碼
回顧一下 createElm
的實現,它的定義在 src/core/vdom/patch.js
中,在函數的一開始有這麼一段代碼:web
// src/core/vdom/patch.js
function createElm(
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return;
}
// ..
}
複製代碼
這裏判斷若是調用 createComponent
函數返回 true
,則結束執行 createElm
函數。傳給 createComponent
函數的 vnode
參數是組件 VNode
,所以 createComponent
函數返回 true
,不會再往下執行。來看 createComponent
函數的定義:app
// src/core/vdom/patch.js
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data;
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
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.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue);
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true;
}
}
}
複製代碼
函數一開始的 isReactivated
和 Keep-alive
相關,暫時不展開講。而後接下來的 if
語句的意思是判斷 vnode.data.hook.init
是否存在,這裏vnode
是一個組件 VNode
,那麼條件知足,而且獲得 i
就是 init
鉤子函數。dom
回顧Vue 源碼探祕(九)(createComponent),在執行 createComponent
函數的時候會調用 installComponentHooks
函數給 vnode.data.hook
安裝四個鉤子函數。回顧 init
鉤子函數的代碼,它被定義在 src/core/vdom/create-component.js
文件中:編輯器
// src/core/vdom/create-component.js
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
}
複製代碼
if
語句依然是 Keep-alive
相關,咱們先跳過。else
邏輯調用了 createComponentInstanceForVnode
函數建立一個Vue
實例,傳入 vnode
、activeInstance
兩個參數。activeInstance
是指什麼呢,在 src/core/instance/lifecycle.js
文件中有這麼幾行代碼:ide
// src/core/instance/lifecycle.js
export let activeInstance: any = null;
export function lifecycleMixin(Vue: Class<Component>) {
Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) {
// ...
const restoreActiveInstance = setActiveInstance(vm);
// setActiveInstance內: const prevActiveInstance = activeInstance
// activeInstance = vm
vm._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);
}
restoreActiveInstance();
// setActiveInstance返回:activeInstance = prevActiveInstance
// ...
};
// ...
}
複製代碼
這裏面在先後分別調用了
setActiveInstance(vm)
和他的返回值,我把函數內部的邏輯揉進了總體代碼邏輯中。函數
能夠看到,activeInstance
是一個全局變量,在調用 __patch__
前先用 prevActiveInstance
保存 activeInstance
,而後將當前實例 vm
賦給 activeInstance
,在執行完 __patch__
後再恢復 activeInstance
原來的值。那爲何要這樣作呢,咱們帶着這個疑問繼續往下看。組件化
咱們回過來繼續看 createComponentInstanceForVnode
函數的代碼:
// src/core/vdom/create-component.js
export function createComponentInstanceForVnode(
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any // activeInstance in lifecycle state
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
};
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate;
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render;
options.staticRenderFns = inlineTemplate.staticRenderFns;
}
return new vnode.componentOptions.Ctor(options);
}
複製代碼
這裏定義了一個options
,將 vnode
做爲 _parentVnode
,將 activeInstance
做爲 parent
。後面和 inline-template
相關的先略過。最後經過構造函數 vnode.componentOptions.Ctor
建立一個對象並返回,並傳入 options
做爲參數。
這裏的 vnode.componentOptions.Ctor
對應的就是子組件的構造函數,回顧上一節,咱們知道它其實是繼承於 Vue
的一個構造器 Sub
,至關於 new Sub(options)
。
回顧 Vue.extend
函數是怎麼定義子構造函數的:
const Sub = function VueComponent(options) {
this._init(options);
};
複製代碼
這裏子構造函數繼承了 Vue.prototype
上的 _init
函數,因此 createComponentInstanceForVnode
函數最後就是將 options
傳給了 Vue.prototype._init
函數並執行。
來看下init
方法:
// src/core/instance/init.js
Vue.prototype._init = function(options?: Object) {
// ...
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
// ...
};
複製代碼
這一段是合併 options
的操做,如今咱們傳入的 options
的 _isComponent
屬性爲 true
,會走 if
邏輯調用 initInternalComponent
函數。簡單看下initInternalComponent
的實現:
// src/core/instance/init.js
export function initInternalComponent(
vm: Component,
options: InternalComponentOptions
) {
const opts = (vm.$options = Object.create(vm.constructor.options));
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode;
opts.parent = options.parent;
opts._parentVnode = parentVnode;
const vnodeComponentOptions = parentVnode.componentOptions;
opts.propsData = vnodeComponentOptions.propsData;
opts._parentListeners = vnodeComponentOptions.listeners;
opts._renderChildren = vnodeComponentOptions.children;
opts._componentTag = vnodeComponentOptions.tag;
if (options.render) {
opts.render = options.render;
opts.staticRenderFns = options.staticRenderFns;
}
}
複製代碼
initInternalComponent
函數的做用就是往 vm.$options
上添加屬性,這裏要重點關注的屬性是 _parentVnode
和 parent
,這兩個分別對應一開始在 init
鉤子函數傳入的 VNode
和 activeInstance
參數。
回到 _init
函數,來看最後一段代碼:
Vue.prototype._init = function(options?: Object) {
// ...
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
複製代碼
這一段代碼也是組件實例與普通實例不一樣之處,因爲組件沒有 el
,因此不會執行 if
語句中的邏輯。從新回到鉤子函數 init
:
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if ( /* ... */ ) { // ...
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
}
複製代碼
在調用 createComponentInstanceForVnode
函數建立了組件實例後,調用了組件實例的 $mount
方法。
回顧Vue 源碼探祕(四)(實例掛載 $mount),$mount
方法分爲原型定義和重寫兩部分,重寫部分就是多了將 template
轉換爲 render
函數的步驟,而組件在編譯時就生成了 render
函數,不會執行重寫部分,只執行了原型定義的 $mount
函數。回顧原型上定義的 $mount
函數:
// src/platforms/web/runtime/index.js
Vue.prototype.$mount = function(
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating);
};
複製代碼
$mount
函數最終調用了 mountComponent
函數。咱們知道 mountComponent
函數會建立一個 Watcher
對象並調用 updateComponent
函數,進而執行vm._render()
方法:
Vue.prototype._render = function(): VNode {
const vm: Component = this;
const { render, _parentVnode } = vm.$options;
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode;
// render self
let vnode;
try {
vnode = render.call(vm._renderProxy, vm.$createElement);
} catch (e) {
// ...
}
// set parent
vnode.parent = _parentVnode;
return vnode;
};
複製代碼
能夠看到 _render
函數拿到 vm.$options._parentVNode
,也就是佔位符 VNode
,對應例子裏面的 App
組件的 VNode
,將它賦值給 vm.$vnode
。以後經過組件的渲染函數 render
建立渲染 vnode
,並把 _parentVnode
賦給了 vnode.parent
。
以後建立出來的渲染 vnode
傳給了 _update
函數:
// src/core/instance/lifecycle.js
Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) {
const vm: Component = this;
const prevEl = vm.$el;
const prevVnode = vm._vnode;
const prevActiveInstance = activeInstance;
activeInstance = vm;
vm._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 = prevActiveInstance;
// 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.
};
複製代碼
也就是說咱們繞了一大圈又回到了 _update
函數。這是由於 Vue
的初始化是深度優先搜索
的過程,Vue
實例引用了組件,若是組件中又引用了組件,那麼它就會一直執行上述流程直到一個 vm
實例完成它的全部子樹的 patch
或者 update
過程。
_update
過程當中有幾個關鍵的代碼,首先 vm._vnode = vnode
的邏輯,這個 vnode
是經過 vm._render()
返回的組件渲染 VNode
,vm._vnode
和 vm.$vnode
的關係就是一種父子關係,用代碼表達就是 vm._vnode.parent === vm.$vnode
。
回顧這一節的內容,在實例化子組件的時候,咱們須要知道這個子組件的父實例是誰,把它存入 vm.$options
中,後面調用 initLifecycle
函數的時候再把它的父實例保存到 vm.$parent
,同時經過 parent.$children.push(vm)
來把子組件的 vm
存儲到父實例的 $children
中,經過這些操做創建父子關係。
activeInstance
的做用就體如今這裏,在 vm._update
的過程當中,把當前的 vm
賦值給 activeInstance
,同時 prevActiveInstance
保留上一次的 activeInstance
。當一個 vm
實例完成它的全部子樹的 patch
或者 update
過程後,activeInstance
經過 prevActiveInstance
又回到它的父實例,這樣就完美地保證了在整個深度遍歷過程當中,在實例化子組件的時候能傳入當前子組件的父 Vue
實例,並在 initLifecycle
的過程當中,經過 vm.$parent
把這個父子關係保留。
回到 _update
函數,這裏又再次調用了 __patch__
方法,而後又再次執行 patch
方法當中的 createElm
方法,就又回到本節開頭提到的判斷:
function createElm(
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return;
}
const data = vnode.data;
const children = vnode.children;
const tag = vnode.tag;
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
);
}
}
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)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
}
if (process.env.NODE_ENV !== "production" && data && data.pre) {
creatingElmInVPre--;
}
} 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);
}
}
複製代碼
這一次傳給 createComponent
函數的 vnode
是渲染 VNode
而不是組件 VNode
,所以會繼續往下執行。
往下的邏輯以前在分析元素節點 VNode
時已經分析過了,而其中有一點區別就是執行 insert(parentElm, vnode.elm, refElm)
這條語句時,因爲 parentElm
參數對應的是 vm.$el
,而組件實例是沒有 $el
的,所以這裏的 parentElm
是 undefined
,再來看 insert
函數的定義:
// src/core/vdom/patch.js
function insert(parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (nodeOps.parentNode(ref) === parent) {
nodeOps.insertBefore(parent, elm, ref);
}
} else {
nodeOps.appendChild(parent, elm);
}
}
}
複製代碼
能夠看到,沒有 parentElm
的話是不會執行插入操做的,那插入操做是在哪裏執行的呢,咱們回顧 createComponent
函數:
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data;
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
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.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue);
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true;
}
}
}
複製代碼
執行完 initComponent
函數後又執行了 insert
函數,插入過程就是在這裏執行的,由於這裏的 parentElm
參數是有值的。
因爲整個過程是一個深度遍歷的過程,若是組件中又嵌套子組件,那麼它會遞歸調用 createComponent
函數完成子組件的一系列過程,所以整個 DOM
的插入順序是先子後父
。
本節帶你們梳理了一個組件的 VNode
建立、初始化、渲染的完整流程。組件的 patch
過程相對於普通元素的 patch
來講複雜了許多,這部分須要反覆翻看,經過斷點調試等方法來加深理解。
在對組件化的實現有一個大概瞭解後,接下來咱們來介紹一下這其中的一些細節。咱們知道編寫一個組件其實是編寫一個 JavaScript
對象,對象的描述就是各類配置,以前咱們提到在 _init
的最初階段執行的就是 merge options
的邏輯:
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
複製代碼
那麼下一節咱們從源碼角度來分析合併配置的過程。
本文使用 mdnice 排版