Vue 2.6 源码剖析-组件化
组件化回顾
Vue 的核心组成只有 数据绑定 和 组件化。
- 一个 Vue 组件就是一个拥有预定义选项的一个 Vue 实例。
- 一个组件可以组成页面上一个功能完毕的区域,组件可以包含脚本、样式、模板。
- 组件化可以方便的把页面拆分成多个可重用的组件。
- 使用组件可以重用页面中的某个区域。
- 组件是可以嵌套的,搭建页面就像搭积木。
组件注册方式
- 全局组件 Vue.component
- 在全局可以使用
- 局部组件
- 在当前注册的范围中使用
全局组件的注册过程
Vue.component 注册全局组件
Vue.component 是静态方法,是在 src\core\global-api\index.js
中 的 initAssetRegisters 函数中注册的。
// src\core\global-api\index.js
// 记录 Vue 构造函数到 _base(留意,后面会用到)
Vue.options._base = Vue
// ...
// 注册 Vue.directive()、Vue.component()、Vue.filter()
initAssetRegisters(Vue)
initAssetRegisters
initAssetRegisters 接收 Vue 的构造函数作为参数。
内部遍历ASSET_TYPES([directive, component, filter]),定义对应的三个Vue的静态方法。
它们的实现都是类似的。
- 如果没有第二个参数,则获取全局的内容
- 如果有第二个参数,分别作响应的处理,最终记录到全局,并返回。
通过源码得知,Vue.component 主要在全局记录了组件的构造函数并返回。
如果它的第二个参数是对象(组件配置选项)不是函数(构造函数),就会调用 Vue.extend 将配置转化为组件的构造函数。
// src\core\global-api\assets.js
export function initAssetRegisters (Vue: GlobalAPI) {
/**
* Create asset registration methods.
*/
// 遍历 ASSET_TYPES 数组,为 Vue 定义相应方法
// ASSET_TYPES: [directive, component, 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') {
// 如果是开发环境,验证id(名称)是否合法
// 不合法会报警告
validateComponentName(id)
}
// isPlainObject 通过 toString() 判断是否是 原始Object对象
// 组件处理
// 如果type是组件,并且第二个参数是对象
// Vue.component('comp', {template: ''})
if (type === 'component' && isPlainObject(definition)) {
// 先获取配置中的name作为名称,没有则取id
definition.name = definition.name || id
// Vue.options._base 存储的就是 Vue 构造函数
// 这里调用 Vue.extend 把组件配置definition 转换为 组件的构造函数
definition = this.options._base.extend(definition)
}
// 指令处理
if (type === 'directive' && typeof definition === 'function') {
// 如果配置是函数,就包装一下
definition = { bind: definition, update: definition }
}
// 将配置记录到对应全局中
// 如果是过滤器,不做处理,直接记录并返回
// 如果组件第二个参数是函数(组件构造函数),不做处理,直接记录并返回
this.options[type + 's'][id] = definition
return definition
}
}
})
}
Vue.extend
在Vue.component(id, options) 中,如果 options 是对象(组件配置选项),不是函数(构造函数),则调用 Vue.extend 把该组件的选项对象,转化成 Vue 构造函数的子类,也就是对应组件的构造函数。
所以组件其实也是一个Vue实例。
开发自定义组件的过程中,可能会用到 Vue.extend 方法。
它是 Vue 的静态方法,在src\core\global-api\extend.js
中定义。
- 它内部就是基于传入的组件对象,创建组件的构造函数。
- 组件的构造函数继承自Vue构造函数
- 所以组件对象拥有和Vue实例一样的成员
// src\core\global-api\extend.js
export function initExtend (Vue: GlobalAPI) {
/**
* Each instance constructor, including Vue, has a unique
* cid. This enables us to create wrapped "child
* constructors" for prototypal inheritance and cache them.
* 每个实例构造函数(包括Vue)都有一个唯一的cid。
* 这使我们能够通过原型继承,创建一个包裹的“子构造函数”,并缓存它们
*/
Vue.cid = 0
let cid = 1
/**
* Class inheritance
*/
Vue.extend = function (extendOptions: Object): Function {
// extendOptions 是组件的选项对象
// 之后会被合并到构造函数的选项对象中(this.options)
extendOptions = extendOptions || {}
// 获取构造函数
// this 是 Vue 构造函数
// 或者是 继承的子构造函数,也就是组件的构造函数
// 因为 组件的构造函数 也拥有 extend 方法
const Super = this
const SuperId = Super.cid
// 先判断是否可以从缓存中获取
// 判断是否有 _Ctor 属性,如果没有初始化一个空对象
// _Ctor 存储缓存的构造函数
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) {
// 内部调用 _init() 初始化
// 下面改造Sub的原型,使其继承自Vue,所以它也可以调用_init方法
this._init(options)
}
// 改造构造函数的原型,使其继承自 Vue
// 所以所有的 Vue 组件都继承自 Vue
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
// 设置cid,后面缓存的时候要用
Sub.cid = cid++
// 合并Super的选项 和 传入的选项,作为当前构造函数的选项
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
// 下面就是将 Vue 的成员拷贝到 Sub 组件中
// 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 computed
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// allow further extension/mixin/plugin usage
// 然后继承Super的静态方法
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
// 把组件构造函数(自己)保存到 [Vue/Sub].options.components 对象
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
// 把组件的构造函数缓存到 options._Ctor
cachedCtors[SuperId] = Sub
// 返回构造函数
return Sub
}
}
调试全局组件注册过程
<div id="app"></div>
<script src="../../dist/vue.js"></script>
<script>
const Comp = Vue.component('comp', {
template: '<div>I am a comp</div>'
})
const vm = new Vue({
el: "#app",
render(h) {
// Vue CLI的使用方式
return h(Comp)
}
});
</script>
断点位置:
-
src\core\global-api\assets.js
中定义 Vue.component 的位置
组件的创建过程
回顾首次渲染过程
- Vue 构造函数
- this._init()
- this.$mount()
- mountComponent()
- new Watcher() 渲染 Watcher
- updateComponent()
- vm._render() -> createElement()
- vm._update()
现在看 createElement 中创建组件的过程。
createElement
src\core\vdom\create-element.js
中定义了createElement。
它内部最终调用了 _createElement。
_createElement 中判断当前为组件时,调用 createComponent 创建组件对应的 VNode 对象。
// src\core\vdom\create-element.js
/**
* @param {*} context // Vue 实例 或 当前组件实例
* @param {*} tag 标签的名称(string) 或 组件(组件构造函数[Class | Function] | 组件选项对象)
* @param {*} data 创建VNode时需要的数据
* @param {*} children 子节点数组
* @param {*} normalizationType 如何处理子节点数组
* @return 最终创建并返回 VNode 对象
*/
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// ...
if (typeof tag === 'string') {
// 如果tag是字符串
// ...
} else {
// 如果tag不是字符串,那它应该是一个组件
// createComponent创建组件对应的Vnode
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
//...
}
createComponent
createComponent 最终把组件转化成了 vnode 对象。
内部还初始化了4个钩子函数。
在 init 钩子函数中,创建了 组件实例。
init 钩子函数是在 patch 的过程中调用的。
// src\core\vdom\create-component.js
/**
* @param {*} Ctor 组件构造函数 或 选项对象
* @param {*} data 创建 VNode 需要的数据
* @param {*} context 上下文:Vue实例或当前组件实例
* @param {*} children 子节点数组
* @param {*} tag 标签名称
* @return 创建好的 Vnode 对象
*/
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// 验证Ctor是否有效
if (isUndef(Ctor)) {
return
}
// 首先获取 Vue 构造函数
// _init 中会把Vue 构造函数中的选项合并到 Vue 实例的选项中
// 所以这里可以通过 实例的选项,获取_base,即Vue构造函数
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
// 如果Ctor时对象,则是选项对象
if (isObject(Ctor)) {
// 调用 Vue.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
// 如果 Ctor 中没有cid,那它就是异步组件(暂不关心)
// 通过extend创建的构造函数肯定有cid
if (isUndef(Ctor.cid)) {
// ...
}
data = data || {}
// 当组件构造函数创建完毕后
// 合并当前组件选项和通过Vue.mixin混入的选项
resolveConstructorOptions(Ctor)
// transform component v-model data into props & events
// 处理组件上的 v-model 指令
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// extract props
// 获取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
if (isTrue(Ctor.options.abstract)) {
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
// 安装组件的钩子函数
// 组件中默认的钩子函数有4个:init/prepatch/insert/destroy
// 组件对象真正创建实在init钩子函数中
installComponentHooks(data)
// return a placeholder vnode
// 获取组件的名称
const name = Ctor.options.name || tag
// 创建组件对应的 VNode 对象(这是createComponent函数的核心)
// 组件的名称是以`vue-component-`为前缀,然后拼接上组件的cid,如果有name属性再拼接name
// 然后传入 Vnode 的data
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
// 这是vnode 的 componentOptions 对象
// 组件的 init 钩子函数接收 vnode 作为参数
// 内部调用 createComponentInstanceForVnode
// 通过 new vnode.componentOptions.Ctor(options) 创建了组件的实例
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
if (__WEEX__ && isRecyclableComponent(vnode)) {
return renderRecyclableComponentTemplate(vnode)
}
// 最后返回 vnode 对象
return vnode
}
installComponentHooks
// src\core\vdom\create-component.js
const hooksToMerge = Object.keys(componentVNodeHooks)
//...
function installComponentHooks (data: VNodeData) {
// 获取 data.hook
// data.hook 是用户传入的组件钩子函数
const hooks = data.hook || (data.hook = {})
// 遍历hooksToMerge
for (let i = 0; i < hooksToMerge.length; i++) {
const key = hooksToMerge[i]
// 获取用户传入的钩子函数
const existing = hooks[key]
// 获取 componentVNodeHooks 中定义的钩子函数
const toMerge = componentVNodeHooks[key]
if (existing !== toMerge && !(existing && existing._merged)) {
// 通过 mergeHook 把两个钩子函数合并到一起
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
}
}
}
// 合并两个钩子函数
function mergeHook (f1: any, f2: any): Function {
// 创建一个函数
// 内部先调用内部(componentVNodeHooks)定义的钩子函数
// 然后调用用户传入的钩子函数
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
}
componentVNodeHooks
// src\core\vdom\create-component.js
// componentVNodeHooks 定义了组件默认的4个钩子函数
const componentVNodeHooks = {
// 组件对象真正创建实在init钩子函数中
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// 这是处理keep-alive的情况(暂不关心)
// ...
} else {
// 调用 createComponentInstanceForVnode 创建组件的实例
// 并把创建好的组件实例,存入到 vnode.componentInstance
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
// activeInstance:当前组件的父组件对象
activeInstance
)
// 组件没有el,所以 _init 中不会调用 $mount
// 组件是在这里调用 $mount 的
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {/*...*/},
insert (vnode: MountedComponentVNode) {/*...*/},
destroy (vnode: MountedComponentVNode) {/*...*/}
}
createComponentInstanceForVnode
export function createComponentInstanceForVnode (
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any, // activeInstance in lifecycle state
): Component {
// 创建 options 对象
const options: InternalComponentOptions = {
_isComponent: true, // 标记当前是组件
_parentVnode: vnode, // 当前创建好的 vnode 对象
parent // 当前组件的父组件对象
}
// check inline-template render functions
// 处理 inline-template(暂不关心)
// <comp inlin-template> xxx </comp>
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
// 通过 new 组件构造函数,创建组件实例,传入了options
return new vnode.componentOptions.Ctor(options)
}
组件 patch 的过程
组件在 createElement 中被转化成了 vnode,但它是在 init 钩子函数中最终被实例化。
init 钩子函数是在 patch 中调用的。
src\core\vdom\patch.js
中定义了patch 函数。
patch 内部最终会调用 createElm 把 vnode 转化成真实DOM,挂载到DOM树。
上面已经介绍了组件是如何转化成 vnode 的。
现在查看 createElm 中是如何处理组件的 vnode 的。
createElm 中会调用 createComponent 来处理组件的 vnode。
// src\core\vdom\patch.js
// 调用 createComponent 处理组件的情况
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
createComponent
createComponent 内部获取组件 init 钩子函数,并调用。
init 通过组件构造函数初始化组件实例。
构造函数中会调用 _init 函数进行初始化。
_init 函数就是 Vue 原型的 实例方法,此时父组件已经创建完成,当前是子组件在调用。
所以组件的创建过程是,先创建父组件,再创建子组件。
_init 中会判断选项的 _isComponent,组件已经把它设置为true,接着会调用 initInternalComponent 合并选项。
// src\core\vdom\patch.js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// 获取 vnode.data.hook 也就是钩子函数
// 然后获取 init 钩子函数
if (isDef(i = i.hook) && isDef(i = i.init)) {
// 调用 init 钩子函数
// 传入两个参数:
// vnode 自身
// false
// init 实例化组件,并把实例对象存入 vnode.componentInstance
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.elm
// 触发 vnode 的create 钩子函数 初始化属性/时事件/样式等
// 触发 组件 的 create 钩子函数(内置create和用户定义的create合并后的钩子函数)
initComponent(vnode, insertedVnodeQueue)
// 调用 insert 把组件插入到 父组件中
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
// src\core\instance\init.js
Vue.prototype._init = function (options?: Object) {
// ...
// merge options
// 合并 options
// 将用户传入的 options 和 Vue 构造函数中初始化的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 {
// ...
}
// ...
// 初始化和生命周期相关的一些属性
// $parent/$root/$children/$refs 等
// 还记录了组件之间的父子关系
initLifecycle(vm)
//...
// 最后调用 $mount 方法挂载
// 子组件的选项中没有 el ,所以子组件不会调用 $mount
// 子组件是在 init 钩子函数中 createComponentInstanceForVnode 创建完组件实例后调用$mount的
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
initInternalComponent
// src\core\instance\init.js
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
// 先基于当前vm构造函数的options,创建当前实例的 options
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
// 获取 createComponentInstanceForVnode 存储的 _parentVnode
// _parentVnode 存储的就是当前组件创建的vnode
const parentVnode = options._parentVnode
// 然后把 parentVnode 和 当前组件的父组件对象(parent)记录到选项中
opts.parent = options.parent
opts._parentVnode = parentVnode
// 下面是记录其他选项(暂不关心)
// ...
}
initLifecycle
// src\core\instance\lifecycle.js
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
// 找到当前组件的父组件
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
// 将当前组件添加到父组件的$children中
// 在父组件中记录子组件
parent.$children.push(vm)
}
// 记录parent,至此建立了父子组件的关系
// parent 在 调用 createComponentInstanceForVnode 是作为参数(activeInstance)传入
// activeInstance 在 lifecycleMixin 中定义的 _update 方法中被初始化
vm.$parent = parent
//...
}
export function lifecycleMixin (Vue: Class<Component>) {
// _update 方法的作用是把 VNode 渲染成真实的 DOM
// 首次渲染会调用,数据更新会调用
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
// 获取之前处理的 vnode 对象
const prevVnode = vm._vnode
// setActiveInstance 把当前 vm 实例缓存起来,传入 activeInstance
// 父组件会先调用 _update 方法
// 然后调用 patch,在patch中创建子组件
// 创建子组件时再调用 _update 方法
const restoreActiveInstance = setActiveInstance(vm)
// 更新_vnode为新vnode
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// Vue原型上的__patch__方法是在入口中被注入进来的
// based on the rendering backend used.、
// 核心部分:调用__patch__,把虚拟DOM转换成真实DOM,最终挂载到 $el
if (!prevVnode) {
// 如果不存在处理过的 vnode,说明是首次渲染
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 数据变更渲染
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
// 调用完patch方法后,还原activeInstance
restoreActiveInstance()
// ...
}
}
setActiveInstance
// src\core\instance\lifecycle.js
export function setActiveInstance(vm: Component) {
// 先把当前的 activeInstance 记录到常量中
const prevActiveInstance = activeInstance
// 然后重新赋值
activeInstance = vm
// 返回一个还原 activeInstance 的函数
// 目的是:解决组件嵌套的问题
return () => {
activeInstance = prevActiveInstance
}
}
src\core\vdom\create-component.js
中定义组件init 钩子函数,中调用 createComponentInstanceForVnode 时传入了 activeInstance。
该文件中从 src\core\instance\lifecycle.js
导入了 activeInstance。
总结
整个跳转有点多,这里重点了解:
- 组件的创建过程是先创建父组件,再创建子组件
- 组件的挂载过程是,先挂载子组件,再挂载父组件
可以总结出来,组件的粒度不是越小越好。
因为嵌套一层组件,就会重复执行一遍组件的创建过程,比较消耗性能。
组件的抽象过程要合理。
比如页面侧边栏,如果没有其他地方使用,可以组合成一个组件,不需要拆分成多个组件。
知识点小记
- 全局组件之所以可以在任意组件中使用是因为 Vue 构造函数的选项被合并到了 VueComponent 组件构造函数的选项中
- 局部组件的使用范围被限制在当前组件内是因为,在创建当前组件的过程中传入的局部组件选项,其它位置无法访问
- 在 createElement() 函数中调用 createComponent() 创建的是组件的 VNode。组件对象是在组件的 init 钩子函数中创建的,然后在 patch() --> createElm() --> createComponent() 中挂载组件
上一篇: Vue中的计算属性和监听属性
下一篇: VUE2.0中监听对象属性的方法