一,前言
上篇,介绍了组件部分-组件合并的实现,主要涉及以下几个点:
- 组件注册流程梳理;
- 组件合并的分析;
- 组件合并的必要性;
- 组件合并的问题;
- 组件合并的实现;
- 生命周期的合并策略-策略模式回顾;
- 组件合并策略的实现;
- 组件合并的测试;
本篇,组件部分-组件的编译;
二,前文回顾
1,组件初始化流程回顾
- 通过
Vue.component全局API,声明全局组件; - 在
Vue.component内部,通过Vue.extend生成组件的构造函数; - 将组件的构造函数维护到全局对象
Vue.options.components中备用; - 当
new Vue初始化时,在mergeOptions方法中,使用策略模式找到预置的组件合并策略,完成全局组件和局部组件的合并操作;
// 全局定义的内容,会被混合在当前实例上
vm.$options = mergeOptions(vm.constructor.options, options);
组件的查找规则:优先查找局部组件,若找不到,则继续沿着链查找全局组件;
至此,在 vm.$options 中,就已经完成了组件关系的构建;
接下来,就要开始做组件模板解析->render 函数->虚拟节点->真实节点...
2,模板解析流程回顾
- 第一步,解析
html模板生成AST语法树; - 第二步,根据
AST语法树生成render函数; - 第三步,在
render函数中,调用_c处理标签tag(即createElement方法),生成元素标签的虚拟节点vnode;
此时,在 _c 方法中处理的标签 tag,可能是元素,也可能是组件;
这里,就需要对tag为组件的情况进行扩展,最终要生成组件的虚拟节点;
三,组件的编译流程分析
以一个简单的组件为例,组件的编译过程与模板的编译过程相似:
<div id="app">
<my-button></my-button>
</div>
- 第一步,解析组件的
html模板生成AST语法树; - 第二步,根据
AST语法树生成render函数; - 第三步,在
render函数中,通过_c处理组件tag,生成组件的虚拟节点componentVnode;
组件编译与标签编译的区别在于:
- 标签编译:需要生成标签的虚拟节点;
- 组件编译:需要生成组件的虚拟节点;
因此,需要对原 createElement 方法进行扩展,使之能够支持组件的编译,生成组件的虚拟节点;
四,创建组件的虚拟节点
1,扩展 createElement 方法
原先的 createElement 方法:生成标签元素的虚拟节点 vnode:
// 参数:_c('标签', {属性}, ...儿子)
export function createElement(vm, tag, data={}, ...children) {
// 返回元素的虚拟节点(元素没有文本)
return vnode(vm, tag, data, children, data.key, undefined);
}
当前,由于组件的加入,在createElement 方法中,tag 不一定是元素,还有可能是组件,所以需要对组件需要单独进行处理:
export function createElement(vm, tag, data={}, ...children) {
console.log(tag);
// 添加 tag 为组件时的处理,创建组件的虚拟节点 componentVnode
// todo ...
return vnode(vm, tag, data, children, data.key, undefined);
}
控制台日志共输出 2 次:
- 第一次:
my-button(组件) - 第二次:
div(元素)
因此,需要对 createElement 方法进行扩展:增加 tag 为组件时的处理逻辑,创建对应组件的虚拟节点 componentVnode;
那么,如何区分当前 tag 是组件还是元素呢?
2,判断当前 tag 是组件 or 元素?
判断依据:tag 是否属于原始标签(普通标签);
- 若
tag属于原始标签,则tag为元素,如:div; - 若
tag不属于原始标签,则tag为组件, 如:my-button;
实现方案:将所有的普通标签统一维护起来,组成以逗号分割的字符串,检查当前 tag 是否包含在其中,若不包含则判定为组件,反之为元素;
创建 isReservedTag(tag) 方法,判断是否为原始标签:
// 添加 tag 为组件时的处理逻辑,创建出组件的虚拟节点
export function createElement(vm, tag, data={}, ...children) {
// 判断是组件 or 元素节点;判断依据:是否属于普通标签;
if (!isReservedTag(tag)) {// 组件:非普通标签即为组件
// 创建组件的虚拟节点
return createComponent(vm, tag, data, children, data.key, Ctor);
}
// 创建元素的虚拟节点
return vnode(vm, tag, data, children, data.key, undefined);
}
/**
* 创建组件的虚拟节点 componentVnode
*/
function createComponent(vm, tag, data, children, key, Ctor) {
// todo...
}
// 判定包含关系
function makeMap(str) {
let tagList = str.split(',');
return function (tagName) {
return tagList.includes(tagName);
}
}
// 原始标签
export const isReservedTag = makeMap(
'template,script,style,element,content,slot,link,meta,svg,view,button,' +
'a,div,img,image,text,span,input,switch,textarea,spinner,select,' +
'slider,slider-neighbor,indicator,canvas,' +
'list,cell,header,loading,loading-indicator,refresh,scrollable,scroller,' +
'video,web,embed,tabbar,tabheader,datepicker,timepicker,marquee,countdown'
)
接下来,实现 componentVnode 方法:创建组件的虚拟节点;
3,createComponent 方法定义
“获取对应组件的构造函数,并创建组件的虚拟节点”:
- 通过
vm.$options.components获取对应组件的构造函数; - 通过
createComponent方法创建组件的虚拟节点;
// 添加 tag 为组件时的处理逻辑,创建出组件的虚拟节点
export function createElement(vm, tag, data={}, ...children) {
if (!isReservedTag(tag)) {// 当前 tag 为组件
// 获取组件的构造函数: 之前已保存在全局 vm.$options.components 上
let Ctor = vm.$options.components[tag];
// 创建组件的虚拟节点
return createComponent(vm, tag, data, children, data.key, Ctor);
}
// 创建元素的虚拟节点
return vnode(vm, tag, data, children, data.key, undefined);
}
/**
* 创建组件的虚拟节点 componentVnode
*/
function createComponent(vm, tag, data, children, key, Ctor) {
let componentVnode;
// todo...
return componentVnode;
}
组件构造函数 Ctor 的取值过程:
- 首先,先到
vm.$options.components对象上查找局部组件,如果找到了Ctor会是一个对象;(因为局部组件在定义时,不会被Vue.extend处理成为组件构造函数) - 如果没找到,则会继续沿着链查找全局组件,如果找到了
Ctor会是一个函数;(因为全局组件在定义时,内部会通过Vue.extend处理成为组件构造函数)
所以,在 createComponent 中,当 Ctor 为对象时,还需要先通过 Vue.extend 处理为组件的构造函数;
/**
* 创建组件的虚拟节点 componentVnode
*/
function createComponent(vm, tag, data, children, key, Ctor) {
if(isObject(Ctor)){
// todo:获取 Vue.extend,并将对象处理成为组件的构造函数
}
let componentVnode;
return componentVnode;
}
问题:在
createComponent中,如何才能拿到Vue.extend方法呢?
4,如何获取到 Vue.extend
为了便于在后续操作中能够快速获取到 Vue.extend 方法:
- 在
Vue初始化流程中,初始化全局 APIinitGlobalAPI时,将Vue整个保存到Vue.options._base中;
// src/global-api/index.js
export function initGlobalAPI(Vue) {
Vue.options = {};
// 当组件初始化时,会使用 Vue.options 和组件 options 进行合并;
// 在这个过程中,_base 也会被合并到组件的 options 上;
// 之后,所有的 vm.$options 就都可以取到 _base 即 Vue;
// 这样,在任何地方通过 vm.$options._base 都可以拿到 Vue;
Vue.options._base = Vue;
Vue.options.components = {};
Vue.extend = function (opt) { }
Vue.component = function (id, definition) { }
}
- 当组件初始化时,会将
Vue.options和组件的options进行合并,在这个过程中_base也将被合并到组件的options上;
Vue.prototype._init = function (options) {
const vm = this;
// 使用 Vue 的 options 和组件自己的options进行合并
vm.$options = mergeOptions(vm.constructor.options, options);
initState(vm);
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
这样一来,所有组件就都能够通过 vm.$options 中的 _base 属性拿到 Vue;
/**
* 创建组件的虚拟节点 componentVnode
*/
function createComponent(vm, tag, data, children, key, Ctor) {
if(isObject(Ctor)){
// 获取 Vue,并通过 Vue.extend 将对象处理成为组件的构造函数
Ctor = vm.$options._base.extend(Ctor)
}
let componentVnode;
return componentVnode;
}
至此,所有的组件最终都通过 Vue.extend 方法的处理,生成了组件的构造函数:
- 全局组件:在
Vue.component内部就被Vue.extend处理; - 局部组件:在
createComponent创建组件虚拟节点时,才被Vue.extend处理;
问题分析:
为什么使用
vm.$options._base,而不是用vm.constructor_base?1,当
vm为组件的实例时,比如new Profile().$mount():
- 由于,子类
Sub继承自Vue,所以,vm一定是Sub的实例;- 在子类
Sub上并没有extend方法,即vm上也不存在extend方法;此时,
vm.constructor指向的子类Sub上没有extend方法;2,
vm.$options一定指向Vue而不会是子类因此,使用
vm.$options._base确保拿到Vue,即必定可以得到extend方法;
接下来,生成组件的虚拟节点 componentVnode;
5,createComponent 方法实现
扩展 vnode 结构
想要生成组件的虚拟节点,需要对原来的 vnode 结构进行扩展,添加组件选项属性componentOptions:
/**
* 创造组件的虚拟节点 componentVnode
*/
function createComponent(vm, tag, data, children, key, Ctor) {
if(isObject(Ctor)){
// 获取 Vue,并通过 Vue.extend 将对象处理成为组件的构造函数
Ctor = vm.$options._base.extend(Ctor)
}
// 创建 vnode 时,组件是没有文本的,需要传入 undefined
let componentVnode = vnode(vm, tag, data, children, key, undefined, Ctor);
return componentVnode;
}
// options:可能是组件的构造函数,也可能是对象
function vnode(vm, tag, data, children, key, text, options) {
return {
vm, // 谁的实例
tag, // 标签
data, // 数据
children, // 儿子
key, // 标识
text, // 文本
componentOptions: options // 组件的选项,包含 Ctor 及其他扩展项
}
}
扩展组件选项 componentOptions
- 组件没有孩子,“组件的孩子”就是
slot插槽,所以children应该被放入组件选项componentOptions中; - 当前元素为组件时,
data数据也属于组件,需要被放入组件选项componentOptions中;同理,组件的其它属性也应该被放入组件选项中(完整的componentOptions包含:Ctor、propsData、listeners、tag、children)
function createComponent(vm, tag, data, children, key, Ctor) {
if(isObject(Ctor)){
Ctor = vm.$options._base.extend(Ctor)
}
// 注意:组件没有孩子,组件的孩子就是插槽,所以需要将原本的 children 放到组件的选项中去
let componentVnode = vnode(vm, tag, data, undefined, key, undefined, {Ctor, children, tag});
return componentVnode;
}
6,测试组件虚拟节点生成
componentVnode 即为组件的虚拟节点;
其中,在componentOptions 选项中,包含组件的构造函数 Ctor;
7,组件的唯一标识
下图来自 Vue2 源码:
与手写版本对比,在创建 VNode 虚拟节点时确定了组件唯一标识:
组件虚拟节点的唯一标识为:vue-component-cid-name
cid:组件实例的唯一标识;name:组件定义中的name属性;
同时注意到,在创建 VNode 之前,还有一步installComponentHooks操作,即在 data 上扩展了组件的生命周期钩子函数;
下一篇,介绍组件的生命周期;
五,结尾
本篇,介绍了组件部分-组件的编译,主要涉及以下几部分:
- 对组件初始化流程、模板解析流程进行回顾;
- 组件的编译流程分析;
- 组件html模板 -> AST语法树 -> render函数 -> 组件vnode
- 扩展 createElement 方法,创建组件虚拟节点;
- 判断 tag 是组件 or 元素;
- 获取 Vue.extend 方法,创建组件构造函数;
- 扩展 vnode 结构,支持组件选项 componentOptions;
- 扩展组件选项 componentOptions;
- 生成组件虚拟节点 componentVnode;
- 测试组件虚拟节点生成;
下一篇,组件部分-组件的生命周期;
维护记录
- 20230226:
- 添加了串联上下文的问题;
- 补充了“组件中的钩子函数”部分;
- 优化了多处描述,修正语义表达不准确;
- 更新了文章摘要;
- 20230308:
- 重新梳理本篇知识点,重新设计文章结构;
- 更新文章目录;
- 重新拆分为三大块:回顾、分析、实现
- 移除“组件钩子函数”部分(合并到下一篇,组件的生命周期)
- 更新文章摘要;
- 20230316:
- 添加“组件的唯一标识”部分,引出“组件的生命周期”;
- 添加“使用 vm.$options._base”的问题分析;