开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 14 天,点击查看活动详情
动态组件
当我们需要在不同的组件之间进行状态切换时,动态组件可以很好的满足需求。动态组件的核心是 component
标签和 is
属性的使用
动态组件的基本使用
<div id="app">
<button @click="changeTabs('child1')">child1</button>
<button @click="changeTabs('child2')">child2</button>
<button @click="changeTabs('child3')">child3</button>
<component :is="chooseTabs">
</component>
</div>
<script>
var child1 = {
template: '<div>content1</div>',
}
var child2 = {
template: '<div>content2</div>'
}
var child3 = {
template: '<div>content3</div>'
}
var vm = new Vue({
el: '#app',
components: {
child1,
child2,
child3
},
methods: {
changeTabs(tab) {
this.chooseTabs = tab;
}
}
})
</script>
AST 解析
<component>
标签的处理基本上和之前的步骤基本一致。针对动态组件解析的差异,集中在 processComponent
上, 由于标签上 is
属性的存在,最终会在 AST
树上添加 component
标志
// 针对动态组件进行解析
function processComponent (el) {
let binding
// 拿到 is 属性对应的值
if ((binding = getBindingAttr(el, 'is'))) {
// 在 AST 树上添加 component 属性
el.component = binding
}
if (getAndRemoveAttr(el, 'inline-template') != null) {
el.inlineTemplate = true
}
}
最终生成的 AST
中包含了 component
属性,属性值为组件名称
render 函数
有了 AST
树,接下来是根据 AST
树生成可执行的 render
函数,由于有 component
属性, render
函数的产生过程会走 genComponent
分支
// render函数生成函数
var code = generate(ast, options);
// generate函数的实现
function generate (ast,options) {
var state = new CodegenState(options);
var code = ast ? genElement(ast, state) : '_c("div")';
return {
render: ("with(this){return " + code + "}"),
staticRenderFns: state.staticRenderFns
}
}
function genElement(el, state) {
···
var code;
// 动态组件分支
if (el.component) {
code = genComponent(el.component, el, state);
}
}
针对动态组件的处理逻辑比较简单,当没有内联模版标志是,拿到后续的字节点进行拼接,和普通组件唯一的区别在于, _c
的第一个参数不再是一个指定的字符串,而是一个代表组件的变量。
// 针对动态组件的处理
function genComponent (
componentName,
el,
state
) {
// 拥有inlineTemplate属性时,children为null
var children = el.inlineTemplate ? null : genChildren(el, state, true);
return ("_c(" + componentName + "," + (genData$2(el, state)) + (children ? ("," + children) : '') + ")")
}
普通组件和动态组件的对比
普通组件的 render 函数
"with(this){return _c('div',{attrs:{"id":"app"}},[_c('child1',[_v(_s(test))])],1)}"
动态组件的 render 函数
"with(this){return _c('div',{attrs:{"id":"app"}},[_c(chooseTabs,{tag:"component"})],1)}"
动态组件和普通组件的区别在于:
-
- 动态组件在
AST
树生成阶段新增了component
属性,这是动态组件的标志
- 动态组件在
-
- 产生
render
函数阶段由于component
属性的窜爱,会执行genComponent
分支 ,genComponent
会针对动态组件的执行函数进行特殊的处理,和普通组件不同的是, 动态组件render
函数中_c
的第一个参数不再是不变的字符串,而是指定的组件名称变量。
- 产生
-
render
函数到虚拟DOM
的阶段和普通组件的流程相同,只是字符串换成变量,并带有{tag: 'component'}
的data
属性。
内置组件
内置组件是已经在源码初始化阶段就全局注册好的组件,在 Vue
官方文档中对内置组件进行了列举,分别是 component
、 transition
、 transition-group
、 keep-alive
、 slot
。在前面对 slot
component
进行分析后,意识到 slot
和 component
并不是真正的内置组件, 这两个没有被当成组件去处理,因此也没有组件的生命周期。 slot
只会在 render
函数阶段转换成 renderSlot
函数进行处理, 而 component
也只是借助 is
属性将 createElement
的第一个参数从字符串转换为变量。
下面,我们分析一下 transition
transition-group
keep-alive
这几个内置组件的注册流程,已经在编译时有什么不同。
构造器定义组件
Vue
在初始化阶段,会在构造器的 components
属性添加三个组件对象,每个组件对象的写法和自定义组件过程一直,有 render
函数,生命周期,也会定义各种数据。
// source/src/platforms/web/runtime/index.js
// Vue构造器的选项配置,compoents选项合并
extend(Vue.options.components, builtInComponents);
extend(Vue.options.components, platformComponents);
extend
方法前面已经分析过,核心逻辑就是将对象上的属性合并到源对象上,属性相同则覆盖
export function extend (to: Object, _from: ?Object): Object {
for (const key in _from) {
to[key] = _from[key]
}
return to
}
最终 Vue
构造器拥有了三个组件的配置选项
Vue.components = {
keepAlive: {},
transition: {},
transition-group: {},
}
注册内置组件
Vue
实例在初始化过程中,最重要的一步就是进行选项合并,而像内置组件这些资源类选项会有专门的选项合并策略。最终构造器上的组件选项会以原型链的形式注册到实例的 components
选贤中
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
// Vue 默认选项的合并,这些选项会合并到每一个 Vue 实例中
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets
})
function mergeAssets (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): Object {
const res = Object.create(parentVal || null) // 创建一个🈳️对象,使其原型指向父类的资源选项,对于内置的 组件、指令、过滤器需要通过原型的方式来进行调用
if (childVal) {
// 开发环境下校验选项的合法性, component directive filter 这些选项需要是一个对象
process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
return extend(res, childVal)
} else {
return res
}
}
mergeAssets
方法中关键的两步其中一个是 const res = Object.create(parentVal || null)
这会以 parentVal
为原型创建一个空对象,最后通过 extend
将用户自定义的 component
选项复制到空对象中,选项合并后,内置组件也因此在全局完成了注册。