我们以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 传的参数是一个组件而不是一个原生的标签。
上一章我们在分析虚拟DOM的实现
过程是 init –> $mount(挂载) –> render(参生虚拟DOM) –> path (映射真实DOM)
render(参生虚拟DOM) 是调用 createElement 的实现的时候,它最终会调用 _createElement 方法,其中有一段逻辑是对参数 tag 的判断,如果是一个普通的 html 标签,像上一章的例子那样是一个普通的 div,则会实例化一个普通 VNode 节点,否则通过 createComponent 方法创建一个组件 VNode。
在scr/core/vnode/create-elements.js
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 。
在src/core/vdom/create-component.js中:
export function createComponent (
//可以是组件类型的类,函数或者对象
Ctor: Class<Component> | Function | Object | void,
// VNode(虚拟DOM)的相关Data
data: ?VNodeData,
// 上下文,也就是我们的当前的Vue实例---vm
context: Component,
// 组件的子VNode
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
}
参数
Ctor: 可以是组件类型的类,函数或者对象
data: // VNode(虚拟DOM)的相关Data
context: // 上下文,也就是我们的当前的Vue实例—vm
children: // 组件的子VNode
createComponent 方法传递完参数,就是下面的代码
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
}
}
这里我们接收到的组件其实是一个对象。
所以 createComponent 里的代码逻辑会执行到 baseCtor.extend(Ctor。
在这里 baseCtor 实际上就是 Vue,这个的定义是在最开始初始化 Vue 的阶段,在 src/core/global-api/index.js 中的 initGlobalAPI 函数有这么一段逻辑:
Vue.options._base = Vue
在 src/core/instance/index.js中,Vue 原型上的 _init 函数中(我们初始化Vue实例的时候)38行
合并options
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
这样就把 Vue 上的一些 option 扩展到了 vm. option上,所以我们也就能通过vm. o p t i o n 上 , 所 以 我 们 也 就 能 通 过 v m . options._base 拿到 Vue 这个构造函数了。它的功能是把 Vue 构造函数的 options 和用户传入的 options 做一层合并,到 vm. options上。所以因为context是vm,vm. o p t i o n s 上 。 所 以 因 为 c o n t e x t 是 v m , v m . options和Vue.options合并了,所以baseCtor就是Vue
如果传入的Ctor是对象的话,就是用Vue.extend()将Ctor转化为新的构造器
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
Vue.extend 函数的定义,在 src/core/global-api/extend.js 中。
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
**步骤1:**
// vue调用该方法,所以this就是vue
const Super = this
const SuperId = Super.cid
// 给当前的组件添加_Ctor做缓存使用,在Vue.extend这个函数的最后,会将子构造器缓存
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
**步骤2:**
// 对组件的名字做校验,不能是h5的保留标签,如header
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
validateComponentName(name)
}
**步骤3:**
// 创建子构造函数
const Sub = function VueComponent (options) {
//和Vue本身一样,需要_init(继承自Vue)
this._init(options)
}
//子构造函数继承Vue
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
// 子构造函数自己的配置和Vue的配置做合并
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
**步骤4:**
// 对子构造器的props和computed做初始化
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// allow further extension/mixin/plugin usage
// 将Vue的全局静态方法赋值给子构造器
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)
**步骤5:**
// cache constructor
// 将子构造器缓存下来
cachedCtors[SuperId] = Sub
return Sub
}
步骤1:
super就是Vue,
cachedCtors作为当前的组件添加_Ctor属性的属性值做缓存使用,在Vue.extend这个函数的最后,会将子构造器缓存
步骤2:
对组件的名字做校验,不能是h5的保留标签,如header,
利用的是validateComponentName方法,来自于、sc/core/utils/options
步骤3:
创建子构造函数
子构造函数继承Vue
子构造函数自己的配置和Vue的配置做合并
步骤4:
sub 这个对象本身扩展了一些属性,如扩展 options、添加全局 API
对sub 这个对象配置中的 props 和 computed 做了初始化工作
步骤5:
将子构造器缓存下来,并存储给cachedCtors,每一个cachedCtors[SuperId]都对应一个组件,这样当不同的页面引用相同的组件的时候,只用创建一次该组件的子构造函数
总结:
Vue.extend 的作用就是构造一个 Vue 的子类,它使用一种非常经典的原型继承的方式把一个纯对象转换一个继承于 Vue 的构造器 Sub 并返回,然后对 Sub 这个对象本身扩展了一些属性,如扩展 options、添加全局 API 等;并且对配置中的 props 和 computed 做了初始化工作;最后对于这个 Sub 构造函数做了缓存,避免多次执行 Vue.extend 的时候对同一个子组件重复构造。
// install component management hooks onto the placeholder node
installComponentHooks(data)
我们来看installComponentHooks函数,就在这个页面
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
}
}
}
是将我们的 data.hook (组件的data的钩子) 和 componentVNodeHooks 合并。
我们来看componentVNodeHooks是啥,也在该 js
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
}
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
}
insert (vnode: MountedComponentVNode) {
}
destroy (vnode: MountedComponentVNode) {
}
}
componentVNodeHooks中存放的也是钩子,共有四个值init、prepatch、insert、destroy。具体实现后面讲解,从函数名上我们也可以猜到,它们分别是正在VNode对象初始化、patch之前、插入到dom中、VNode对象销毁时调用。
installComponentHooks函数将我们的 data.hook (组件的data的钩子) 和 componentVNodeHooks 合并的时候采用的是mergeHook函数方法
mergeHook:
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
}
其实就是如果 data.hook中有这个钩子,componentVNodeHooks也有这个钩子,那么就按照双方的逻辑各执行一遍。
还在这个js
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 的,并且会有组件的相关配置这个参数。
1.创建子类构造器
2.组件data都会有hook,与installComponentHooks合并
3.生成组件VNode