Vue.js 另一個核心思想是組件化广鳍。所謂組件化逆趣,就是把頁面拆分成多個組件 (component)蝶溶,每個組件依賴的 CSS、JavaScript宣渗、模板抖所、圖片等資源放在一起開發(fā)和維護(hù)。組件是資源獨(dú)立的痕囱,組件在系統(tǒng)內(nèi)部可復(fù)用田轧,組件和組件之間可以嵌套
我們將從源碼的角度來分析 Vue 的組件內(nèi)部是如何工作的,只有了解了內(nèi)部的工作原理鞍恢,才能讓我們使用它的時候更加得心應(yīng)手傻粘。
繼續(xù)使用vue-cli初始化的代碼來研究和調(diào)試
import Vue from 'vue'
import App from './App.vue'
var app = new Vue({
el: '#app',
// 這里的 h 是 createElement 方法
render: h => h(App)
})
這也是通過 render 函數(shù)去渲染的,通過 createElement 傳的參數(shù)是一個組件而不是一個原生的標(biāo)簽帮掉,那么接下來我們就開始分析這一過程弦悉。
new Vue的時候也像之間分析的一樣通過init函數(shù)的 通過轉(zhuǎn)化成Vnode 然后去轉(zhuǎn)化成patch渲染
createElement 的實(shí)現(xiàn)的時候,它最終會調(diào)用 _createElement 方法蟆炊,其中有一段邏輯是對參數(shù) tag 的判斷稽莉,如果是一個普通的 html 標(biāo)簽,像上一章的例子那樣是一個普通的 div涩搓,則會實(shí)例化一個普通 VNode 節(jié)點(diǎn)污秆,否則通過 createComponent 方法創(chuàng)建一個組件 VNode。
src/core.vdoo/create-element.js
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
context
)
}
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && 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 對象昧甘,它本質(zhì)上是一個 Component 類型良拼,那么它會走到上述代碼的 else 邏輯,直接通過 createComponent 方法來創(chuàng)建 vnode疾层。所以接下來我們來看一下 createComponent 方法的實(shí)現(xiàn)将饺,它定義在 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)
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
}
這個主要就是建立一個繼承于VUE的一個子類贡避,安裝一些鉤子函數(shù)和新建一個組件化的Vnode數(shù)據(jù)
構(gòu)造子類構(gòu)造函數(shù)
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
我們在編寫一個組件的時候痛黎,通常都是創(chuàng)建一個普通對象予弧,還是以我們的 App.vue 為例,代碼如下:
import HelloWorld from './components/HelloWorld'
export default {
name: 'app',
components: {
HelloWorld
}
}
這里 export 的是一個對象湖饱,所以 createComponent 里的代碼邏輯會執(zhí)行到 baseCtor.extend(Ctor)掖蛤,在這里 baseCtor 實(shí)際上就是 Vue,這個的定義是在最開始初始化 Vue 的階段井厌,在 src/core/global-api/index.js 中的 initGlobalAPI 函數(shù)有這么一段邏輯:
Vue.options._base = Vue
這里定義的是 Vue.option蚓庭,而我們的 createComponent 取的是 context.$options,實(shí)際上在 src/core/instance/init.js 里 Vue 原型上的 _init 函數(shù)中有這么一段邏輯:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
這樣就把 Vue 上的一些 option 擴(kuò)展到了 vm.$option 上仅仆,所以我們也就能通過 vm.$options._base 拿到 Vue 這個構(gòu)造函數(shù)了器赞。mergeOptions 的實(shí)現(xiàn)我們會在后續(xù)章節(jié)中具體分析,現(xiàn)在只需要理解它的功能是把 Vue 構(gòu)造函數(shù)的 options 和用戶傳入的 options 做一層合并墓拜,到 vm.$options 上港柜。
在了解了 baseCtor 指向了 Vue 之后,我們來看一下 Vue.extend 函數(shù)的定義咳榜,在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 的作用就是構(gòu)造一個 Vue 的子類,它使用一種非常經(jīng)典的原型繼承的方式把一個純對象轉(zhuǎn)換一個繼承于 Vue 的構(gòu)造器 Sub 并返回涌韩,然后對 Sub 這個對象本身擴(kuò)展了一些屬性畔柔,如擴(kuò)展 options、添加全局 API 等臣樱;并且對配置中的 props 和 computed 做了初始化工作靶擦;最后對于這個 Sub 構(gòu)造函數(shù)做了緩存,避免多次執(zhí)行 Vue.extend 的時候?qū)ν粋€子組件重復(fù)構(gòu)造雇毫。
這樣當(dāng)我們?nèi)?shí)例化 Sub 的時候奢啥,就會執(zhí)行 this._init 邏輯再次走到了 Vue 實(shí)例的初始化邏輯,實(shí)例化子組件的邏輯在之后的章節(jié)會介紹。
const Sub = function VueComponent (options) {
this._init(options)
}
安裝組件鉤子函數(shù)
installComponentHooks(data)
我們之前提到 Vue.js 使用的 Virtual DOM 參考的是開源庫 snabbdom嘴拢,它的一個特點(diǎn)是在 VNode 的 patch 流程中對外暴露了各種時機(jī)的鉤子函數(shù)桩盲,方便我們做一些額外的事情,Vue.js 也是充分利用這一點(diǎn)席吴,在初始化一個 Component 類型的 VNode 的過程中實(shí)現(xiàn)了幾個鉤子函數(shù):
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 的鉤子函數(shù)合并到 data.hook 中赌结,在 VNode 執(zhí)行 patch 的過程中執(zhí)行相關(guān)的鉤子函數(shù),具體的執(zhí)行我們稍后在介紹 patch 過程中會詳細(xì)介紹孝冒。這里要注意的是合并策略柬姚,在合并過程中,如果某個時機(jī)的鉤子已經(jīng)存在 data.hook 中庄涡,那么通過執(zhí)行 mergeHook 函數(shù)做合并量承,這個邏輯很簡單,就是在最終執(zhí)行的時候,依次執(zhí)行這兩個鉤子函數(shù)即可撕捍。
實(shí)例化 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 實(shí)例化一個 vnode 并返回拿穴。需要注意的是和普通元素節(jié)點(diǎn)的 vnode 不同,組件的 vnode 是沒有 children 的忧风,會增加一個包含標(biāo)簽和子節(jié)點(diǎn)等的對象傳入 這點(diǎn)很關(guān)鍵默色,在之后的 patch 過程中我們會再提。
總結(jié)狮腿、
這一節(jié)我們分析了 createComponent 的實(shí)現(xiàn)腿宰,了解到它在渲染一個組件的時候的 3 個關(guān)鍵邏輯:構(gòu)造子類構(gòu)造函數(shù),安裝組件鉤子函數(shù)和實(shí)例化 vnode缘厢。createComponent 后返回的是組件 vnode吃度,它也一樣走到 vm._update 方法,進(jìn)而執(zhí)行了 patch 函數(shù)贴硫,我們在上一章對 patch 函數(shù)做了簡單的分析规肴,那么下一節(jié)我們會對它做進(jìn)一步的分析。