Vue源码解析05-组件化
前言
组件(Component)是Vue.js框架强大的功能之一,我们想要编写一个完整、健壮的项目离不开Vue的组件,所以本渣从源码角度梳理了一下Vue组件的创建流程,在此做简单记录
个人认为Vue的组件分三步
- 组件的创建(我们平常会使用Vue.component创建一个全局组件)
- 组件的解析,也就是将组件转化为对应的VNode(平常我们说的虚拟DOM)
- 组件的挂载(VNode向真实DOM转化并显示在界面上的过程)
组件的创建
官方文档指出,我们可以使用**Vue.component(id:String,definition:Object|function)**注册或获取一个全局组件,它接收两个参数:组件名称(id)和组件配置项
那么,先来看一下Vue.component都做了什么事情
- 为了能够梳理出一个清晰的脉络和阅读代码的方便,我们只粘贴关键代码
Vue.Component的实现
Vue.component是在**initGlobalAPI()**方法中进行初始化的,具体查找过程因为不是重点,所以在此不做过多赘述.
//这里的type,指的是component、directive、filter,也就是说,这三者的处理方法大同小异
Vue[type] = function (
id: string,//组件id
definition: Function | Object//配置对象
): Function | Object | void {
if (!definition) {
//如果第二个参数不存在,系统认为是获取一个全局组件,则返回组件的构造方法
return this.options[type + 's'][id]
} else {
......
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
//重点:这里使用extend方法将definition转化为VueComponent构造函数
definition = this.options._base.extend(definition)
}
......
//将组件的构造函数加入到options中相应的component、directive、filter对象中去
this.options[type + 's'][id] = definition
return definition
}
}
- 总结一下,Vue.component的主要作用就是执行了Vue.extend方法,生成了一个组件的构造函数VueComponent(),并将该构造方法挂载到Vue.components属性上
下面我们来看一下Vue.extend方法的实现
Vue.extend方法的实现
Vue.extend = function (extendOptions: Object): Function {
const Super = this//这里的this指的是Vue
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const name = extendOptions.name || Super.options.name
//创建一个VueComponent的构造函数
//VueComponent继承自Vue
const Sub = function VueComponent(options) {
this._init(options)
}
//继承于Vue,Super指的是Vue构造函数
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
/*
* 这里进行了一些extend、mixin、use方法的继承
和其他的一些方法的初始化、选项合并等
*/
// 将构造函数缓存起来
cachedCtors[SuperId] = Sub
//返回VueComponent
return Sub
}
通过对上面代码的分析我们能大概清楚Vue.extend的作用:
- 创建一个VueComponent构造函数并返回
- VueComponent继承自Vue,同时继承了Vue中的extend、mixin、use等方法和一些其他的选项
那么我们最终可以得出结论:Vue组件的创建过程其实就是通过用户配置的一些组件信息,创建了一个继承自Vue的组件构造函数VueComponent,并将该构造函数挂载到Vue.components上
组件的解析
我们都知道Vue从创建到显示在界面上大致的理解为:带模板语法的HTML->VNode(虚拟DOM)->挂载真实DOM(展示在界面上)
组件的解析就是将组件转换为VNode的过程
组件解析的源头:_render
读过Vue源码的小伙伴肯定对_render函数不陌生,这个函数主要的作用就是生成VNode,因为生成VNode的过程比较复杂,所以我们这里只挑选跟Component组件有关的代码进行分析
组件的VNode的创建:从_createElement到createComponent
我们先做个简单的认知:_createElement是创建VNode节点的方法,其中就包含了创建HTML原声节点(div、p、span等标签)和组件Component
我们看一下_createElement的简化代码:
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
// 先判断是否是保留标签(div、p等 )
if (config.isReservedTag(tag)) {
// 直接创建一个虚拟DOM
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else
//如果没有pre命令
if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
//Ctor:组件的构造函数
vnode = createComponent(Ctor, data, context, children, tag)
} else {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// 如果tag直接是一个组件的选项或者构造函数
vnode = createComponent(tag, data, context, children)
}
我们只取出了_createElement中于Component密切相关的核心代码,可以看出,我们是他调用的是createComponent()进行VNode的生成,下面我们再看一下createComponent的逻辑:
这里我们删除了对构造函数的校验等非核心操作
export function createComponent(
Ctor: Class<Component> | Function | Object | void,//组件的构造函数
data: ?VNodeData,//组件data选项
context: Component,//上下文
children: ?Array<VNode>,//children
tag?: string//组件name
): VNode | Array<VNode> | void {
//关于异步组件的处理
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
return createAsyncPlaceholder(asyncFactory,data,context,children,tag)
}
}
data = data || {}
//解析构造函数中的options选项,主要作用是更新了Ctor.superOptions选项,
//主要是防止父组件的options更新后与当前组件的superOptions不一致
resolveConstructorOptions(Ctor)
if (isDef(data.model)) {
//处理组件的v-model事件
transformModel(Ctor.options, data)
}
//获取Ctor中的props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// functional component
if (isTrue(Ctor.options.functional)) {
//函数组件的创建
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
const listeners = data.on
data.on = data.nativeOn
//为data选项添加组件的钩子函数,init(),prepatch(),insert(),destroy()
installComponentHooks(data)
//获取组件的名称或者标签名
const name = Ctor.options.name || tag
//创建虚拟节点,自定义组件的VNode
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
//返回组件的vnode
return vnode
}
从上述代码我们可以看出,crateComponent()的主要作用就是将组件的构造函数解析为VNode并返回
总结:组件的解析发生在Vue._render函数调用的时候,_render函数的主要作用是生成VNode,当我们的组件被解析到的时候会将组件的构造方法从Vue.components中取出,然后通过createComponent方法生成VNode并返回
同时,createComponent()方法对组件的v-model、functional、异步组件进行了处理,同时为组件的VNode添加了**init()、prepatch()、insert()、destroy()**等钩子函数
组件的挂载
最终我们的组件是需要真正挂载到界面上展示出来的.所以这就涉及到了Vue的挂载过程了.而我们的组件也是在此过程中挂载到界面上的.
为了把我们的问题简单化,这里只分析初次挂载的过程,真正更新的时候的挂载过程都是大同小异的
挂载的入口:mountComponent()
这里我们按照代码实际运行的顺序去分析一下DOM挂载的入口(精简中的精简版):
export function mountComponent(
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
//调用声明周期beforeMount
callHook(vm, 'beforeMount')
// 定义组件更新函数,函数的内容就是调用Vue实例的_update方法
updateComponent = () => {
//核心代码
vm._update(vm._render(), hydrating)
}
// 创建Watcher实例
new Watcher(vm, updateComponent, noop, {
before() {
if (vm._isMounted && !vm._isDestroyed) {
//beforeCreate钩子
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
if (vm.$vnode == null) {
//挂载完成的标记
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
分析上面的最终简化版的mountComponent()代码,真正执行挂载操作的其实是vm._update(vm._render())方法,它接收一个我们之前提到的_render()函数生成的VNode然后执行挂载操作
挂载的重点:patch()函数
这里特此说明一下:我们跳过了中间繁琐的非重点环节(因为查找函数调用顺序,在源码中很容易找到,不是介绍重点),直指挂载的核心函数patch()方法
- patch函数的重点就是对比新老节点然后进行更新操作,如果是初次挂载,那么它会直接执行创建新节点的操作
- 最终patch()函数,会返回一个最后生成的最新的真实DOM:vnode.elm,这个真实dom会被挂载到vue实例的$el上保存起来
createElm(
vnode,//VNode
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
// 在父组件中追加转换后的真实dom
nodeOps.nextSibling(oldElm)
)
createElm创建真实DOM
patch()函数中调用了createElm来生成真实的DOM,那么我们来看一下createElm的代码
function createElm(
vnode,//传入的vnode
insertedVnodeQueue,
parentElm,//父组件的真实DOM
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested // for transition enter check
//尝试创建组件实例
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)
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
//插入到父节点中,这个时候挂载之前的原真实DOM还没有删除
insert(parentElm, vnode.elm, refElm)
}
}
从上面的代码分析得知,createElm的主要作用就是调用各种方法将生成的真实DOM插入到父节点中,其中将组件DOM插入到父节点的方法便是createComponent
createComponent:创建组件DOM的真正执行者
createComponent的方法其实很简单,因为他的主要作用就是,调用了一个组件的init()钩子函数(这个钩子函数包含了组件的初始化、创建、挂载的生命周期),
然后,将自身的一个真实dom插入到父节点中
//生成组件的真实DOM
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
//普通节点是没有data的
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
//如果存在init钩子函数,则执行
//执行实例的创建和挂载,这个时候子组件还没有显示出来,因为父组件还没有挂载
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
//插入到父元素中
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
上面代码中的i.hook(vnode,false),其实就是执行我们之前提到过的生成组件VNode的时候在VNode上的data中挂载的一个init()钩子函数,我们来看一下他的代码
//组件的实例化及挂载
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参数生效,如果有keepAlive生效的话,则只更新当前组件的一些涉及到parent或者插槽相关的东西,而不去patch组件本身
- 初次调用init()的话,则执行组件实例的$mount()方法,其实这相当与在vue的组件树中的一个递归调用,组件这个时候就会进行初始化、创建、挂载的流程
- 执行到挂载的时候会将自身的一个真实DOM挂载到父节点上,一直向上递推这个过程,直到根节点挂载到界面中,我们的界面就能够完整的显示出来了
总结
Vue组件最终经过了创建、编译解析和挂载操作,最终展示在了界面上.
简单可以概括成以下步骤:
- 用户利用Vue.component生成一个继承自Vue类的VueComponent的构造函数,并将构造函数存储在Vue.components中
- 界面渲染过程中,在render生成VNode的过程中,会调用Vue.components中相应的组件的构造函数生成对应的VNode
- 挂载阶段,程序会递归调用组件的初始化、创建、挂载过程.从而实现了组件树的挂载展示
**PS:**拜读大神的源码真的收益匪浅,深刻认识到自己还是一个渣渣