Appearance
十一、组件化
组件化回顾
组件化是Vue中非常重要的一个概念,Vue的核心组成只有数据绑定和组件化,Vue官方文档中关于Vue的组件很简单,一个Vue组件就是一个拥有预定义选项的Vue实例,所以一个组件就是一个Vue实例.
一个组件可以组成页面上一个功能完备的区域,组件可以包含脚本、样式、模板.我们可以把一个页面抽象成若干个组件,也可以把这些组件组合成一个页面.
组件化可以让我们方便的把页面拆分成多个可重用的组件,使用组件可以让我们重用某一个区域,另外组件之间是可以嵌套的.
组件注册
vue中注册组件的两种方式
- 全局组件,任何位置都能使用
- 局部组件,只能在当前注册的范围中使用
html
<div id="app"></div>
<script>
// 注册全局组件comp
const comp = Vue.component('comp', {
template: '<div>I am a comp</div>'
})
const vm = new Vue({
el: '#app',
// 通过h函数创建组件对应的vnode
render (h) {
return h(comp)
}
})
</script>
下面看一下Vue.component的源码实现,其函数定义在core/global-api/index.js中
js
// 这个函数注册了 Vue.directive()\Vue.component()\Vue.filter()
initAssetRegisters(Vue)
这个函数在同级的assets.js中定义
js
/* @flow */
import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'
//参数是Vue构造函数
export function initAssetRegisters (Vue: GlobalAPI) {
/**
* 没有直接定义而是遍历数组,ASSET_TYPES里面其实就是'component','directive','filter',这个是直接注册Vue.component,Vue.directive,Vue.filter
*/
ASSET_TYPES.forEach(type => {
// 这三个函数都接收两个参数,处理相似所以一起定义
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
// 没有传定义的话会找到之前options中定义的方法直接返回
// id就是组件名称 or 指令名称
if (!definition) {
return this.options[type + 's'][id]
} else {
// 验证名称是否合法,不合法的话就直接报警告
if (process.env.NODE_ENV !== 'production' && type === 'component') {
validateComponentName(id)
}
// <————————这里重点重点重点看:————————————>
/**
* 判断类型是否是组件,且定义是否是原始的Object,判断其转换成字符串是不是'[object Object]'
*/
if (type === 'component' && isPlainObject(definition)) {
// 如果设置了组件名称name就用name,如果没有就用id作为组件名称
definition.name = definition.name || id
// this.options._base就是Vue构造函数
// Vue.extend()就是把普通对象转换成了VueComponent构造函数
// 官方文档中也可以直接传一个Vue.extend构造函数,如果第二个参数definition是Vue.extend的构造函数,那么就直接执行return的最后一句话
definition = this.options._base.extend(definition)
}
//<———————————————————————————————>
// 如果是指令
...
// 所有的内容处理之后够会直接挂载到this.options下面去,通过这个注册的是全局的组件\指令等
this.options[type + 's'][id] = definition
return definition
}
}
})
}
下面看一下Vue.extend方法.这个方法把该组件的选项对象转换成Vue构造函数的子类,也就是对应组件的构造函数.
Vue.extend
内部基于传入的选项对象,创建了组件的构造函数,组件的构造函数继承自Vue构造函数,所以组件对象拥有和实例一样的成员
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.
* 定义唯一cid的目的,是保证创建一个包裹的子构造函数,通过原型继承,并且能够缓存他们
*/
Vue.cid = 0
let cid = 1
/**
* Class inheritance
* 参数是组件的选项,对象
*/
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
// Super是this是Vue构造函数,或者是组件的构造函数
const Super = this
const SuperId = Super.cid
// 从缓存中加载组件的构造函数,如果有就直接返回,没有就初始化成一个空对象{}
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
// 获取组件名称,开发环境验证组件名称是否合法
// 在Vue.component中已经验证过一次,但是Vue.extend在外部可以直接使用,所以这里再验证一次
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
// 如果是开发环境验证组件的名称
validateComponentName(name)
}
// ----------核心代码-------------
// 创建一个构造函数 VueComponent ,组件对应的构造函数
const Sub = function VueComponent (options) {
// 调用_init()初始化
this._init(options)
}
// 改变了构造函数的原型,让其继承自Vue,故所有的组件都继承自Vue
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
//设置cid,后面缓存的时候用
Sub.cid = cid++
// 合并 options 选项
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.
// 把Super中的成员拷贝到VueComponent构造函数中来
// 初始化子组件的props,computed
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.
// 注册Vue.component\Vue.filter\Vue.directive方法
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
// 在选项的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
// 最后返回组件的构造函数VueComponent
return Sub
}
}
调试组件注册过程
组件的创建过程
- 在core/vdom/create-element.js里面,createElement函数,里面调用了
_createElement
函数,在函数中,判断tag是不是string,不是说明是一个组件,这个时候调用了createComponent函数,并返回了其对应的vnode对象,下面看一下这个函数的内部
js
export function createComponent (
// 组件类,构造函数,函数,对象
Ctor: Class<Component> | Function | Object | void,
// 创建vnode需要的数据
data: ?VNodeData,
// 创建上下文,Vue实例或者当前组件实例
context: Component,
// 子节点数组
children: ?Array<VNode>,
// 标签名称
tag?: string
// 返回值是创建好的VNode对象
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
// 通过实例的选项获取Vue构造函数
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
// 如果 Ctor 是选项对象的话
// 就调用extend,把选项对象转换成组件的构造函数
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
...
// async component
// 处理异步组件,如果Ctor上没有cid,就是异步组件
// 组件的构造函数中设置了cid,这里跳过
let asyncFactory
if (isUndef(Ctor.cid)) {
...
}
data = data || {}
// 当组件构造函数创建完毕后,合并当前组件选项和通过Vue.mixins混入的选项
resolveConstructorOptions(Ctor)
// transform component v-model data into props & events
// 处理组件上的v-model指令
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
...
// 创建组件vnode的核心位置
// 安装组件的钩子函数 默认钩子函数init/prepatch/insert/destory
installComponentHooks(data)
// return a placeholder vnode
// 获取组件的名称
const name = Ctor.options.name || tag
// 核心核心核心
// 创建组件对应的VNode对象
// { Ctor, propsData, listeners, tag, children },componentOptions有这些属性
// 注意:Ctor在init钩子函数内部通过new VNode.componentOptions.Ctor创建了组件的对象
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
...
// 最后返回vnode对象
return vnode
}
- 这里看一下installComponentHooks函数,这个函数是创建组件vnode的核心
js
function installComponentHooks (data: VNodeData) {
//获取data.hook,用户传入的组件钩子函数
const hooks = data.hook || (data.hook = {})
// 这里遍历hooksToMerge中的名字,init/prepatch/insert/destory
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
}
}
}
- 这里面的
hooksToMerge
是啥?
js
const hooksToMerge = Object.keys(componentVNodeHooks)
- 找到这个发现是这个componentVNodeHooks对象的key的集合,那componentVNodeHooks是什么?里面有四个钩子函数,而init钩子函数中,通过createComponentInstanceForVnode函数创建组件的实例
js
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (...) {
...
} else {
// 创建组件的实例
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
...
},
insert (vnode: MountedComponentVNode) {
...
},
destroy (vnode: MountedComponentVNode) {
...
}
}
- 那createComponentInstanceForVnode这个方法内部是这样返回的
js
export function createComponentInstanceForVnode (
...
): Component {
...
// 组件的构造函数,传入了options
return new vnode.componentOptions.Ctor(options)
}
下面跳回2看一下mergeHook如何将两个钩子函数合并?
js
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
}
由此组件的创建过程就完毕了,createComponent这个函数中初始化了四个钩子函数,在init钩子函数中创建了组件对象.init钩子函数在什么时候调用的?在patch中调用.
组件的patch过程
没看懂,可以再看一遍
core/vdom/patch.js中定义了patch函数,在patch函数内部最终会调用createElm把vnode转换成真实DOM然后挂载到DOM树,那createElm中是如何处理组件中的vnode的呢?
createElm会调用createComponent来处理组件中的vnode
先创建父组件再创建子组件,先挂载子组件再挂载父组件
组件不是拆分的越多越好,因为嵌套一个组件,就会重复执行一遍组件的创建过程,比较消耗性能.组件的抽象过程要合理.
之后的看不懂了,没有记
mountComponent之后的渲染过程
这里注意的点是,在createComponent的时候,创建父组件,父组件中遇到自定义组件,继续创子组件,子组件的内容返回,更新父组件的内容,层层向上