vue2源码解析(十四):组件渲染流程


theme: channing-cyan

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天,点击查看活动详情

摘要

vue2源码学习之路。

查看之前的文章

专栏:vue2源码解析

上一篇文章中,分析了组件的初始化。

组件注册分为全局注册和局部注册,无论哪种方式,最终都会使用Vue.extend包装,转换成构造函数形式。

创建组件构造函数时,会将全局的组件和自己身上定义的组件进行合并。使用组件时,先查找自身,如果找不到,再使用沿着原型链查找全局。

页面渲染时,先将html转换成ast,然后生成虚拟dom,最后根据虚拟dom生成真实dom。

组件也是一样,接下来继续分析组件的渲染流程。

区分组件和原生标签

组件和标签来说,生成虚拟节点时是有区别的,所以要根据tag类型进行区分。

源码中采用了映射表的方式,如图

image.png

如果是原生标签,就返回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>`,
        },
    },
});

image.png

无论哪种形式,最终都要使用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钩子。

image.png

创建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


文章就到这里,下次再见!

© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容