开启掘金成长之旅!这是我参与「掘金日新计划 · 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 选项复制到空对象中,选项合并后,内置组件也因此在全局完成了注册。