本文为原创文章,未获授权禁止转载,侵权必究!
本篇是 Vue 源码解析系列第 4 篇,关注专栏
前言
组件化是 Vue.js 的核心思想。组件化即把页面拆分成多个组件 (component),每个组件依赖的 CSS、JavaScript、模板等资源放在一起开发和维护。组件是资源独立的,组件在系统内部可复用,组件和组件之间可以嵌套。
在 数据驱动
章节我们讲到 createElement
时,最终会调用 _createElement
方法,该方法定义在 src/core/vdom/create-element.js
,其中一段逻辑对 tag
判断,如果 tag
为普通的 html
标签,即 tag
为 div
(案例一),则会实例化一个普通的 vnode
节点,否则通过 createComponent
方法创建一个组件 vnode
(案例二)
// 案例一
<div id="app"> {{ message }}</div>
const app = new Vue({
el: '#app',
data() {
return {
message: 'Hello Vue'
}
}
})
// 案例二
<div id="app"></div>
const childComp = {
name: 'ChildComp',
template: '<div>{{ msg }}</div>',
data() {
return {
msg: 'Hello Vue'
}
}
}
const app = new Vue({
el: '#app',
// 这里的 h 是 createElement 方法
render: h => h(childComp)
})
// tag 为 "div"
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 {
// tag 为 组件对象
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
组件创建
上文分析到组件的创建实际是调用 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
方法主要有三个核心步骤:
- 构造子类构造函数
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
这里的 baseCtor
实际就是 Vue
, context.$options._base
定义在 Vue 初始化阶段。在 src/core/global-api/index.js
文件里 initGlobalAPI
函数中有这么一段逻辑:
Vue.options._base = Vue
这里定义的是 Vue.options
,但 createComponent
取的是 context.$options
。实际上在 Vue 初始化时,即 _init
方法,它被定义在 src/core/instance/init.js
,存在如下逻辑:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
mergeOptions
方法主要把 Vue 构造函数的 options
和用户传入的 options
合并到 vm.$options
上,这样我们就能通过 vm.$options._base
拿到 Vue 这个构造函数了。
之后我们在看下这段逻辑 baseCtor.extend(Ctor)
,实际是执行 Vue.extend
方法,该方法定义在 src/core/global-api/extend.js
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this // vm
const SuperId = Super.cid // vm.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)
}
// 实例化 Sub
const Sub = function VueComponent (options) {
// 执行 Vue 实例的初始化逻辑
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
// options 扩展
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.
// 初始化 props
if (Sub.options.props) {
initProps(Sub)
}
// 初始化 computed
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
的时候对同一个子组件重复构造。(可查看相应的注释)
- 安装组件钩子函数
// install component management hooks onto the placeholder node
installComponentHooks(data)
installComponentHooks
方法定义在 src/core/vdom/create-component.js
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
需要注意的是,组件 vnode
和普通元素节点的 vnode
有所不同,组件的 vnode
没有 children
。
组件更新
组件 VNode
创建完毕,之后会执行 vm._update
方法,该方法实际是执行 vm.__patch__
把 VNode
转换成真正 DOM 节点。patch
的过程会调用 createEle
方法创建元素节点,它定义在 src/core/vdom/patch.js
中。patch
的流程:createComponent
--> 子组件初始化 --> 子组件 render
--> 子组件 patch
。另外需要注意的是,嵌套组件的插入顺序为 先子后父
。
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// ...
}