vue2源码解析(十四):组件渲染流程
摘要
vue2源码学习之路。
查看之前的文章
上一篇文章中,分析了组件的初始化。
组件注册分为全局注册和局部注册,无论哪种方式,最终都会使用Vue.extend包装,转换成构造函数形式。
创建组件构造函数时,会将全局的组件和自己身上定义的组件进行合并。使用组件时,先查找自身,如果找不到,再使用沿着原型链查找全局。
页面渲染时,先将html转换成ast,然后生成虚拟dom,最后根据虚拟dom生成真实dom。
组件也是一样,接下来继续分析组件的渲染流程。
区分组件和原生标签
组件和标签来说,生成虚拟节点时是有区别的,所以要根据tag类型进行区分。
源码中采用了映射表的方式,如图
如果是原生标签,就返回true,否则返回undefined。
创建element.js文件,路径src/util/element.js
isReservedTag函数
export function isReservedTag(tag) {
return isHTMLTag(tag) || isSVG(tag);
}
isHTMLTag 函数
export const isHTMLTag = makeMap(
"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"
);
isSVG函数
export const isSVG = makeMap(
"svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font-face," +
"foreignobject,g,glyph,image,line,marker,mask,missing-glyph,path,pattern," +
"polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view",
true
);
makeMap函数
export function makeMap(str, expectsLowerCase) {
const map = Object.create(null);
const list = str.split(",");
for (let i = 0; i < list.length; i++) {
map[list[i]] = true;
}
return expectsLowerCase
? (val) => map[val.toLowerCase()]
: (val) => map[val];
}
在生成虚拟dom的函数中加入判断。
function createElementVNode(vm, tag, data, ...children) {
if (data == null) data = {};
let vnode;
if (isReservedTag(tag)) {
// 原生标签
vnode = new VNode(tag, data, children, undefined, undefined, vm);
} else {
// 组件
}
return vnode;
}
组件虚拟节点的创建
注册组件时,传入的选项可能存在对象和构造函数两种形式。
举例来说
Vue.component("global-component", {
template: `<div>全局组件</div>`,
});
const app = new Vue({
el: "#app",
components: {
test: {
template: `<div>局部组件</div>`,
},
},
});
无论哪种形式,最终都要使用Vue.extend包装,从而统一处理。
在组件初始化时,已经将组件的所有选项合并到了Vue.options.components
这个属性里。
因此,通过组件名称就可以获取到组件对应的选项。
创建resolveAsset函数,获取组件配置项。
export function resolveAsset(options, type, id) {
const asset = options[type];
const res = asset[id];
return res;
}
createElementVNode函数
function createElementVNode(vm, tag, data, ...children) {
if (data == null) data = {};
let vnode;
if (isReservedTag(tag)) {
vnode = new VNode(tag, data, children, undefined, undefined, vm);
} else {
// 组件
let Ctor = resolveAsset(vm.$options, "components", tag);
vnode = createComponent(Ctor, data, vm, children, tag);
}
return vnode;
}
创建createComponent函数,这个函数的功能就是生成组件虚拟节点。
组件的虚拟节点和原生标签的虚拟节点有两点不同,一是增加了生命周期钩子,二是增加了componenntOptions属性,这个属性专门用来保存组件的构造函数。
首先将组件的配置项转成构造函数形式,然后给组件添加生命周期钩子,最后,根据组件名、属性等参数生成虚拟节点。
export function createComponent(Ctor, data, context, children, tag) {
const baseCtor = context.$options._base;
data = data || {};
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}
// 给组件增加生命周期
installComponentHooks(data);
const name = getComponentName(Ctor.options) || tag;
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ""}`,
data,
undefined,
undefined,
undefined,
context,
{
Ctor,
children,
tag,
}
);
return vnode;
}
组件没有children属性,所以children是undefined。
组件的子节点以slot插槽的形式出现。
VNode类
路径src/vdom/vnode.js
export default class VNode {
constructor(tag, data, children, text, elm, context, componentOptions) {
this.tag = tag;
this.data = data;
this.children = children;
this.text = text;
this.elm = elm;
this.context = context;
this.key = data && data.key;
this.componentOptions = componentOptions;
}
}
组件上有四个生命周期钩子:组件初始化时调用init钩子、组件节点对比时调用prepatch钩子、插入节点时调用insert钩子、销毁时调用destory钩子。
创建installComponentHooks函数,这个函数中做了一些合并和去重处理。
function installComponentHooks(data) {
const hooks = data.hook || (data.hook = {});
for (let i = 0; i < hooksMerge.length; i++) {
const key = hooksMerge[i];
const existing = hooks[key];
const toMerge = componentVNodeHooks[key];
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge;
}
}
}
mergeHook函数
function mergeHook(f1, f2) {
const merged = (a, b) => {
f1(a, b);
f2(a, b);
};
merged._merged = true;
return merged;
}
hooksMerge
const hooksMerge = Object.keys(componentVNodeHooks);
创建componentVNodeHooks函数,这个函数定义了四个钩子,这里暂时只实现init。
在组件初始化时,会调用init,init方法内部通过new关键字调用组件的构造函数,然后将生成的实例保存在vnode上。
最后,执行$mount
函数进行挂载。
注意,这里的$mount
是不传参数的。
const componentVNodeHooks = {
init(vnode) {
const child = (vnode.componentInstance =
new vnode.componentOptions.Ctor({}));
child.$mount();
},
};
组件真实节点的生成
执行init,vnode.componentInstance
上面就会有一个$el属性,代表组件的真实dom。
所以在页面渲染过程中,只需要在适当的时候调用init即可。
创建createComponentElm函数,用来执行init。
生成真实dom后,返回一个标识true,用来做判断。
function createComponentElm(vnode) {
let i = vnode.data;
// 取出hook中的init
if ((i = i.hook) && (i = i.init)) {
i(vnode);
}
if (vnode.componentInstance) {
return true;
}
}
找到之前写的createElm函数,添加判断条件,如果是组件,就直接返回vnode.componentInstance.$el
。
function createElm(vnode) {
let { tag, data, children, text } = vnode;
if (typeof tag === "string") {
// 还有一种可能是组件
if (createComponentElm(vnode)) {
return vnode.componentInstance.$el;
}
// 标签
vnode.el = document.createElement(tag);
patchProps(vnode.el, {}, data);
children.forEach((element) => {
// 将子元素也处理成真实节点
vnode.el.appendChild(createElm(element));
});
} else {
// 文本
vnode.el = document.createTextNode(String(text.text));
}
return vnode.el;
}
patch函数中也添加一个判断,oldVnode是undefined,说明是组件,直接根据vnode创建节点即可。
export function patch(oldVnode, vnode) {
if (isUndef(oldVnode)) {
return createElm(vnode);
} else {
...
}
}
至此,组件的渲染过程完成。
总结
总结一下整个组件的实例化过程。
组件分为局部注册和全局注册,无论哪种形式,最终都会转换成构造函数形式。
两种注册方式内部都使用了Vue.extend,这个函数的核心是创建一个子类来继承父类。
创建子类实例时,会调用父类(Vue)上的_init方法。
组件定义了四个生命周期钩子,初始化时调用init钩子,用new关键字调用这个组件的构造函数,然后使用$mount挂载。
创建虚拟节点时,根据标签区分出原生标签和组件,生成组件的虚拟节点。
组件创建真实节点时,遇到组件的虚拟节点,就调用组件的init方法,将组件的真实节点保存到vnode.componentInstance.$el属性中。
完整代码,请移步gihub vue2-source
文章就到这里,下次再见!
转载自:https://juejin.cn/post/7172883672905809950