传统的页面开发主张将内容、样式和行为分开,便于开发和维护。等到React、Vue等MVVM前端框架大行其道时,人们更倾向于使用html、css、js聚合在一块儿建立组件,经过编写小型、独立和一般可复用的组件来构建大型应用。
组件是现代开发框架的基石,下面详细介绍Vue组件的实现原理。
css
在Vue中组件注册分为两种:局部注册、全局注册。全局注册是经过 Vue.component 方法进行的,局部注册是经过在实例化组件时添加 components 选项完成的。
下面详细介绍组件注册以及相关内容。
html
Vue.options 的 components 属性是在 /src/core/global-api/index.js 文件中调用 initGlobalAPI 函数来定义的。
前端
initGlobalAPI(Vue)
// initGlobalAPI 代码
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
Vue.options._base = Vue
extend(Vue.options.components, builtInComponents)
// ASSET_TYPES
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
// builtInComponents
import KeepAlive from './keep-alive'
export default { KeepAlive }
复制代码
在 /src/platforms/web/runtime/index.js 文件中会对 Vue.options.components 进一步赋值。
vue
import platformComponents from './components/index'
extend(Vue.options.components, platformComponents)
// platformComponents
import Transition from './transition'
import TransitionGroup from './transition-group'
export default {
Transition,
TransitionGroup
}
复制代码
最终 Vue.options.components 中会包含三个内置组件:
node
Vue.options.components = {
KeepAlive: {/* ... */},
Transition: {/* ... */},
TransitionGroup: {/* ... */}
}
复制代码
在《选项合并》一文中讲过,资源选项的合并是经过 mergeAssets 函数进行的。合并策略是以父选项对象为原型,所以:
react
// vm 为Vue实例,即Vue组件
vm.$options.components.prototype = Vue.options.components = {
KeepAlive: {/* ... */},
Transition: {/* ... */},
TransitionGroup: {/* ... */}
}
复制代码
Vue.extends 用来根据传入的配置选项建立一个Vue构造函数的“子类”,精简代码以下:
web
Vue.extend = function (extendOptions) {
/* ... */
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
)
/* ... */
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
if (name) {
Sub.options.components[name] = Sub
}
/* ... */
return Sub
}
复制代码
能够看到 Vue.extend 返回的函数 VueComponent 跟Vue构造函数同样,都是调用 _init 方法进行初始化。VueComponent 函数自身也会添加跟Vue相同的静态属性和方法。
VueComponent 与 Vue 的主要区别是静态属性 options 不一样。VueComponent.options 是将 Vue.extend 参数和原有构造函数的 options 参数经过 mergeOptions 函数进行合并而获得的。
另外,会将构造函数添加到自身的 options.components 对象属性上,也就是说经过 VueComponent 实例化的对象上的属性 $options.components.prototype 上除了内置组件还会有自定义组件的构造函数。
api
Vue关于资源的静态方法(Vue.component、Vue.directive、Vue.filter)定义以下:
数组
initAssetRegisters(Vue)
function initAssetRegisters (Vue) {
ASSET_TYPES.forEach(type => {
Vue[type] = function (id,definition ){
if (!definition) {
return this.options[type + 's'][id]
} else {
if (process.env.NODE_ENV !== 'production' && type === 'component') {
validateComponentName(id)
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
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.component 方法其定义以下所示:
前端框架
Vue.component = function (id, definition){
if (!definition) {
return this.options.components[id]
} else {
// 组件名合法性检测
validateComponentName(id)
if (isPlainObject(definition)) {
definition.name = definition.name || id
definition = Vue.extend(definition)
}
Vue.options.components[id] = definition
return definition
}
}
复制代码
由以上代码可知,全局注册的实质是根据全局注册组件选项生成Vue子构造函数,而后将该子构造函数添加到Vue.options.components对象上。
使用 components 选项来注册组件,会将要注册组件信息存储在当前组件实例的 $options.components 对象上。
在局部组件根据渲染函数生成对应VNode时,是由 createComponent 函数来最终生成VNode的。
function createComponent (Ctor,data,context,children,tag) {
/*...*/
var baseCtor = context.$options._base;
// Ctor 为组件配置对象
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}
/*...*/
}
复制代码
由上述代码可知,在生成VNode的过程当中会调用所在组件实例的 extend 方法根据注册信息生成对应的子构造函数。
组件的解析过程和普通标签同样:
一、根据模板生成渲染函数。
二、根据渲染函数生成虚拟DOM。
三、根据虚拟DOM生成真实DOM。
下面以一个简单的例子来讲明组件的解析过程:
<body>
<div id="app"></div>
</body>
<script> var ComponentA = { template: '<div>组件A</div>' } var vue = new Vue({ el: '#app', template: `<div id="app" class="home"><component-a></component-a></div>`, components: { "component-a": ComponentA } }) </script>
复制代码
模板中组件生成的渲染函数比较简单,跟标签同样由 _c() 函数包裹。_c() 的第一个参数为组件名,第二个参数为组件属性对象,第三个参数为使用 <slot> 接收的内容。
function anonymous() {
with(this){
return _c(
'div',
{staticClass:"home",attrs:{"id":"app"}},
[_c('component-a')],
1
)
}
}
复制代码
组件的具体配置参数信息存储在 vm.$options.components 中:
vm.$options.components = {
"component-a" : {
template: "<div>组件A</div>"
}
}
复制代码
组件生成VNode是调用渲染函数中的 _c() 完成的,_c() 最终会调用 _createElement 来生成VNode。_createElement 中关于组件处理的代码以下所示:
// context 为当前组件实例
if ((!data || !data.pre) &&
isDef(Ctor = resolveAsset(context.$options, 'components', tag){
vnode = createComponent(Ctor, data, context, children, tag);
}
复制代码
resolveAsset 对组件类型资源的处理代码以下:
function resolveAsset (options,type,id,warnMissing) {
if (typeof id !== 'string') { return }
var assets = options[type];
if (hasOwn(assets, id)) { return assets[id] }
var camelizedId = camelize(id);
if (hasOwn(assets, camelizedId)) { return assets[camelizedId] }
var PascalCaseId = capitalize(camelizedId);
if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] }
var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];
if (warnMissing && !res) {
warn('Failed to resolve ' + type.slice(0, -1) + ': ' + id,options);
}
return res
}
复制代码
该函数对组件的处理比较有意思,首先是关于组件名称的问题:在模板中使用的组件名称,在组件注册时能够有三种形式。注册时能够跟使用时保持一致,也可使用驼峰命名或者首字母大写的驼峰命名。
其次是关于组件局部注册以及全局注册的问题:局部注册的组件会保存在 vm.$options.components 中,全局注册的组件保存在 Vue.options.components 中,而 Vue.options.components 在 vm.$options.components 的原型链上。
resolveAsset 函数查询组件注册信息会先查注册的局部变量,若是找不到再沿着原型链查询。这就是局部组件只能自身使用,全局注册的组件可以全局使用的缘由。
组件VNode生成函数 createComponent 的精简代码以下所示:
function createComponent (Ctor,data,context,children,tag){
if (isUndef(Ctor)) { return }
const baseCtor = context.$options._base
if (isObject(Ctor)) {Ctor = baseCtor.extend(Ctor)}
/* 省略异步组件相关处理代码 */
data = data || {}
resolveConstructorOptions(Ctor)
/* 省略v-model相关处理代码 */
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
/* 省略函数式组件相关处理代码 */
const listeners = data.on
data.on = data.nativeOn
/* 省略抽象组件相关处理代码 */
installComponentHooks(data)
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相关代码 */
return vnode
}
复制代码
首先是根据局部组件注册信息调用 extend 方法生成子构造函数,而后调用 resolveConstructorOptions 函数来更新子构造函数的 options 属性。这里会有一个疑问:在 extend 方法中已经使用 mergeOptions 方法完成对子构造函数 options 属性合并更新,为何还要调用 resolveConstructorOptions 函数处理 options?
function resolveConstructorOptions (Ctor) {
let options = Ctor.options
if (Ctor.super) {
const superOptions = resolveConstructorOptions(Ctor.super)
const cachedSuperOptions = Ctor.superOptions
if (superOptions !== cachedSuperOptions) {
Ctor.superOptions = superOptions
const modifiedOptions = resolveModifiedOptions(Ctor)
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions)
}
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
}
return options
}
复制代码
这是为了防止在组件构造函数建立之后使用全局 mixins 更改父构造函数的选项,resolveConstructorOptions 函数的做用就是根据原型链上对象的 options 值来更新子构造函数的 options。
接着调用 extractPropsFromVNodeData 函数来从当前实例中提取局部组件 props 的值,调用 installComponentHooks 来在 data 属性上安装组件的钩子函数。
最后使用 new VNode() 来生成组件类型VNode,传入的第一个参数是根据组件名拼接处理的;第三个参数不传,也就是说组件VNode没有 children 属性;与生成其余类型VNode不一样,第七个参数会传入组件选项对象 componentOptions;第八个参数会根据是否为异步组件而传入不一样的值。
组件钩子安装函数 installComponentHooks 以及相关代码以下所示:
const componentVNodeHooks = {
init (vnode, hydrating) {/* 省略具体实现 */},
prepatch (oldVnode, vnode) {/* 省略具体实现 */},
insert (vnode) {/* 省略具体实现 */},
destroy (vnode) {/* 省略具体实现 */}
}
const hooksToMerge = Object.keys(componentVNodeHooks)
function installComponentHooks (data) {
var hooks = data.hook || (data.hook = {});
for (var i = 0; i < hooksToMerge.length; i++) {
var key = hooksToMerge[i];
var existing = hooks[key];
var toMerge = componentVNodeHooks[key];
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge;
}
}
}
function mergeHook (f1, f2) {
const merged = (a, b) => {
f1(a, b)
f2(a, b)
}
merged._merged = true
return merged
}
复制代码
组件钩子函数生成逻辑比较简单:将 data.hook 与 componentVNodeHooks 中的函数加以合并,合并策略为将同名函数合并到同一函数中。
若是本来 data.hook 中没有钩子函数,则最终 data.hook 的值以下所示:
data.hook = componentVNodeHooks = {
init (vnode, hydrating) {/* 省略具体实现 */},
prepatch (oldVnode, vnode) {/* 省略具体实现 */},
insert (vnode) {/* 省略具体实现 */},
destroy (vnode) {/* 省略具体实现 */}
}
复制代码
最终有四个钩子函数:init、prepatch、insert、destroy。钩子函数的具体功能在后面用到时再详细讲解。
构造函数 VNode() 中关于生成组件实例的代码以下所示:
export default class VNode {
constructor (tag,data,children,text,elm,
context,componentOptions,asyncFactory) {
this.tag = tag
this.data = data
this.context = context
this.componentOptions = componentOptions
this.asyncFactory = asyncFactory
/*省略...*/
}
get child (){
return this.componentInstance
}
}
复制代码
例子中的组件VNode最终以下所示:
vnode = {
tag: 'vue-component-1-component-a',
data:{
on: undefined,
hook:{
init (vnode, hydrating) {/* 省略具体实现 */},
prepatch (oldVnode, vnode) {/* 省略具体实现 */},
insert (vnode) {/* 省略具体实现 */},
destroy (vnode) {/* 省略具体实现 */}
}
},
componentOptions:{
Ctor: function VueComponent(options){/*组件构造函数*/}
tag: "component-a"
children: undefined
listeners: undefined
propsData: undefined
},
asyncFactory: undefined,
componentInstance: undefined
/*省略...*/
}
复制代码
在 patch 的过程当中,组件类型VNode生成真实DOM是调用函数 createPatchFunction 中的内部函数 createComponent 来完成的。
函数 createComponent 代码以下所示:
function createComponent(vnode,insertedVnodeQueue,parentElm,refElm){
var i = vnode.data
if (isDef(i)) {
var isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false)
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
复制代码
在不考虑 keepAlive 的状况下,组件类型VNode生成DOM的过程为:
一、调用 data.hook.init 方法生成组件实例 componentInstance 属性,并完成组件的挂载。
二、调用 initComponent 函数使用钩子函数完成组件初始化。
三、调用 insert 方法将生成的DOM插入。
钩子函数 init 函数代码以下所示:
init (vnode, hydrating) {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
const mountedNode = vnode
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child =
vnode.componentInstance =
createComponentInstanceForVnode(vnode,activeInstance)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
}
复制代码
keepAlive 的状况在后续讲解内置组件时阐述。通常状况下会走 else 分支,使用 createComponentInstanceForVnode 函数建立 VNode 的组件实例属性。最后调用组件实例的 $mount 方法挂载实例。
function createComponentInstanceForVnode (vnode,parent) {
var options = {
_isComponent: true,
_parentVnode: vnode,
parent: parent
};
var inlineTemplate = vnode.data.inlineTemplate;
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render;
options.staticRenderFns = inlineTemplate.staticRenderFns;
}
return new vnode.componentOptions.Ctor(options)
}
复制代码
createComponentInstanceForVnode 函数主要做用是调取组件的构造函数生成组件构造实例。
initComponent 函数在组件 tag 存在的状况下,主要做用是调用局部变量 cbs.create 中的各类钩子函数来完成初始化,cbs.create 在Virtual DOM一文中有详细介绍。 以后使用 setScope 设置 style 做用域。
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 {
registerRef(vnode);
insertedVnodeQueue.push(vnode);
}
}
复制代码
函数式组件跟 react 里面的无状态组件很类似,函数式组件无状态 (没有响应式数据),也没有实例 (没有 this 上下文)。由于只是函数,没有实例,因此函数式组件相对于普通组件来讲渲染开销较低。
下面先简单介绍函数式组件的使用,再阐述其源码实现。
函数式组件的使用方式通常有两种:
一、使用 Vue.component 声明组件时,在选项中将 functional 属性置为 true,且手动实现 render 函数。
二、在单文件组件中,使用 <template functional> 代替 <template> 声明模板。
函数式组件的中的 render 函数除了第一个 createElement 参数以外,还添加了第二个参数 context 对象。组件须要的一切都是经过 context 参数传递。
context 对象包含属性以下:
context = {
props:{ /*提供全部 prop 的对象 */ },
children: [ /*VNode 子节点的数组*/ ],
slots: () => {},/*一个返回了包含全部插槽的对象的函数*/
scopedSlots: { /*暴露传入的做用域插槽的对象*/ },
data: { /*不是数据对象,是组件属性,createElement第二个参数*/ },
parent: { /*对父组件的引用*/ },
listeners: { /*事件监听器的对象,data.on 的一个别名*/ },
injections: { /*被 inject 选项注入的属性。*/ },
}
复制代码
下面是一个简单的函数式组件的例子,后续以此为例阐述函数式组件原理。
<body>
<div id="app"></div>
</body>
<script> var ComponentA = { functional: true, render: function(createElement,context) { return createElement('div',context.props.name) } } var vue = new Vue({ el: '#app', template: `<div id="app" class="home"> <component-a name='组件A'></component-a> </div>`, components: { "component-a": ComponentA } }) </script>
复制代码
依旧按照组件的编译顺序来探究其实现原理。
上述示例由模板生成的渲染函数以下所示,能够看到,函数式组件生成渲染函数与普通组件并没有不一样之处。
with(this){
return _c(
'div',
{staticClass:"home",attrs:{"id":"app"}},
[
_c('component-a',{attrs:{"name":"组件A"}})
],
1
)
}
复制代码
在由渲染函数生成VNode的过程当中,会调用生成组件VNode的函数 createComponent,在该函数中有对函数式组件的特殊处理。
function createComponent(Ctor,data,context,children,tag){
/* 省略... */
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor,propsData,data,context,children)
}
/* 省略... */
}
复制代码
从以上代码中能够看出,函数式组件的VNode是 createFunctionalComponent 函数的返回值。
function createFunctionalComponent(Ctor,propsData,data,contextVm,children){
var options = Ctor.options;
var props = {};
var propOptions = options.props;
if (isDef(propOptions)) {
for (var key in propOptions) {
props[key] = validateProp(key, propOptions, propsData || emptyObject);
}
} else {
if (isDef(data.attrs)) { mergeProps(props, data.attrs); }
if (isDef(data.props)) { mergeProps(props, data.props); }
}
var renderContext = new FunctionalRenderContext(
data,
props,
children,
contextVm,
Ctor
);
var vnode = options.render.call(null, renderContext._c, renderContext);
if (vnode instanceof VNode) {
return cloneAndMarkFunctionalResult(vnode, data, renderContext.parent, options, renderContext)
} else if (Array.isArray(vnode)) {
var vnodes = normalizeChildren(vnode) || [];
var res = new Array(vnodes.length);
for (var i = 0; i < vnodes.length; i++) {
res[i] = cloneAndMarkFunctionalResult(vnodes[i], data, renderContext.parent, options, renderContext);
}
return res
}
}
复制代码
createFunctionalComponent 函数主要有四个功能:
一、将 attrs、props 上的值都合并到 props 中。
二、根据传入的 context 值,合并生成上下文参数对象 renderContext。
三、由手写的 render 函数生成VNode。
四、克隆VNode,而后添加fnContext、fnOptions等属性。
这里能够看出函数式组件与普通组件最大的区别:普通组件生成组件VNode,VNode对上有指向组件实例的componentInstance属性。函数式组件根据render函数生成VNode,自己并无相应的组件实例。。
根据函数式组件生成的VNode以下所示:
VNode = {
/* 省略... */
tag: "div",
children: [{/*子节点VNode*/}],
devtoolsMeta: {renderContext: {/*createElement第二个参数对象*/}},
fnContext: {/*上下文信息*/},
fnOptions: {/*函数式组件选项*/},
isCloned: true,
isRootInsert: true,
componentInstance: undefined,
componentOptions: undefined,
data: undefined
/* 省略... */
}
复制代码
在 patch 阶段,由于根据函数式组件生成的VNode上并无组件选项 componentOptions 属性,根据VNode生成真实DOM的过程与普通组件同样。
实际上,函数式组件仅仅是生成包裹内容对应的VNode,在生成真实DOM的时候,函数式组件彻底透明,生成的DOM由根据包裹内容而定的。
组件注册的方式有两种:局部注册、全局注册。组件注册的实质是根据传入的选项生成Vue子构造函数,在使用组件时使用子构造函数生成组件实例。全局注册组件的信息在局部组件注册对象的原型上,所以全局注册的组件能够不重复注册而被全局使用。
根据组件生成的渲染函数除了slot以外跟普通的标签同样,组件渲染函数生成的VNode上有组件选项信息属性 componentOptions。在 patch 的过程当中,首先生成组件实例,而后根据组件实例生成真实DOM并挂载。
普通组件都会生成对应的组件实例对象,相对而言开销比较大。函数式组件不会生成专门的VNode以及实例对象,函数式组件至关于一个容器,在组件生成时直接渲染包裹的内容。
欢迎关注公众号:前端桃花源,互相交流学习!