一,前言
上篇,介绍了《组件部分-组件挂载流程简述》;
至此,【Vue2.x 源码学习】专栏核心的几个专题就初步完成了,包括:
- 响应式数据原理(第一篇 ~ 第十篇)
- 模板编译原理(第十一篇 ~ 第二十篇)
- 依赖收集、异步更新、生命周期(第二十一篇 ~ 第二十七篇)
- diff 算法原理(第二十八篇 ~ 第三十三篇)
- 组件部分(第三十四篇 ~ 第四十二篇)
简单总结一下,专栏存在的问题及后续迭代计划:
- 响应式数据原理部分:写的有些乱,顺序上还需要再做调整,好在事情讲清楚了;
- 模板编译原理部分:部分内容仍需要再细化,要把流程讲清楚,让人一看就懂;
- diff 算法部分:还需要添加一些必要图示和动画,这部分不难,但做好不容易;
- 组件部分:近半月工作比较忙,这部分质量并不高,二轮首先优化这块;
计划在 8 月更文活动完结后,进入第二轮优化阶段;
目标:修复问题较大的几篇文章,并在过程中梳理规划出第三轮优化的内容;
收回来;本篇对组件部分的几篇文章做一次阶段性总结《组件部分-组件相关流程总结》;
二,主要流程划分
- Vue.component 的实现
- Vue.extend 的实现
- 组件合并的实现
- 组件编译的实现
- 创建组件的虚拟节点
- 组件生命周期的实现
- 创建组件的真实节点
- 组件挂载的实现
三,各流程的实现简述
1,Vue.component 的实现
- 在 Vue 初始化流程中,会对全局 API 做集中处理,创建出 Vue.component API;
- 将 Vue 保存到全局对象
Vue.options上,以便在后续流程中,组件可以直接通过vm.$options._base获取到Vue;备注:子类组件上并没有extend方法,需要通过Vue才能获取到,再将组件定义对象处理为组件的构造函数; - 在
Vue.component中,若组件定义为对象,使用Vue.extend处理为组件构造函数; - 扩展
Vue.options对象,将全局组件定义维护到Vue.options.components上;
// src/global-api/index.js
export function initGlobalAPI(Vue) {
Vue.options = {};
Vue.options._base = Vue; // 便于组件通过 vm.$options._base 拿到 Vue;
Vue.options.components = {};
Vue.extend = function (definition) {}
Vue.component = function (id, definition) {
let name = definition.name || id;
definition.name = name;
// 处理组件定义,生成组件构造函数
if(isObject(definition)){
definition = Vue.extend(definition)
}
// 维护组件与构造函数的映射关系
Vue.options.components[name] = definition;
}
}
Vue.options.components的作用:
- 利用全局对象
vm.constructor.options完成全局组件与局部组件的合并; - 通过组件虚拟节点的标签名,查询对应组件的构造函数,完成组件的实例化;
2,Vue.extend 的实现
Vue.extend:使用基础 Vue 构造器,创建一个子类;Vue.extend:内部会根据组件定义生成一个继承于 Vue 原型的组件子类Sub;- 修复
constructor指向问题:由于Object.create会产生一个新的实例作为子类的原型,导致constructor指向错误,应指向当前子类Sub; - 返回组件的构造函数
Sub,Vue.component中将对组件构造函数进行全局映射;
// src/global-api/index.js
export function initGlobalAPI(Vue) {
Vue.extend = function (definition) {
const Super = this;
const Sub = function (options) {
this._init(options);
}
// 子类继承父类原型
Sub.prototype = Object.create(Super.prototype);
// 修复 constructor 指向问题,指向 Sub
Sub.prototype.constructor = Sub;
return Sub;
}
}
3,组件合并的实现
- 此时,
vm.constructor.options已经包含了Vue.options.components中的全局组件声明; - 执行
new Vue时,会进行组件的初始化,进入_init方法; - 在
_init方法中,通过mergeOptions方法:将new Vue传入的局部组件定义options与全局组件定义进行合并操作; - 在
mergeOptions方法中,通过策略模式,获取到预设的组件合并策略函数; - 组件的合并策略:创建新对象继承于全局组件定义,并将局部组件定义添加到新对象中;此时会优先在新对象中查找局部组件定义,若未找到,会继续通过链上的继承关系查找全局组件定义;
// src/init.js#initMixin
Vue.prototype._init = function (options) {
const vm = this;
// 组件合并
vm.$options = mergeOptions(vm.constructor.options, options);
initState(vm);
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
组件的合并策略
// src/utils.js
let strats = {}; // 用于存放策略函数
// 设置组件的合并策略
strats.component = function (parentVal, childVal) {
// 将全局组件定义放到链上
let res = Object.create(parentVal);
// 将局部组件定义放到对象上
if(childVal){
for (let key in childVal) {
res[key] = childVal[key];
}
// 优先查找局部组件定义,若未找到,会继续通过链上的继承关系查找全局组件定义;
return res;
}
}
// 根据合并策略进行选项的合并
export function mergeOptions(parentVal, childVal) {
let options = {};
for(let key in parentVal){
mergeFiled(key);
}
for(let key in childVal){
if(!parentVal.hasOwnProperty(key)){
mergeFiled(key);
}
}
function mergeFiled(key) {
// 策略模式:获取当前 key 的合并策略
let strat = strats[key];
if(strat){
options[key] = strat(parentVal[key], childVal[key]);
}else{ // 默认合并策略:新值覆盖老值
options[key] = childVal[key] || parentVal[key];
}
}
return options;
}
需要注意:
- 在
vm.constructor.options中的全局组件,可能已被Vue.extend处理为函数(组件的构造函数); - 在
options中的局部组件,不会被Vue.extend处理,此时还是一个对象;
4,组件编译的实现
- 与模板编译流程相似:组件模板 -> AST 语法树 -> render 函数
5,创建组件的虚拟节点
- 在
render函数中,通过createElement方法:生成组件的虚拟节点; - 在
createElement方法中,进行标签筛查,若为非普通标签则视为组件;获取组件定义(有可能是构造函数),并通过createComponent方法,创建组件虚拟节点componentVnode; - 在
createComponent中,当获取到的组件定义Ctor为对象时,需先通过Vue.extend处理为组件的构造函数; - 获取事先保存在全局
vm.$options._base中的Vue,使用Vue.extend处理,生成组件构造函数; - 通过
vnode方法生成组件的虚拟节点componentVnode,将组件相关信息封装到componentOptions对象中;完整的componentOptions包括:Ctor、propsData、listeners、tag、children;
// src/vdom/index.js
export function createElement(vm, tag, data={}, ...children) {
// 处理组件类型
if (!isReservedTag(tag)) {
let Ctor = vm.$options.components[tag];
// 创建组件的虚拟节点
return createComponent(vm, tag, data, children, data.key, Ctor);
}
// 创建元素的虚拟节点
return vnode(vm, tag, data, children, data.key, Ctor);
}
// 创建组件虚拟节点 componentVnode
function createComponent(vm, tag, data, children, key, Ctor) {
if(isObject(Ctor)){
// 通过 Vue.extend 创建组件的构造函数
Ctor = vm.$options._base.extend(Ctor)
}
let componentVnode = vnode(vm, tag, data, undefined, key, undefined, {Ctor, children, tag});
return componentVnode;
}
注意,所有组件最终都会被 Vue.extend 处理成为组件的构造函数:
- 全局组件:在
Vue.component内部可能已经被Vue.extend处理完成; - 局部组件:在
createComponent创建组件虚拟节点时,被Vue.extend处理;
6,组件生命周期的实现
- 在
createComponent方法创建组件虚拟节点过程中,通过扩展data属性,为组件添加生命周期钩子函数; - 在组件初始化时,通过执行
init钩子函数,实现组件的实例化并完成页面挂载;
// src/vdom/index.js
function createComponent(vm, tag, data, children, key, Ctor) {
if(isObject(Ctor)){
Ctor = vm.$options._base.extend(Ctor)
}
// 扩展组件的生命周期
data.hook = {
init(){
console.log("Hook-init:执行组件实例化并完成挂载");
// 注意:此处的 vm 不是组件实例,需将当前组件实例存取来
let child = vnode.componentInstance = new Ctor({});
child.$mount();
},
prepatch(){},
postpatch(){}
}
let componentVnode = vnode(vm, tag, data, undefined, key, undefined, {Ctor, children, tag});
return componentVnode;
}
注意:将组件实例保存到虚拟节点上
vnode.componentInstance,便于后续获取组件真实节点,完成组件的挂载操作;
7,创建组件的真实节点
- 在
createElm方法中,通过执行createComponent方法,将组件虚拟节点生成真实节点并返回;
备注:
createComponent执行完毕后,vnode.componentInstance赋值为组件实例,vnode.componentInstance.$el即为组件的真实节点;
// 根据虚拟节点创建真实节点(递归)
export function createElm(vnode) {
let { tag, data, children, text, vm } = vnode;
if (typeof tag === 'string') {
if(createComponent(vnode)){// 组件处理:根据组件的虚拟节点创建真实节点
return vnode.componentInstance.$el;
}
vnode.el = document.createElement(tag)
updateProperties(vnode, data)
children.forEach(child => {
vnode.el.appendChild(createElm(child))
});
} else {
vnode.el = document.createTextNode(text)
}
return vnode.el;
}
- 在
createComponent方法中,若存在hook即为组件,通过组件init钩子函数,进行组件初始化操作;
// 根据组件的虚拟节点创建真实节点
function createComponent(vnode) {
let i = vnode.data;
// 先确定有 hook;再拿到 init 方法;
if((i = i.hook)&&(i = i.init)){
i(vnode); // 使用 init 方法处理 vnode
}
}
- 组件的
init钩子函数中,通过new Ctor实例化组件时,会执行_init进行组件的初始化,此时,vm.$options.el为空,不会自动挂载组件;
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = mergeOptions(vm.constructor.options, options);
initState(vm);
// 由于 el 不存在,所以不会执行 vm.$mount
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
- 通过
child.$mount()进行组件挂载操作,由于$mount参数el为null,所以也不会进行挂载;
Vue.prototype.$mount = function (el) {
const vm = this;
const opts = vm.$options;
el = document.querySelector(el);
vm.$el = el;
if (!opts.render) {
let template = opts.template;
if (!template) {
template = el.outerHTML;
}
let render = compileToFunction(template);
opts.render = render;
}
mountComponent(vm);
}
- 生成组件
render函数后,执行mountComponent进行组件的挂载;
// src/lifeCycle.js
export function mountComponent(vm) {
let updateComponent = ()=>{
vm._update(vm._render());
}
callHook(vm, 'beforeCreate');
new Watcher(vm, updateComponent, ()=>{
callHook(vm, 'created');
},true)
callHook(vm, 'mounted');
}
- 在
updateComponent中,通过_render产生组件虚拟节点:
Vue.prototype._render = function () {
const vm = this;
let { render } = vm.$options;
let vnode = render.call(vm);
return vnode
}
vm.render执行完成后,继续执行_update方法:
// src/lifeCycle.js
export function lifeCycleMixin(Vue){
Vue.prototype._update = function (vnode) {
const vm = this;
let preVnode = vm.preVnode;
vm.preVnode = vnode;
if(!preVnode){// 初渲染
// 传入当前真实元素vm.$el,虚拟节点vnode,返回真实节点
vm.$el = patch(vm.$el, vnode);
}else{// 更新渲染:新老虚拟节点做 diff 比对
vm.$el = patch(preVnode, vnode);
}
}
}
- 初渲染
preVnode为空,patch方法中oldVnode为null(组件的el为空),使用组件的虚拟节点,创建出组件的真实节点并返回:
export function patch(oldVnode, vnode) {
if(!oldVnode){// 组件挂载流程
return createElm(vnode); // 直接使用组件虚拟节点创建真实节点
}
- 返回的
vm.$el即为组件的真实节点;
8,组件挂载的实现
- 在
createElm方法内中,会递归的生成真实节点,并插入对应的父节点中; createElm为深度优先遍历,最终将完整的div挂载到页面上;
// 根据虚拟节点创建真实节点(递归)
export function createElm(vnode) {
let { tag, data, children, text, vm } = vnode;
if (typeof tag === 'string') {
if(createComponent(vnode)){// 组件处理:根据组件的虚拟节点创建真实节点
return vnode.componentInstance.$el;
}
vnode.el = document.createElement(tag)
updateProperties(vnode, data)
// 将真实节点插入到对应的父节点中
children.forEach(child => {
vnode.el.appendChild(createElm(child))
});
} else {
vnode.el = document.createTextNode(text)
}
return vnode.el;
}
备注:
_update执行完成,继续回到mountComponent方法,执行beforeCreate钩子、生成组件独立的渲染watcher、执行mounted钩子,完成组件挂载;
// src/lifeCycle.js
export function mountComponent(vm) {
let updateComponent = ()=>{
vm._update(vm._render());
}
callHook(vm, 'beforeCreate');
new Watcher(vm, updateComponent, ()=>{
callHook(vm, 'created');
},true)
callHook(vm, 'mounted');
}
四,结尾
本篇,对组件相关流程与实现进行了简单总结;
【Vue2.x 源码学习】第一阶段完结,后续将持续进行优化;