Vue 2.x 源码阅读记录(二):组件化
写在前面
文章为阅读笔记向,需 clone 下来 Vue 源码并加以调试服用~~
一个最基本的组件例子,下面围绕这个例子,对源码进行分析:
// 创建一个名为 App 的组件
const App = Vue.component('App', {
name: 'App',
template: `<h1>My App</h1>`,
props: {},
data() {
return {};
},
methods: {},
});
new Vue({
el: '#app',
render: (h) => h(App)
});
createComponent
在渲染的部分,我们分析了 _createElement
方法的具体实现,其中有一段代码是分析了tag
,如果传入的是 html 标签则生成标签的 vnode 对象,否则调用 createComponent
方法创建一个组件的 vnode:
// 开始创建 vnode
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
)
}
// tag 如果是一个内置节点(div, span 等),直接创建一个 vNode 实例
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
// tag 如果是一个已注册的组件名,则创建一个组件类型的 vnode
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
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
// tag 直接传递了一个组件,创建组件类型的 vnode
vnode = createComponent(tag, data, context, children)
}
createComponent
定义在src\core\vdom\create-component.js
,它在我们例子中主要做的三件事是:构造子类构造函数、安装组件函数钩子、实例化 vNode。先大致看看它的流程:
// vnode = createComponent(tag, data, context, children)
// 创建组件 vNode
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// debugger
if (isUndef(Ctor)) {
return
}
/*
context = 当前 Vue 实例
Vue.option 在 src\core\global-api\index.js 定义并将 Vue 的构造函数赋值给 _base:Vue.options._base = Vue
在初始化 _init 时有一个合并 option 的操作
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
baseCtor 就是当前上下文的构造函数
*/
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
// 如果 Ctor 是一个普通对象,则调用 Vue.extend 返回的
// 在例子中,使用的是 Vue.component() 创建组件,返回的就是 extend 返回的一个组件构造函数
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
// 向组件上挂载 init、prepatch、insert、destroy 钩子函数,后面介绍
installComponentHooks(data)
// return a placeholder vnode
const name = Ctor.options.name || tag
// 创建组件的 vnode
// 组件名由 Vue 根据组件的 cid 和 name/tag 定义
// 将 props、事件、tag、children 等属性存到 VNode 的 context 对象下
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)
}
// 最后返回组件的 vnode
return vnode
}
构造子类构造函数
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
在我们 import Vue from 'vue'
的时候,会执行一个 initGlobalAPI
方法,它定义在src\core\global-api\index.js
,主要做的是将一些静态的属性和方法挂载到 Vue 构造函数上:
export function initGlobalAPI (Vue: GlobalAPI) {
// config
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
Object.defineProperty(Vue, 'config', configDef)
// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}
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)
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
}
在这里面,看到了 _base
的定义,它指向的是 Vue 的构造函数:
Vue.options._base = Vue
而在createComponent
中,如果传入的ctor
是一个对象,则调用baseCtor
的extend
方法,这个方法返回一个继承了 Vue 构造函数的子类,通常来说,就是一个组件的构造函数。它在上面的initExtend
方法中被定义:
/**
* Class inheritance
* Vue.extend 静态方法,返回一个继承了 Vue 实例的组件构造函数
* 在 initGlobalAPI 的运行中被 initExtend 定义
*/
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)
}
// 定义一个子类继承 Vue 构造函数
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
}
}
安装组件钩子函数
// install component management hooks onto the placeholder node
installComponentHooks(data)
它的过程就是把 componentVNodeHooks
的钩子函数合并到 data.hook
中 ,这些 hook 在后面 patch 中会在不同阶段调用:
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)) {
// 在合并过程中,如果某个时机的钩子已经存在 data.hook 中,那么通过执行 mergeHook 函数做合并
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
}
实例化 vNode
// return a placeholder vnode
const name = Ctor.options.name || tag
// 创建组件的 vnode
// 组件名由 Vue 根据组件的 cid 和 name/tag 定义
// 将 props、事件、tag、children 等属性存到 VNode 的 context 对象下
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
在createComponent
返回组件的 vnode 后,一样会走到_update
方法,执行patch
函数。
patch
通过前面我们知道,_update
中createElm
方法用于 patch vnode,将 vnode 生成真实 DOM 并插入。这里大致记录一下createElm
流程:
vnode
为普通元素:创建一个对应的tag
的元素,调用createChildren
循环调用createElm
完成 DOM 的渲染:
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
// ...
createChildren(vnode, children, insertedVnodeQueue)
vnode
为组件时,调用组件的init
钩子函数进行组件的创建与插入 DOM 过程,并终止:
// createElm
// 如果 createComponent 返回 true,则说明已经完成组件 DOM 的 patch
// 随后中止 createElm 后面的 DOM 逻辑
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
这里分步骤来看createComponent
内部执行的流程:
1、调用init
钩子函数,相当于组件流程的入口:
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
// 是一个组件 vnode
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
// 在 src\core\vdom\create-component.js 中的 createComponent 挂载了组件钩子函数
// 满足条件则 i 就变成了 init 钩子函数
i(vnode, false /* hydrating */)
}
}
// ...
}
2、init
函数内部调用了createComponentInstanceForVnode
,拿到组件实例对象并执行组件的挂载方法:
// 组件实例
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
// activeInstance 在 _update 的调用中被赋值,是当前实例的父级构造函数
activeInstance
)
// 调用组件的 $mount 方法
// 由于组件是不用传入 el 的,所以是 $mount(undefined, false)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
3、createComponentInstanceForVnode
执行组件的_init
,_init
中执行initLifecycle
将当前组件实例插入到父实例的$children
中。接着执行了initInternalComponent
,合并了一些组件的 options。最后返回组件的实例:
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
}
// 调用 Vue 的子类构造函数,返回组件实例
// 也就是组件构造函数调用了 this._init(options)
// 组件也会走一遍 new Vue(options) 的初始化过程
return new vnode.componentOptions.Ctor(options)
}
4、init
函数调用了组件实例的$mount(vnode.elm)
,由于组件的 vnode 只是一个占位符,所以elm
是 undefined
。执行了$mount
中拿到用户传入的template
生成render
方法,调用mountComponent
。调用 mountComponent
,就相当于进入了普通元素的_update(_render())
流程。由于elm
是undefined
,所以在这个 patch 过程中不会被插入到 DOM 中,但是生成的 DOM 元素会被赋值给占位 vnode 的componentInstance
属性下的$el
属性。:
// 开始调用 $mount
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
5、createComponent
中的i
执行完成之后,执行initComponent
方法,将vnode.componentInstance.$el
赋值给vnode.elm
:
// createComponent
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
// 在完成组件的整个 patch 过程后,最后执行 insert(parentElm, vnode.elm, refElm) 完成组件的 DOM 插入
// init 钩子里会不断地去递归子组件,所以 DOM 会按先子后父的顺序插入
// 如果组件 patch 过程中又创建了子组件,那么DOM 的插入顺序是先子后父。
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
// initComponent
vnode.elm = vnode.componentInstance.$el
6、上面执行insert
了方法,将vnode.elm
插入 DOM 树,删除旧 vnode,返回组件vnode.elm
赋值给vm.$el
。至此,createComponent
完成了对组件的 patch,然后将主要流程还给createElm
,此时createComponent
返回true
,则当前createElm
流程结束。这是 Vue 一种深度遍历的手段:
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
// 如果 createComponent 始终返回 true,表示是当前还是一个组件,继续进入组件流程
return
}
7、最后,再由patch
函数删除 DOM 中的旧节点,完成整个 patch 过程:
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
}
PS:记录一些在整个 patch 过程中的重要遍历以及定位:
// 在 initLifecycle 中定义,initLifecycle 把当前实例 vm 插入到其父实例中 parent(如果父实例存在)
$parent, $root, $children
// 在 mountComponent,第一次 patch 时为 #app 根元素
vm.$el = el
// createComponent 中将挂载组件钩子函数,定义组件占位符 vnode(tag = vue-component-cid..)
// _render() 返回的 vnode 对象
vm._vnode
// 在 createComponentInstanceForVnode 中定义,表示父 vnode 和当前组件 vnode
options.parent, _parentVnode
/*
小结:在 patch 的 createElm 中的 createComponent 中只要传入的 vnode 属于组件 vnode,就会返回 true,从而结束 createElm(patch) 的执行,createElm 的 vnode 一旦为组件,则组件从头开始执行 _init(),当组件 patch(insert) 完之后,才会继续执行父级的 patch ,从而形成先子后父的插入顺序。
*/
合并配置
普通实例合并
一个基本的实例配置例子,合并之后会将所有配置存储到实例的$options
属性上:
let app = new Vue({
// mixins: [mixin],
el: '#app',
data: {
msg: 'Hello'
},
created() {}
})
/*
app.$options: {
components: {}
created: [ƒ]
data: ƒ mergedInstanceDataFn()
directives: {}
el: "#app"
filters: {}
render: ƒ anonymous( )
staticRenderFns: []
_base: ƒ Vue(options)
}
*/
在initGlobalAPI
的调用中,首先定义了一些默认的配置,包括components
、directives
、filters
,随后将 Vue 的一些内置组件keep-alive
、transition
、transitionGroup
合并到components
配置中,这就是为什么 Vue 可以不用注册就使用这些组件:
{/* 默认的 options 定义 */}
Vue.options = Object.create(null)
{/* 添加 components, directives, filters 属性 */}
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
{/* 向 components 里赋值 builtInComponents 中的属性 */}
{/* 这里实际上就是将 Vue 的内置组件率先合并到默认的 components 中 */}
extend(Vue.options.components, builtInComponents)
在创建实例时执行_init
方法,对这个例子,它调用mergeOptions
合并默认和传入的配置:
// 合并 new Vue 的配置参数
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor), // 默认 options
options || {}, // 用户传入的 options
vm
)
在例子中,普通实例的resolveConstructorOptions
直接返回默认的options
:
export function resolveConstructorOptions (Ctor: Class<Component>) {
let options = Ctor.options
if (Ctor.super) {
//...
}
// 当前是 Vue 根构造函数时直接返回默认的 options
return options
}
mergeOptions
将默认生成的配置与传入的配置合并,然后返回赋值给$options
属性。它遍历默认和传入的配置,在内部为每个配置的项都设置了独立的合并策略函数,调用它们进行合并,使用options
存储并返回:
const options = {}
let key
for (key in parent) {
// console.log('parentKey :>> ', key);
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
// child 不存在 parent 中
mergeField(key)
}
}
// console.log('strats :>> ', strats);
function mergeField (key) {
const strat = strats[key] || defaultStrat
// strats 将所有选项都定义一个自己的合并策略函数
// 合并策略函数将 parent 和 child 合并后的属性赋值给 options[key]
options[key] = strat(parent[key], child[key], vm, key)
}
return options
这里看一下生命周期函数的合并策略,生命周期函数最终会被合并为一个数组,便于 mixin 等属性的生命周期函数顺序执行:
/**
* Hooks and props are merged as arrays.
*/
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
const res = childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
return res
? dedupeHooks(res)
: res
}
function dedupeHooks (hooks) {
const res = []
for (let i = 0; i < hooks.length; i++) {
if (res.indexOf(hooks[i]) === -1) {
res.push(hooks[i])
}
}
return res
}
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
带mixins
的场景
接下来向配置中添加一个mixin
,看看是如何合并的:
const mixin = Vue.mixin({
created() {
console.log('mixin created')
}
})
let app = new Vue({
mixins: [mixin],
el: '#app',
data: {
msg: 'Hello'
},
created() {}
})
/*
app.$options: {
components: {}
created: (2) [ƒ, ƒ]
data: ƒ mergedInstanceDataFn()
directives: {}
el: "#app"
filters: {}
mixins: [ƒ]
render: ƒ anonymous( )
staticRenderFns: []
_base: ƒ Vue(options)
}
*/
在mergeOptions
中,如果传入的配置中带有mixins
则将其遍历,递归调用mergeOptions
将各个mixin
的配置合并到parent
默认配置中,最后再将传入的其它配置合并:
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
需要注意的是,在initGlobalAPI
调用的initMixin
中,定义了Vue.mixin
静态方法。在调用它时会将当前mixin
与Vue
的默认options
合并,再到后面实例化时将这个合并后的mixin
与实例的options
合并:
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
}
组件合并配置
再将例子改造成组件的使用场景:
const childComponent = {
template: '<div>{{msg}}</div>',
data() {
return {
msg: 'Hello Vue'
};
},
created() {
console.log('component created');
},
mounted() {
console.log('component mounted');
},
};
const mixin = Vue.mixin({
created() {
console.log('mixin created');
}
})
let app = new Vue({
mixins: [mixin],
el: '#app',
data: {
msg: 'Hello'
},
created() {},
render: (h) => h(childComponent)
})
在渲染组件环节,会调用createComponentInstanceForVnode
来生成组件特有的options
并调用组件构造函数。在initGlobalAPI
生成子组件构造函数的initExtend
方法中,将父构造函数的 options 与当前组件传入的 options 合并:
// 将父构造函数的 options 与当前组件传入的 options 合并
Sub.options = mergeOptions(
Super.options,
extendOptions
)
createComponentInstanceForVnode
实例化组件构造函数,执行_init
方法,此时合并配置调用的是initInternalComponent
:
if (options && options._isComponent) {
// 组件构造函数的 _init() 走这里
initInternalComponent(vm, options)
}
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
// vm.constructor.options 拿到之前父构造函数与当前组件传入的配置合并后的配置
// 相当于 vm.$options = Object.create(Sub.options)
// Object.create 创建一个指定原型的对象,也就是将 vm.constructor.options 放到了 opts.__proto__ 属性中
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
// _parentVnode 在 createComponentInstanceForVnode 被赋值
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
}
}
最终,组件合并后将结果保存在$options
, 子组件初始化过程通过 initInternalComponent
方式要比外部初始化 Vue 通过 mergeOptions
的过程要快 ,因为没有递归与合并策略等,最终组件的$options
是这样的:
/*
vm.$options: {
parent: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
propsData: undefined
render: ƒ anonymous( )
staticRenderFns: []
_componentTag: undefined
_parentListeners: undefined
_parentVnode: VNode {tag: "vue-component-1", data: {…}, children: undefined, text: undefined, elm: div, …}
_renderChildren: undefined
__proto__:
components: {}
created: (2) [ƒ, ƒ]
data: ƒ data()
directives: {}
filters: {}
mounted: [ƒ]
template: "<div>{{msg}}</div>"
_Ctor: {0: ƒ}
_base: ƒ Vue(options)
__proto__: Object
}
*/
生命周期
在组件渲染的各个阶段会调用不同的生命周期,它使用一个总线callHook
函数来调用生命周期钩子。它在内部调用了传入的hook
,并带有错误处理:
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
// hook 函数
const handlers = vm.$options[hook]
const info = `${hook} hook`
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
// 执行 hook 函数,内部进行了错误处理
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
beforeCreate & created
在初始化调用_init
方法时,会调用这两个钩子:
// 初始化生命周期,渲染等
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
// 初始化 props, data, methods, watch, computed 等
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
由于beforeCreate
调用在initState
前面,所以beforeCreate
时无法访问实例内的data
、methods
等属性。created
在调用实例初始化完成调用$mount
时还没有调用$mount
,所以created
无法访问refs
、DOM 等元素属性。
beforeMount & mounted
在开始调用mountComponent
方法时,beforeMount
会调用,此时可以访问data
、methods
等属性。在vm._update(vm._render(), hydrating)
将 DOM 挂载完成之后,mounted
会被调用,此时可以访问 DOM:
xport function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// ...
// 执行 beforeMount 生命周期
callHook(vm, 'beforeMount')
let updateComponent
// ...
updateComponent = () => {
vm._update(vm._render(), hydrating /* false */)
}
// ...
new Watcher(vm, updateComponent, noop /* 空函数 */, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
// 组件已挂载并未销毁,执行 beforeUpdate 生命周期
callHook(vm, 'beforeUpdate')
}
}
}, true)
// ...
if (vm.$vnode == null) {
// 渲染完成
vm._isMounted = true
// 执行 mounted 生命周期
callHook(vm, 'mounted')
}
// ...
}
vm.$vnode = _parentVnode
在_render
方法中定义,如果vm.$vnode
为null
,表示这不是一次组件的 mount,而是new Vue()
的 mount。那么再看看组件的 mount。首先了解一下,在patch
方法中,定义了一个insertedVnodeQueue = []
队列,当vnode
是组件时,在组件插入 DOM 之前的initComponent
方法中,会将组件vnode
push 到队列中:
// patch
const insertedVnodeQueue = []
// initComponent
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
// empty component root.
// skip all element-related modules except for ref (#3455)
registerRef(vnode)
// make sure to invoke the insert hook
// 组件 vnode push 到队列中
insertedVnodeQueue.push(vnode)
}
}
由于在组件渲染过程中执行_render
的时候,$vnode
已经赋值了_parentVnode
。所以组件的init
钩子最后调用child.$mount
不会执行mounted
钩子。在patch
方法最后,组件渲染完成之后,会调用 invokeInsertHook
方法,它调用了队列中所有组件的insert
钩子函数:
function invokeInsertHook (vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// element is really inserted
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
// 执行组件的 insert 钩子
queue[i].data.hook.insert(queue[i])
}
}
}
insert
钩子内调用了组件的mounted
生命周期,insertedVnodeQueue
的添加顺序是先子后父,所以对于同步渲染的子组件而言,mounted
钩子函数的执行顺序也是先子后父 :
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
// 组件的 mounted 调用
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 */)
}
}
},
beforeUpdate & updated
beforeUpdate
的执行时机是在mountComponent
渲染 Watcher 的before
函数中,它在组件mounted
之后才会执行:
// 在挂载时添加一个`渲染 watcher`
// 用于元素的第一次渲染和后续的更新
new Watcher(vm, updateComponent, noop /* 空函数 */, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
// 组件已挂载并未销毁,执行 beforeUpdate 生命周期
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
updated
后续更新。。。
beforeDestroy & destroyed
组件销毁的流程暂时还没有了解,但最后都会调用$destroy
全局方法来销毁组件。beforeDestroy
在$destroy
开始调用时执行。经过一系列销毁操作之后执行destroyed
,包括从父实例的$children
删除自身、卸载watcher
、移除 DOM 等:
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// remove self from parent
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
// 从父的 $children 树中删除自身
remove(parent.$children, vm)
}
// teardown watchers
if (vm._watcher) {
// 卸载 watcher
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// call the last hook...
vm._isDestroyed = true
// invoke destroy hooks on current rendered tree
// 递归销毁子组件
vm.__patch__(vm._vnode, null)
// fire destroyed hook
callHook(vm, 'destroyed')
// turn off all instance listeners.
vm.$off()
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
}
在销毁过程中会递归销毁该组件的子组件,所以destroyed
也是先子后父顺序执行。
组件注册
全局注册
先看一个全局注册组件的例子:
const MyComp = Vue.component('MyComp', {
template: `
<div class="my-comp">
<span>{{msg}}</span>
</div>
`,
data() {
return {
msg: 'Hello MyComp'
};
},
});
new Vue({
el: '#app',
render: (h) => h(MyComp)
});
在执行initGlobalAPI
时,最后执行了initAssetRegisters
方法,这个方法为Vue
构造函数添加了component
、directive
、filter
三个全局方法,其中component
就是注册一个全局组件:
// 定义了 Vue.component, Vue.directive, Vue.filter 三个静态方法
// 这三个常量值也成为组件内部的选项
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && type === 'component') {
// 检查组件名规范
validateComponentName(id)
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
// 调用实例上的 exntend 方法创建组件
// 相当于 Vue.extend(definition /* options */)
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
// 向 options 的 components 选项中存入该组件
this.options[type + 's'][id] = definition
return definition
}
}
})
当调用Vue.component
时,实际上是调用了this.options._base.extend
,相当于Vue.extend
,返回一个组件的构造函数:
definition = this.options._base.extend(definition)
此时MyComp
是一个组件的构造函数。后来执行到这个组件的initInternalComponent
时,会将这个组件的options
合并到vm.$options
的原型上:
var opts = vm.$options = Object.create(vm.constructor.options);
要注意的是,组件在初始化构造函数的时候合并了根Vue
构造函数的options
:
Sub.options = mergeOptions(
Super.options,
extendOptions
)
合并完之后,该组件自身components
中也会有该组件自身的构造函数供自身调用,并且按照组件的合并策略,会将Vue
构造函数的components
作为一个原型对象合并到组件的components.__proto__
下:
function mergeAssets (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): Object {
const res = Object.create(parentVal || null)
if (childVal) {
process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
return extend(res, childVal)
} else {
return res
}
}
此时组件的components.__proto__
选项下包含Vue.components
下的所有组件,在该组件下就可以使用transition
以及其它全局的组件。
改造一下例子:
const MyComp = Vue.component('MyComp', {
template: `
<div class="my-comp">MyComp</div>
`,
data() {
return {
msg: 'Hello MyComp'
};
},
});
const App = Vue.component('App', {
template: `
<div class="my-comp">
<MyComp />
</div>
`,
data() {
return {
msg: 'Hello App'
};
},
});
new Vue({
el: '#app',
render: (h) => h(App)
});
当App
组件在执行的_createElement
时,它的tag
是MyComp
,因此会下面这个逻辑。调用resolveAsset
返回组件构造函数给Ctor
在进行组件 vnode 的创建:
if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
// tag 如果是一个已注册的组件名,则创建一个组件类型的 vnode
vnode = createComponent(Ctor, data, context, children, tag)
}
resolveAsset
通过一系列条件与原型链查找(由于前面提到过,组件构造函数在初始化时会合并父实例的options
,并且在initInternalComponent
中组件构造函数的options
定义在__proto__
上),将需要的组件构造函数找出:
export function resolveAsset (
options: Object,
type: string,
id: string,
warnMissing?: boolean
): any {
/* istanbul ignore if */
if (typeof id !== 'string') {
return
}
const assets = options[type]
// check local registration variations first
// 从 components 里取出组件构造函数返回
if (hasOwn(assets, id)) return assets[id]
const camelizedId = camelize(id)
// 驼峰写法的组件名
if (hasOwn(assets, camelizedId)) return assets[camelizedId]
const PascalCaseId = capitalize(camelizedId)
// 首字母大写的组件名
if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
// fallback to prototype chain
const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
warn(
'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
options
)
}
return res
}
局部注册
例子:
const localComp = Vue.extend({
template: '<h1>Local component</h1>'
});
new Vue({
el: '#app',
components: {
localComp
},
});
组件注册由于直接在选项中编写,会直接进入mergeOptions
的组件合并策略环节。将传入的components
直接合并到vm.$options.components
, 这样就可以在resolveAsset
的时候拿到这个组件的构造函数,并作为createComponent
的钩子的参数 :
function mergeAssets (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): Object {
const res = Object.create(parentVal || null)
if (childVal) {
process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
return extend(res, childVal)
} else {
return res
}
}
二者区别:
Vue.component
全局创建的组件直接向Vue
构造函数的options.components
选项中添加,子组件创建时会合并Vue
的options
并将使用原型链串联起来,所以在子组件内都可以使用全局注册的组件。
局部创建的组件只会保存在当前$options.component
中,其它组件不会合并当前的$options
,所以只能在当前组件内访问注册的组件。
理解组件注册最重要的地方就是组件 options 的合并、组件合并策略、initInternalComponent
三个地方。
异步组件
异步组件在需要使用(render)到的时候才会去创建一个组件,是一种优化手段。看一个基本例子,AsyncComp
只有在render
时才会去创建组件实例:
const AsyncComp = Vue.component('AsyncComp', (resolve, reject) => {
// require(MyComp, resolve);
setTimeout(() => {
resolve({
template: '<h1>My component</h1>'
});
}, 1000);
});
new Vue({
render: (h) => h(AsyncComp)
}).$mount('#app');
异步组件第二个参数接收一个工厂函数,在完成异步操作后将组件 options 传给resolve
函数。在Vue.component
方法的处理中,不会将工厂函数变成组件的构造函数,仅仅是保存到Vue.options
中:
// 调用 Vue.component
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
// 调用实例上的 exntend 方法创建组件
// 相当于 Vue.extend(definition /* options */)
definition = this.options._base.extend(definition)
}
// 向 options 的 components 选项中存入工厂函数
this.options[type + 's'][id] = definition
return definition
在不对组件进行 render 的情况下,组件上没有options
、cid
以及其它方法,只是一个函数体。因为在上述代码中并没有执行到extend
方法。当组件被渲染的时候,还是会走到createElement
>createComponent
逻辑,对应例子,createComponent
会执行这一块逻辑:
// 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
)
}
}
resolveAsyncComponent
用来解析异步组件,并处理了工厂函数、promise、高级异步组件三种异步组件写法。
工厂函数
写法就是上面的例子。在工厂函数流程中定义了resolve
和reject
两个方法,它们使用once
方法做了一层包装,来确保对同一个组件只执行一次:
/**
* Ensure a function is called only once.
*/
export function once (fn: Function): Function {
let called = false
return function () {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
}
const resolve = once((res: Object | Class<Component>) => {
// cache resolved
// ensureCtor 实际是一个闭包,它返回组件构造函数
// 这里相当于调用了组件的构造函数,也就是 vm._init
// _init 方法没有返回值,所以 factory.resolved 是一个 undefined,表示组件构造函数已被调用
factory.resolved = ensureCtor(res, baseCtor)
factory.resolved = ensureCtor(res, baseCtor)
// invoke callbacks only if this is not a synchronous resolve
// (async resolves are shimmed as synchronous during SSR)
if (!sync) {
forceRender(true)
} else {
owners.length = 0
}
})
const reject = once(reason => {
process.env.NODE_ENV !== 'production' && warn(
`Failed to resolve async component: ${String(factory)}` +
(reason ? `\nReason: ${reason}` : '')
)
if (isDef(factory.errorComp)) {
factory.error = true
forceRender(true)
}
})
resolve
方法使用ensureCtor
方法将传入的组件转为构造函数,在ensureCtor
中实际将组件options
调用Vue.extend
来生成组件构造函数执行extend
逻辑,这也就是按需引入组件的本质:
function ensureCtor (comp: any, base) {
if (
comp.__esModule ||
(hasSymbol && comp[Symbol.toStringTag] === 'Module')
) {
comp = comp.default
}
return isObject(comp)
? base.extend(comp)
: comp
}
在异步组件流程中,resolve
接下来执行forceRender
方法,将当前渲染的每个实例都执行一次$forceUpdate
方法,$forceUpdate
调用了实例上的渲染 watcher 的update
方法触发组件重新渲染。 之所以这么做是因为 Vue 通常是数据驱动视图重新渲染,但是在整个异步组件加载过程中是没有数据发生变化的,所以通过执行 $forceUpdate
可以强制组件重新渲染一次:
const forceRender = (renderCompleted: boolean) => {
for (let i = 0, l = owners.length; i < l; i++) {
(owners[i]: any).$forceUpdate()
}
if (renderCompleted) {
owners.length = 0
if (timerLoading !== null) {
clearTimeout(timerLoading)
timerLoading = null
}
if (timerTimeout !== null) {
clearTimeout(timerTimeout)
timerTimeout = null
}
}
}
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}
最后调用工厂函数,返回到createComponent
中返回一个异步组件占位符 vnode:
const res = factory(resolve, reject)
// createElement
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
)
}
promise 异步组件
写法:
Vue.component(
'AsyncComp',
() => import('SomeComp')
);
解析 promise 异步组件执行() => import()
时,res
返回一个 Promise 对象。最后将resolve
和reject
作为res.then
方法参数调用这两个方法,就完成了 promise 异步组件写法:
const res = factory(resolve, reject) // Promise
if (isObject(res)) {
if (isPromise(res)) {
// () => Promise
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
}
// 。。。
}
高级异步组件(较少使用,详情看源码)
异步组件和普通组件区别
普通组件在定义时以及执行了组件的构造函数,完成了组件的合并配置等一些初始化的工作;异步组件在没有进入 render 流程时则没有进行组件初始化,待到 render 的时候才会执行extend
生成组件的构造函数及其初始化。代码逻辑层面,就是何时执行extend
的区别。