Vue.js 源码学习 - 组件化(上)
Vue.js 的另一个核心思想是组件化。所谓组件化,就是把页面拆分成多个组件,每个组件依赖的 CSS、JavaScript、模板、图片等资源可以放在一起开发和维护。组件是资源独立的,组件在系统内部可复用,组件与组件之间可嵌套。
本文就从源码的角度来分析 Vue 的组件内部是如何工作的。由于本文涉及内容比较多,所以拆分成上下两部分来写。
通过下面的代码来分析下 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 标签,则会实例化一个普通的 VNode,否则通过 createComponent 方法创建一个组件 VNode。_createElement 方法在 src/core/vdom/create-element.js 文件中:
export function _createElement(
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// ...
if (typeof tag === 'string') {
// ...
} 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)
}
// ...
// 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
)
// ...
return vnode
}
由于 createComponent 的逻辑有些复杂,所以我就把一些暂时用不到的省略了,只剩下三个核心步骤:构造子类构造函数、安装组件钩子函数和实例化 VNode。下面就分别介绍这三个关键步骤。
1.1、构造子类构造函数
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
通常编写组件的时候,是创建一个普通对象,所以会执行上面的代码,baseCtor.extend(Ctor) 这里的 baseCtor 其实就是 Vue,这个的定义是在最开始初始化 Vue 的阶段,在 src/core/global-api/index.js 中的 initGlobalAPI 函数有这么一段逻辑:
Vue.options._base = Vue
在知道了 baseCtor 的指向后,来分析一下 Vue.extend 函数的定义。在 src/core/global-api/extend.js 文件中:
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)
}
1.2、安装组件钩子函数
installComponentHooks(data)
Vue.js 使用的 Virtual DOM 有一个特点,就是在 VNode 的 patch 过程中对外暴露了各种时机的钩子函数,方便做一些额外的事情,Vue.js 利用这一点,在初始化一个 Component 类型的 VNode 的过程中实现了几个钩子函数:
const componentVNodeHooks = {
init(vnode: VNodeWithData, hydrating: boolean): ?boolean {
// ...
},
prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
// ...
},
insert(vnode: MountedComponentVNode) {
// ...
},
destroy(vnode: MountedComponentVNode) {
// ...
},
}
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 函数做合并,就是在最终执行的时候,依次执行这两个钩子函数即可。
1.3、实例化 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
)
最后一步就是通过 new VNode 实例化一个 vnode 并返回。需要注意的是,组件的 vnode 是没有 children 的,和普通元素节点的 vnode 不同。
createComponent 后返回的是组件 vnode,它也一样走到 vm._update 方法,进而执行了 patch 函数,下面就来对 patch 函数做进一步的分析。
二、patch
当我们通过 createComponent 创建了组件 VNode,接下来会走到 vm._update,执行 vm.__patch__ 去把 VNode 转换成真正的 DOM 节点。之前文章中分析过一个普通的 VNode 节点,下面看看组件的 VNode 会有哪些不一样的地方。
patch 的过程会调用 createElm 创建元素节点,看下它的实现,定义在 src/core/vdom/patch.js 文件中:
function createElm(
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// ...
}
上面会判断 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
}
}
}
从上面代码可以看出,首先对 vnode.data 做了一些判断:如果 vnode 是一个组件 VNode,那么条件会满足,并且得到 i 就是 init 钩子函数,可以回顾第一章中创建组件 VNode 的时候合并钩子函数就包含 init 钩子函数。下面看下 init 钩子函数的实现,定义在 src/core/vdom/create-component.js 文件中:
function 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)
}
}
这里暂时不考虑 keepAlive 的情况,上面的代码会执行 else 的逻辑,它是通过 createComponentInstanceForVnode 创建一个 Vue 的实例,然后调用 $mount 方法挂载子组件。先来看一下 createComponentInstanceForVnode 的实现:
export function createComponentInstanceForVnode(
// we know it's MountedComponentVNode but flow doesn't
vnode: any,
// activeInstance in lifecycle state
parent: any
): 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)。
回到组件 init 的过程,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 做了哪些事:
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 restoreActiveInstance = setActiveInstance(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()
// 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.
}
上面的代码中有几个关键的地方,首先是 vm._vnode = vnode 的逻辑,这个 vnode 是通过 vm._render() 返回的组件渲染 VNode,vm._vnode 和 vm.$vnode 的关系就是一种父子关系,用代码表达就是 vm._vnode.parent === vm.$vnode;还有一个就是 activeInstance 的作用就是保持当前上下文的 Vue 实例,它是在 lifecycle 模块的全局变量。
因为实际上 JavaScript 是一个单线程,Vue 整个初始化是一个深度遍历的过程,在实例化子组件的过程中,它需要知道当前上下文的 Vue 实例是什么,并把它作为子组件的父 Vue 实例。
在 vm._update 的过程中,把当前的 vm 赋值给 activeInstance,同时通过 const preActiveInstance = activeInstance 用 preActiveInstance 保留上一次的 activeInstance。实际上,preActiveInstance 和当前的 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,这里我们只传了 2 个参数,所以对应的 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,则重复本节开始的过程,这样通过一个递归的方式就可以完整地构建了整个组件树。
在完成组件的整个 patch 过程后,最后执行 insert(parentElm, vnode.elm, refElm) 完成组件的 DOM 插入,如果组件 patch 过程中又创建了子组件,那么 DOM 的插入顺序是先子后父。
三、合并配置
new Vue 的过程通常有 2 种场景,一种是外部我们的代码主动调用 new Vue(options) 的方式实例化一个 Vue 对象;另一种是组件创建过程中内部通过 new Vue(optioins) 实例化子组件。
这两种场景都会执行 _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')
},
})
var vm = new Vue({
el: '#app',
render: (h) => h(childComp),
})
3.1、外部调用场景
执行 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)
// ...
}
上面遍历 ASSET_TYPES 后的代码相当于:
Vue.options.components = {}
Vue.options.directives = {}
Vue.options.filters = {}
接着通过 extend(Vue.options.components, builtInComponents) 把一些内置组件扩展到 Vue.options.components 上,比如 <keep-alive>、<transition> 和 <transition-group>。
然后回到 mergeOptions 这个函数,它的定义在 src/core/util/options.js 文件中:
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)
// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, 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。
因此,执行完合并之后,vm.$options 的值差不多是如下这样的:
vm.$options = {
components: {},
created: [
function created() {
console.log('parent created')
},
],
directives: {},
filters: {},
_base: function Vue(options) {
// ...
},
el: '#app',
render: function (h) {
//...
},
}
3.2、组件场景
由于组件的构造函数是通过 Vue.extend 继承自 Vue 的,所以先看下下面的代码:
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.options 中。
组件的初始化过程中,会执行下面的代码:
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 的过程中,会执行 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 = Sub.options。
接着又把实例化子组件传入的子组件父 VNode 实例 parentVnode、子组件的父 Vue 实例 parent 保存到 vm.$options 中,另外还保留了 parentVnode 配置中的如 propsData 等其它的属性。
initInternalComponent 只是做了简单一层对象赋值,并不涉及到递归、合并策略等复杂逻辑。
因此,执行完合并之后,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>',
},
}
四、总结
到这里,组件化的创建、patch、配置合并已经分析完了,下篇将从生命周期、组件注册、异步组件三个方面分析。