基本使用:
全局组件和局部组件
<script>
// 全局组件
Vue.component("parent-component", {
template: `<div>我是全局组件</div>`,
});
let vm = new Vue({
el: "#app",
data() {
return {
aa: 1,
};
},
template: `<div id="a">
hello 这是我自己写的Vue{{aa}}
<parent-component><parent-component>
<child-component></child-component>
</div>`,
// 局部组件
components: {
"child-component": {
template: `<div>我是局部组件</div>`,
},
},
});
</script>
initGlobalApi
初始化子类构造器、组件注册方法
// src/global-api/index.js
import initExtend from "./initExtend";
import initAssetRegisters from "./assets";
const ASSETS_TYPE = ["component", "directive", "filter"];
export function initGlobalApi(Vue) {
Vue.options = {}; // 全局的组件 指令 过滤器
ASSETS_TYPE.forEach((type) => {
Vue.options[type + "s"] = {};
});
//_base指向Vue
Vue.options._base = Vue;
// extend方法定义
initExtend(Vue);
//assets注册方法 包含组件 指令和过滤器
initAssetRegisters(Vue);
}
initExtend
初始化Vue.extend
// src/global-api/initExtend.js
import { mergeOptions } from "../util/index";
export default function initExtend(Vue) {
let cid = 0; //组件的唯一标识
// 创建子类继承Vue父类 便于属性扩展
Vue.extend = function (extendOptions) {
// 创建子类的构造函数 并且调用初始化方法
const Sub = function VueComponent(options) {
this._init(options); //调用Vue初始化方法
};
//改变子类的原型
Sub.cid = cid++;
Sub.prototype = Object.create(this.prototype); // 子类原型指向父类,即指向Vue
Sub.prototype.constructor = Sub; //constructor指向自己
Sub.options = mergeOptions(this.options, extendOptions); //合并自己的options和父类的options
return Sub;
};
}
initAssetRegisters
创建全局组件的方法
// src/global-api/asset.js
// 初始化全局api
initAssetRegisters(Vue);
var ASSET_TYPES = [
'component',
'directive',
'filter'
];
function initAssetRegisters(Vue){
// 定义ASSET_TYPES中每个属性的方法,其中包括component
ASSET_TYPES.forEach(function (type) {
// type: component,directive,filter
Vue[type] = function (id,definition) {
if (!definition) {
// 直接返回注册组件的构造函数
return this.options[type + 's'][id]
}
...
if (type === 'component') {
// 验证component组件名字是否合法
validateComponentName(id);
}
if (type === 'component' && isPlainObject(definition)) {
// 组件名称设置
definition.name = definition.name || id;
// Vue.extend() 创建子组件,返回子类构造器
definition = this.options._base.extend(definition);
}
// 为Vue.options 上的component属性添加将子类构造器
this.options[type + 's'][id] = definition;
return definition
}
});
}
- 调用extend方法为组件创建一个子类构造器,此时的this.options._base代表的就是Vue构造器。它会基于父类去创建一个子类,此时的父类是Vue,并且创建过程子类会继承父类的方法,并会和父类的选项进行合并,最终返回一个子类构造器
- 代码处还有一个逻辑,Vue.component()默认会把第一个参数作为组件名称,但是如果组件选项有name属性时,name属性值会将组件名覆盖
组件的合并策略
全局组件挂载在 Vue.options.components 上 ,局部组件也定义在自己的 options.components 上面 ,合并时使用到了原型继承的方式来进行组件合并,组件内部优先查找自己局部定义的组件,找不到会向上查找原型中定义的组件
// src/util/index.js
const ASSETS_TYPE = ["component", "directive", "filter"];
// 组件 指令 过滤器的合并策略
function mergeAssets(parentVal, childVal) {
//比如有同名的全局组件和自己定义的局部组件 那么parentVal代表全局组件 自己定义的组件是childVal 首先会查找自已局部组件有就用自己的 没有就从原型继承全局组件 res.__proto__===parentVal
const res = Object.create(parentVal);
if (childVal) {
for (let k in childVal) {
res[k] = childVal[k];
}
}
return res;
}
// 定义组件的合并策略
ASSETS_TYPE.forEach((type) => {
strats[type + "s"] = mergeAssets;
});
创建组件 Vnode
Vue.component('test', {
template: '<span></span>'
})
var vm = new Vue({
el: '#app',
template: '<div><test></test></div>'
})
父render函数
function() {
with(this){return _c('div',[_c('test')],1)}
}
- render函数生成Vnode过程中,子会优先父执行生成Vnode过程,也就是_c(‘test’)函数会先被执行。'test’会先判断是普通的html标签还是组件的占位符。
- 如果为一般标签,会执行new Vnode过程,这也是上一章节我们分析的过程;如果是组件的占位符,则会在判断组件已经被注册过的前提下进入createComponent创建子组件Vnode的过程。
- createComponent是创建组件Vnode的过程,创建过程会再次合并选项配置,并安装组件相关的内部钩子(后面文章会再次提到内部钩子的作用),最后通过new Vnode()生成以vue-component开头的Virtual DOM
- render函数执行过程也是一个循环递归调用创建Vnode的过程,执行3,4步之后,完整的生成了一个包含各个子组件的Vnode tree
// src/util/index.js
//判断是否是对象
export function isObject(data) {
if (typeof data !== "object" || data == null) {
return false;
}
return true;
}
//判断是不是常规html标签
export function isReservedTag(tagName) {
// 定义常见标签
let str =
"html,body,base,head,link,meta,style,title," +
"address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section," +
"div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul," +
"a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby," +
"s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video," +
"embed,object,param,source,canvas,script,noscript,del,ins," +
"caption,col,colgroup,table,thead,tbody,td,th,tr," +
"button,datalist,fieldset,form,input,label,legend,meter,optgroup,option," +
"output,progress,select,textarea," +
"details,dialog,menu,menuitem,summary," +
"content,element,shadow,template,blockquote,iframe,tfoot";
let obj = {};
str.split(",").forEach((tag) => {
obj[tag] = true;
});
return obj[tagName];
}
createElement
// src/vdom/index.js
import { isObject, isReservedTag } from "../util/index";
// 创建元素vnode 等于render函数里面的 h=>h(App)
// 内部执行将render函数转化为Vnode的函数
function _createElement(context,tag,data,children,normalizationType) {
···
if (typeof tag === 'string') {
// 子节点的标签为普通的html标签,直接创建Vnode
if (config.isReservedTag(tag)) {
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
);
// 子节点标签为注册过的组件标签名,则子组件Vnode的创建过程
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 创建子组件Vnode
vnode = createComponent(Ctor, data, context, children, tag);
}
}
}
resolveAsset判断组件是否已注册并返回组件构造器
// 需要明确组件是否已经被注册
function resolveAsset (options,type,id,warnMissing) {
// 标签为字符串
if (typeof id !== 'string') {
return
}
// 这里是 options.component
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] }
// fallback to prototype chain
var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];
if (warnMissing && !res) {
warn(
'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
options
);
}
// 最终返回子类的构造器
return res
}
createComponent
- 拿到注册过的子类构造器后,调用createComponent方法创建子组件Vnode
function createComponent(vm, tag, data, key, children, Ctor) {
//针对局部注册的组件
if (isObject(Ctor)) {
//如果没有被改造成构造函数,再调用extend方法
Ctor = vm.$options._base.extend(Ctor);
}
data = data || {};
// 构造器配置合并
resolveConstructorOptions(Ctor);
// 挂载组件钩子
installComponentHooks(data);
//return a placeholder vnode
var name = Ctor.options.name || tag;
// 创建子组件vnode,名称以 vue-component- 开头
var vnode = new VNode(("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),data, undefined, undefined, undefined, context,{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },asyncFactory);
return vnode
- 局部注册添加的是一个子组件的配置对象,而全局注册添加的是一个子类构造器。
- 因此局部注册中缺少了一步构建子类构造器的过程,这个过程放到createComponent的源码,源码中根据选项是对象还是函数来区分局部和全局注册组件,如果选项的值是对象,则该组件是局部注册的组件,此时在创建子Vnode时会调用 父类的extend方法去创建一个子类构造器
installComponentHooks
// 组件内部自带钩子
var componentVNodeHooks = {
// 组件创建过程的自身初始化方法
init(vnode) {
let child = (vnode.componentInstance = new Ctor({ _isComponent: true })); //实例化组件
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
},
prepatch: function prepatch (oldVnode, vnode) {
},
insert: function insert (vnode) {
},
destroy: function destroy (vnode) {
}
};
var hooksToMerge = Object.keys(componentVNodeHooks);
// 将componentVNodeHooks 钩子函数合并到组件data.hook中
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];
// 如果钩子函数存在,则执行mergeHook$1方法合并
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook$1(toMerge, existing) : toMerge;
}
}
}
function mergeHook$1 (f1, f2) {
// 返回一个依次执行f1,f2的函数
var merged = function (a, b) {
f1(a, b);
f2(a, b);
};
merged._merged = true;
return merged
}
- 组件默认自带的这几个钩子函数会在后续patch过程的不同阶段执行
渲染组件真实节点
-
不管是全局注册的组件还是局部注册的组件,组件并没有进行实例化,那么组件实例化的过程发生在哪个阶段呢?我们接着看Vnode tree渲染真实DOM的过程
-
经过vm._render()生成完整的Virtual Dom树后,紧接着执行Vnode渲染真实DOM的过程,这个过程是vm.update()方法的执行,而其核心是vm.patch。
-
vm.__patch__内部会通过 createElm去创建真实的DOM元素,期间遇到子Vnode会递归调用createElm方法。
-
递归调用过程中,判断该节点类型是否为组件类型是通过createComponent方法判断的,该方法和渲染Vnode阶段的方法createComponent不同,他会调用子组件的init初始化钩子函数,并完成组件的DOM插入。
-
init初始化钩子函数的核心是new实例化这个子组件并将子组件进行挂载,实例化子组件的过程又回到合并配置,初始化生命周期,初始化事件中心,初始化渲染的过程。实例挂载又会执行$mount过程。
-
完成所有子组件的实例化和节点挂载后,最后才回到根节点的挂载。
// src/vdom/patch.js
// patch用来渲染和更新视图
export function patch(oldVnode, vnode) {
if (!oldVnode) {
// 组件的创建过程是没有el属性的
return createElm(vnode);
} else {
// 非组件创建过程省略
}
}
// 判断是否是组件Vnode
function createComponent(vnode) {
// 初始化组件
// 创建组件实例
let i = vnode.data;
// 下面这句话很关键 调用组件data.hook.init方法进行组件初始化过程 最终组件的vnode.componentInstance.$el就是组件渲染好的真实dom
if ((i = i.hook) && (i = i.init)) {
//调用init方法实例化组件
i(vnode);
}
// 如果组件实例化完毕有componentInstance属性 那证明是组件
if (vnode.componentInstance) {
return true;
}
}
// 虚拟dom转成真实dom
function createElm(vnode) {
const { tag, data, key, children, text } = vnode;
// 判断虚拟dom 是元素节点还是文本节点
if (typeof tag === "string") {
if (createComponent(vnode)) {
// 如果是组件 返回真实组件渲染的真实dom
return vnode.componentInstance.$el;
}
// 虚拟dom的el属性指向真实dom 方便后续更新diff算法操作
// vnode.el = document.createElement(tag);
// 解析虚拟dom属性
//updateProperties(vnode);
// 如果有子节点就递归插入到父节点里面
//children.forEach((child) => {
//return vnode.el.appendChild(createElm(child));
//});
createChildren(vnode, children, insertedVnodeQueue);
···
//子节点插入到父节点里面
insert(parentElm, vnode.elm, refElm);
} else {
// 文本节点
vnode.el = document.createTextNode(text);
}
return vnode.el;
}
function createChildren(vnode, children, insertedVnodeQueue) {
for (var i = 0; i < children.length; ++i) {
// 遍历子节点,递归调用创建真实dom节点的方法 - createElm
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
}
}
- patch过程如果遇到子Vnode,会优先实例化子组件,并且执行子组件的挂载流程,而挂载流程又会回到_render,_update的过程。在所有的子Vnode递归挂载后,最终才会真正挂载根节点
建立组件联系
- Vue在组件和组件之间建立了一层关联
在初始化实例阶段调用initLifecycle
function initLifecycle (vm) {
var options = vm.$options;
// 子组件注册时,会把父组件的实例挂载到自身选项的parent上
var parent = options.parent;
// 如果是子组件,并且该组件不是抽象组件时,将该组件的实例添加到父组件的$parent属性上
//如果父组件是抽象组件,则一直往上层寻找,直到该父级组件不是抽象组件,并将,将该组件的实例添加到父组件的$parent属性
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent;
}
parent.$children.push(vm);
}
// 将自身的$parent属性指向父实例。
vm.$parent = parent;
vm.$root = parent ? parent.$root : vm;
vm.$children = [];
vm.$refs = {};
vm._watcher = null;
vm._inactive = null;
vm._directInactive = false;
// 该实例是否挂载
vm._isMounted = false;
// 该实例是否被销毁
vm._isDestroyed = false;
// 该实例是否正在被销毁
vm._isBeingDestroyed = false;
}
- 在vue中有很多内置的抽象组件,例如<keep-alive>,<slot>等,这些抽象组件并不会出现在子父级的路径上,并且它们也不会参与DOM的渲染。
总结
- 全局组件需要进行全局注册,核心方法是Vue.component,他需要在根组件实例化前进行声明注册,原因是我们需要在实例化前拿到组件的配置信息并合并到options.components选项中。注册的本质是调用extend创建一个子类构造器,全局和局部的不同是局部创建子类构造器是发生在创建子组件Vnode阶段的createComponent中
- 而创建子Vnode阶段最关键的一步是定义了很多内部使用的钩子
- 有了一个完整的Vnode tree接下来会进入真正DOM的生成,在这个阶段如果遇到子组件Vnode会进行子构造器的实例化,并完成子组件的挂载
- 递归完成子组件的挂载后,最终才又回到根组件的挂载。