一、組件化
-
Vue.js
另一個核心思想是組件化。所謂組件化,就是把頁面拆分成多個組件(component)
,每個組件依賴的CSS、JavaScript
、模板、圖片等資源放在一起開發和維護。組件是資源獨立的,組件在系統內部可複用,組件和組件之間可以嵌套。 -
我們在用
Vue.js
開發實際項目的時候,就是像搭積木一樣,編寫一堆組件拼裝生成頁面。在Vue.js
的官網中,也是花了大篇幅來介紹什麼是組件,如何編寫組件以及組件擁有的屬性和特性。我們將從源碼的角度來分析Vue
的組件內部是如何工作的,只有瞭解了內部的工作原理,才能讓我們使用它的時候更加得心應手。 -
接下來我們會用
Vue-cli
初始化的代碼爲例,來分析一下Vue
組件初始化的一個過程,如下所示:
import Vue from 'vue'
import App from './App.vue'
var app = new Vue({
el: '#app',
// 這裏的 h 是 createElement 方法
render: h => h(App)
})
和之前相同的點也是通過
render
函數去渲染的,不同的這次通過createElement
傳的參數是一個組件而不是一個原生的標籤,那麼接下來我們就開始分析這一過程。
二、createComponent 的理解
- 我們之前在分析
createElement
的實現的時候,它最終會調用_createElement
方法,其中有一段邏輯是對參數tag
的判斷,如果是一個普通的html
標籤,是一個普通的 div,則會實例化一個普通VNode
節點,否則通過createComponent
方法創建一個組件VNode
,如下所示:
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
- 在這裏傳入的是一個
App
對象,它本質上是一個Component
類型,那麼它會走到上述代碼的else
邏輯,直接通過createComponent
方法來創建vnode
。所以接下來我們來看一下createComponent
方法的實現,它定義在src/core/vdom/create-component.js
文件中,如下所示:
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
// if at this stage it's not a constructor or an async component factory,
// reject.
if (typeof Ctor !== 'function') {
if (process.env.NODE_ENV !== 'production') {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}
// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
if (Ctor === undefined) {
// return a placeholder node for async component, which is rendered
// as a comment node but preserves all the raw information for the node.
// the information will be used for async server-rendering and hydration.
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
data = data || {}
// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor)
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// functional component
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn
if (isTrue(Ctor.options.abstract)) {
// abstract components do not keep anything
// other than props & listeners & slot
// work around flow
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
// install component management hooks onto the placeholder node
installComponentHooks(data)
// return a placeholder vnode
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
// Weex specific: invoke recycle-list optimized @render function for
// extracting cell-slot template.
// https://github.com/Hanks10100/weex-native-directive/tree/master/component
/* istanbul ignore if */
if (__WEEX__ && isRecyclableComponent(vnode)) {
return renderRecyclableComponentTemplate(vnode)
}
return vnode
}
-
從上面可以看到,
createComponent
的邏輯也會有一些複雜,但是分析源碼比較推薦的是隻分析核心流程,分支流程可以之後針對性的看,所以這裏針對組件渲染這個 case 主要就三個關鍵步驟:
構造子類構造函數,安裝組件鉤子函數和實例化vnode
。 -
構造子類構造函數,如下所示:
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
我們在編寫一個組件的時候,通常都是創建一個普通對象,還是以我們的 App.vue 爲例,代碼如下:
import HelloWorld from './components/HelloWorld'
export default {
name: 'app',
components: {
HelloWorld
}
}
- 這裏
export
的是一個對象,所以createComponent
裏的代碼邏輯會執行到baseCtor.extend(Ctor)
,在這裏baseCtor
實際上就是Vue
,這個的定義是在最開始初始化Vue
的階段,在src/core/global-api/index.js
中的initGlobalAPI
函數有這麼一段邏輯:
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue
- 這裏定義的是
Vue.options
,而我們的createComponent
取的是context.$options
,實際上在src/core/instance/init.js
裏Vue
原型上的_init
函數中有這麼一段邏輯:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
-
這樣就把
Vue
上的一些option
擴展到了vm.$options
上,所以我們也就能通過vm.$options._base
拿到 Vue 這個構造函數了。mergeOptions
的實現我們會在後面具體分析,現在只需要理解它的功能是把Vue
構造函數的options
和用戶傳入的options
做一層合併,到vm.$options
上。 -
在瞭解了
baseCtor
指向了Vue
之後,我們來看一下Vue.extend
函數的定義,在src/core/global-api/extend.js
中,如下所示:
/**
* Class inheritance
*/
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
validateComponentName(name)
}
const Sub = function VueComponent (options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// allow further extension/mixin/plugin usage
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub
}
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// cache constructor
cachedCtors[SuperId] = Sub
return Sub
}
-
Vue.extend
的作用就是構造一個Vue
的子類,它使用一種非常經典的原型繼承的方式把一個純對象轉換一個繼承於Vue
的構造器Sub
並返回,然後對Sub
這個對象本身擴展了一些屬性,如擴展options
、添加全局API
等;並且對配置中的props
和computed
做了初始化工作;最後對於這個Sub
構造函數做了緩存,避免多次執行Vue.extend
的時候對同一個子組件重複構造。 -
這樣當我們去實例化
Sub
的時候,就會執行this._init
邏輯再次走到了Vue
實例的初始化邏輯,如下所示:
const Sub = function VueComponent (options) {
this._init(options)
}
- 安裝組件鉤子函數,如下所示:
// install component management hooks onto the placeholder node
installComponentHooks(data)
- 我們之前提到
Vue.js
使用的Virtual DOM
參考的是開源庫 snabbdom,它的一個特點是在VNode
的patch
流程中對外暴露了各種時機的鉤子函數,方便我們做一些額外的事情,Vue.js
也是充分利用這一點,在初始化一個Component
類型的VNode
的過程中實現了幾個鉤子函數:
const componentVNodeHooks = {
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)
}
},
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
},
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
// vue-router#1212
// During updates, a kept-alive component's child components may
// change, so directly walking the tree here may call activated hooks
// on incorrect children. Instead we push them into a queue which will
// be processed after the whole patch process ended.
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
},
destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
}
const hooksToMerge = Object.keys(componentVNodeHooks)
function installComponentHooks (data: VNodeData) {
const hooks = data.hook || (data.hook = {})
for (let i = 0; i < hooksToMerge.length; i++) {
const key = hooksToMerge[i]
const existing = hooks[key]
const toMerge = componentVNodeHooks[key]
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
}
}
}
function mergeHook (f1: any, f2: any): Function {
const merged = (a, b) => {
// flow complains about extra args which is why we use any
f1(a, b)
f2(a, b)
}
merged._merged = true
return merged
}
-
整個
installComponentHooks
的過程就是把componentVNodeHooks
的鉤子函數合併到data.hook
中,在VNode
執行patch
的過程中執行相關的鉤子函數。這裏要注意的是合併策略,在合併過程中,如果某個時機的鉤子已經存在data.hook
中,那麼通過執行mergeHook
函數做合併,這個邏輯很簡單,就是在最終執行的時候,依次執行這兩個鉤子函數即可。 -
實例化
VNode
,如下所示:
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
最後一步非常簡單,通過
new VNode
實例化一個vnode
並返回。需要注意的是和普通元素節點的vnode
不同,組件的vnode
是沒有children
的,這點很關鍵,在之後的patch
過程中我們會再提。
- 總結:這裏我們分析了
createComponent
的實現,瞭解到它在渲染一個組件的時候的三個關鍵邏輯:構造子類構造函數,安裝組件鉤子函數和實例化vnode
。createComponent
後返回的是組件vnode
,它也一樣走到vm._update
方法,進而執行了patch
函數。
三、patch 的理解
-
當我們通過
createComponent
創建了組件VNode
,接下來會走到vm._update
,執行vm.__patch__
去把VNode
轉換成真正的DOM
節點。針對一個普通的VNode
節點,會有哪些不一樣的地方。 -
patch
的過程會調用createElm
創建元素節點,回顧一下createElm
的實現,它的定義在src/core/vdom/patch.js
中:
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// ...
}
createComponent
,我們刪掉多餘的代碼,只保留關鍵的邏輯,這裏會判斷createComponent(vnode, insertedVnodeQueue, parentElm, refElm)
的返回值,如果爲true
則直接結束,那麼接下來看一下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
}
}
}
createComponent
函數中,首先對vnode.data
做了一些判斷:
let i = vnode.data
if (isDef(i)) {
// ...
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
// ...
}
// ..
}
如果
vnode
是一個組件 VNode,那麼條件會滿足,並且得到i
就是init
鉤子函數,回顧上節我們在創建組件 VNode 的時候合併鉤子函數中就包含init
鉤子函數,定義在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)
}
},
init
鉤子函數執行也很簡單,我們先不考慮keepAlive
的情況,它是通過createComponentInstanceForVnode
創建一個 Vue 的實例,然後調用$mount
方法掛載子組件,
先來看一下createComponentInstanceForVnode
的實現:
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)
}
createComponentInstanceForVnode
函數構造的一個內部組件的參數,然後執行new vnode.componentOptions.Ctor(options)
。這裏的vnode.componentOptions.Ctor
對應的就是子組件的構造函數,它實際上是繼承於Vue
的一個構造器Sub
,相當於new Sub(options)
這裏有幾個關鍵參數要注意幾個點,_isComponent
爲true
表示它是一個組件,parent
表示當前激活的組件實例。所以子組件的實例化實際上就是在這個時機執行的,並且它會執行實例的_init
方法,這個過程有一些和之前不同的地方需要挑出來說,代碼在src/core/instance/init.js
中:
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// 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
)
}
// ...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
這裏首先是合併
options
的過程有變化,_isComponent
爲 true,所以走到了initInternalComponent
過程,這個函數的實現也簡單看一下:
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
}
}
- 這個過程我們重點記住以下幾個點即可:
opts.parent = options.parent
、opts._parentVnode = parentVnode
,它們是把之前我們通過createComponentInstanceForVnode
函數傳入的幾個參數合併到內部的選項$options
裏了。再來看一下_init
函數最後執行的代碼:
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
- 由於組件初始化的時候是不傳
el
的,因此組件是自己接管了$mount
的過程,回到組件init
的過程,componentVNodeHooks
的init
鉤子函數,在完成實例化的_init
後,接着會執行child.$mount(hydrating ? vnode.elm : undefined, hydrating)
。這裏hydrating
爲true
一般是服務端渲染的情況,我們只考慮客戶端渲染,所以這裏$mount
相當於執行child.$mount(undefined, false)
,它最終會調用mountComponent
方法,進而執行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
}
-
我們只保留關鍵部分的代碼,這裏的
_parentVnode
就是當前組件的父VNode
,而render
函數生成的vnode
當前組件的渲染vnode
,vnode
的parent
指向了_parentVnode
,也就是vm.$vnode
,它們是一種父子的關係。 -
我們知道在執行完
vm._render
生成VNode
後,接下來就要執行vm._update
去渲染VNode
了。來看一下組件渲染的過程中有哪些需要注意的,vm._update
的定義在src/core/instance/lifecycle.js
中:
export let activeInstance: any = null
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
過程中有幾個關鍵的代碼,首先vm._vnode = vnode
的邏輯,這個vnode
是通過vm._render()
返回的組件渲染VNode
,vm._vnode
和vm.$vnode
的關係就是一種父子關係,用代碼表達就是vm._vnode.parent === vm.$vnode
,還有一段比較有意思的代碼:
export let activeInstance: any = null
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
// ...
const prevActiveInstance = activeInstance
activeInstance = vm
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
// ...
}
- 這個
activeInstance
作用就是保持當前上下文的Vue
實例,它是在lifecycle
模塊的全局變量,定義是export let activeInstance: any = null
,並且在之前我們調用createComponentInstanceForVnode
方法的時候從lifecycle
模塊獲取,並且作爲參數傳入的。因爲實際上JavaScript
是一個單線程,Vue
整個初始化是一個深度遍歷的過程,在實例化子組件的過程中,它需要知道當前上下文的Vue
實例是什麼,並把它作爲子組件的父Vue
實例。子組件的實例化過程先會調用initInternalComponent(vm, options)
合併options
,把parent
存儲在vm.$options
中,在$mount
之前會調用initLifecycle(vm)
方法,如下所示:
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
// ...
}
可以看到
vm.$parent
就是用來保留當前vm
的父實例,並且通過parent.$children.push(vm)
來把當前的vm
存儲到父實例的$children
中。
- 在
vm._update
的過程中,把當前的vm
賦值給activeInstance
,同時通過const prevActiveInstance = activeInstance
用prevActiveInstance
保留上一次的activeInstance
。實際上,prevActiveInstance
和當前的vm
是一個父子關係,當一個vm
實例完成它的所有子樹的patch
或者update
過程後,activeInstance
會回到它的父實例,這樣就完美地保證了createComponentInstanceForVnode
整個深度遍歷過程中,我們在實例化子組件的時候能傳入當前子組件的父Vue
實例,並在_init
的過程中,通過vm.$parent
把這個父子關係保留。
那麼回到_update
,最後就是調用__patch__
渲染 VNode 了,如下所示:
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
// ...
}
// ...
}
- 這裏又回到了開始的過程,之前分析過負責渲染成
DOM
的函數是createElm
,注意這裏我們只傳了 兩個參數,所以對應的parentElm
是undefined
,我們再來看看它的定義,如下所示:
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)) {
// ...
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)
}
// ...
} 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)
}
}
-
注意,這裏我們傳入的
vnode
是組件渲染的vnode
,也就是我們之前說的vm._vnode
,如果組件的根節點是個普通元素,那麼vm._vnode
也是普通的vnode
,這裏createComponent(vnode, insertedVnodeQueue, parentElm, refElm)
的返回值是false
。接下來的過程就和之前一樣了,先創建一個父節點佔位符,然後再遍歷所有子VNode
遞歸調用createElm
,在遍歷的過程中,如果遇到子VNode
是一個組件的VNode
,則重複本節開始的過程,這樣通過一個遞歸的方式就可以完整地構建了整個組件樹。 -
由於我們這個時候傳入的
parentElm
是空,所以對組件的插入,在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, false /* hydrating */)
}
// ...
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
在完成組件的整個
patch
過程後,最後執行insert(parentElm, vnode.elm, refElm)
完成組件的 DOM 插入,如果組件patch
過程中又創建了子組件,那麼DOM 的插入順序是先子後父。
- 總結:一個組件的
VNode
是如何創建、初始化、渲染的過程也就介紹完畢了。在對組件化的實現有一個大概瞭解後,接下來我們來介紹一下這其中的一些細節。我們知道編寫一個組件實際上是編寫一個JavaScript
對象,對象的描述就是各種配置,之前我們提到在_init
的最初階段執行的就是merge options
的邏輯,那麼接下來我們從源碼角度來分析合併配置的過程。
四、合併配置
-
new Vue
的過程通常有兩種場景,一種是外部我們的代碼主動調用new Vue(options)
的方式實例化一個Vue
對象;另一種是我們上一節分析的組件過程中內部通過new Vue(options)
實例化子組件。 -
無論哪種場景,都會執行實例的
_init(options)
方法,它首先會執行一個merge options
的邏輯,相關的代碼在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
值也有非常大的不同,接下來會從兩種場景的options
合併過程。爲了更直觀,我們可以舉個簡單的示例,如下所示:
import Vue from 'vue'
let childComp = {
template: '<div>{{msg}}</div>',
created() {
console.log('child created')
},
mounted() {
console.log('child mounted')
},
data() {
return {
msg: 'Hello Vue'
}
}
}
Vue.mixin({
created() {
console.log('parent created')
}
})
let app = new Vue({
el: '#app',
render: h => h(childComp)
})
- 外部調用場景,當執行
new Vue
的時候,在執行this._init(options)
的時候,就會執行如下邏輯去合併options
:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
- 這裏通過調用
mergeOptions
方法來合併,它實際上就是把resolveConstructorOptions(vm.constructor)
的返回值和options
做合併,resolveConstructorOptions
的實現先不考慮,在我們這個場景下,它還是簡單返回vm.constructor.options
,相當於Vue.options
,那麼這個值又是什麼呢,其實在initGlobalAPI(Vue)
的時候定義了這個值,代碼在src/core/global-api/index.js
中:
export function initGlobalAPI (Vue: GlobalAPI) {
// ...
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue
extend(Vue.options.components, builtInComponents)
// ...
}
- 首先通過
Vue.options = Object.create(null)
創建一個空對象,然後遍歷ASSET_TYPES
,ASSET_TYPES
的定義在src/shared/constants.js
中:
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
所以上面遍歷
ASSET_TYPES
後的代碼相當於:
Vue.options.components = {}
Vue.options.directives = {}
Vue.options.filters = {}
-
接着執行了
Vue.options._base = Vue
,它的作用在我們上節實例化子組件的時候介紹了。 -
最後通過
extend(Vue.options.components, builtInComponents)
把一些內置組件擴展到Vue.options.components
上,Vue
的內置組件目前有<keep-alive>
、<transition>
和<transition-group>
組件,這也就是爲什麼我們在其它組件中使用<keep-alive>
組件不需要註冊的原因。 -
那麼回到
mergeOptions
這個函數,它的定義在src/core/util/options.js
中:
/**
* Merge two option objects into a new one.
* Core utility used in both instantiation and inheritance.
*/
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
const extendsFrom = child.extends
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
-
mergeOptions
主要功能就是把parent
和child
這兩個對象根據一些合併策略,合併成一個新對象並返回。比較核心的幾步,先遞歸把extends
和mixins
合併到parent
上,然後遍歷parent
,調用mergeField
,然後再遍歷child
,如果key
不在parent
的自身屬性上,則調用mergeField
。 -
這裏有意思的是
mergeField
函數,它對不同的key
有着不同的合併策略。舉例來說,對於生命週期函數,它的合併策略是這樣的,如下所示:
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
}
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
這其中的
LIFECYCLE_HOOKS
的定義在src/shared/constants.js
中:
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured'
]
-
這裏定義了
Vue.js
所有的鉤子函數名稱,所以對於鉤子函數,他們的合併策略都是mergeHook
函數。這個函數的實現也非常有意思,用了一個多層三元運算符,邏輯就是如果不存在childVal
,就返回parentVal
;否則再判斷是否存在parentVal
,如果存在就把childVal
添加到parentVal
後返回新數組;否則返回childVal
的數組。所以回到mergeOptions
函數,一旦parent
和child
都定義了相同的鉤子函數,那麼它們會把兩個鉤子函數合併成一個數組。 -
通過執行
mergeField
函數,把合併後的結果保存到options
對象中,最終返回它。因此,在我們當前這個case
下,執行完如下合併後:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
vm.$options
的值差不多是如下這樣:
vm.$options = {
components: { },
created: [
function created() {
console.log('parent created')
}
],
directives: { },
filters: { },
_base: function Vue(options) {
// ...
},
el: "#app",
render: function (h) {
//...
}
}
- 組件場景,由於組件的構造函數是通過
Vue.extend
繼承自Vue
的,先回顧一下這個過程,代碼定義在src/core/global-api/extend.js
中:
/**
* Class inheritance
*/
Vue.extend = function (extendOptions: Object): Function {
// ...
Sub.options = mergeOptions(
Super.options,
extendOptions
)
// ...
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// ...
return Sub
}
我們只保留關鍵邏輯,這裏的
extendOptions
對應的就是前面定義的組件對象,它會和Vue.options
合併到Sub.opitons
中。
- 接下來子組件的初始化過程,代碼定義在
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
}
// ...
return new vnode.componentOptions.Ctor(options)
}
- 這裏的
vnode.componentOptions.Ctor
就是指向Vue.extend
的返回值Sub
, 所以 執行new vnode.componentOptions.Ctor(options)
接着執行this._init(options)
,因爲options._isComponent
爲 true,那麼合併options
的過程走到了initInternalComponent(vm, options)
邏輯。先來看一下它的代碼實現,在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
方法首先執行const opts = vm.$options = Object.create(vm.constructor.options)
,這裏的vm.constructor
就是子組件的構造函數Sub
,相當於vm.$options = Object.create(Sub.options)
。 -
接着又把實例化子組件傳入的子組件父
VNode
實例parentVnode
、子組件的父Vue
實例parent
保存到vm.$options
中,另外還保留了parentVnode
配置中的如propsData
等其它的屬性。這麼看來,initInternalComponent
只是做了簡單一層對象賦值,並不涉及到遞歸、合併策略等複雜邏輯。因此,在我們當前這個case
下,執行完如下合併後:
initInternalComponent(vm, options)
vm.$options
的值差不多是如下這樣:
vm.$options = {
parent: Vue /*父Vue實例*/,
propsData: undefined,
_componentTag: undefined,
_parentVnode: VNode /*父VNode實例*/,
_renderChildren:undefined,
__proto__: {
components: { },
directives: { },
filters: { },
_base: function Vue(options) {
//...
},
_Ctor: {},
created: [
function created() {
console.log('parent created')
}, function created() {
console.log('child created')
}
],
mounted: [
function mounted() {
console.log('child mounted')
}
],
data() {
return {
msg: 'Hello Vue'
}
},
template: '<div>{{msg}}</div>'
}
}
- 總結:
Vue
初始化階段對於options
的合併過程就介紹完了,我們需要知道對於options
的合併有兩種方式,子組件初始化過程通過initInternalComponent
方式要比外部初始化 Vue 通過mergeOptions
的過程要快,合併完的結果保留在vm.$options
中。縱觀一些庫、框架的設計幾乎都是類似的,自身定義了一些默認配置,同時又可以在初始化階段傳入一些定義配置,然後去merge
默認配置,來達到定製化不同需求的目的。只不過在Vue
的場景下,會對merge
的過程做一些精細化控制,雖然我們在開發自己的JSSDK
的時候並沒有Vue
這麼複雜,但這個設計思想是值得我們借鑑的。