深入浅出MV*框架源码(六):Moon中的组件

前言

组件化是前端生产力提升的另一大变革,之前jq时代大家都是一份代码复制来复制去,对代码复用性和可维护性非常不友好。现在MV*框架全部是支持组件的,只不过使用上略有区别。

Component

首先,让我们注册一个具有template、props和自定义事件的组件:

Moon.component("my-component", {
    // options
    template: `<h3>This is a Component {{content}} 计数器{{count}}! 
    <button m-on:click='increment'>计数器</button></h3>`,
    props: ['content'],
    data: function() {
        return {
            count: 0
        }
    },
    methods: {
        increment: function() {
            this.set("count", this.get("count") + 1);
            this.emit("increment");
        }
    }
});

它是一个计数器组件,拥有content这个从父组件传来的prop,也有increment这个事件可以发射到父组件以便相互通信。
让我们打个断点看看里面发生了什么:


component.jpg

可以看出,我们传了name和options两个参数用于注册组件。首先,初始化了组件的options,然后让MoonComponent继承Moon,并重写了init方法(主要是为了处理props数据)。
之后把MoonComponent和options挂载到全局的components[name]上,返回MoonComponent构造函数。

html->code过程与组件

组件既然是继承自Moon实例,自然也少不了html->tokens->ast->code->vnode->node->html的过程。拿我们这个例子来说也是一样:

<my-component content="'父组件内容'" m-on:increment="incrementTotal"></my-component>

这段html自然也会先被转成code。只不过组件对于编译过程中来说暂时还没有什么特别的。

code->html与组件

组件html被稀里糊涂转成code了,之后解析code的过程就变得麻烦许多。

第一个与之相遇的就是m函数:


m-component.jpg

可见它会先判断此组件是否是函数式组件,是的话就按函数式的方法返回一段函数式组件vnode,不是的话先挂载到meta上去再创建vnode。

函数式组件

函数式组件没有任何状态,也没有生命周期方法。它只是一个接收参数的函数。创建函数式组件的代码其实很好懂:

var createFunctionalComponent = function(props, children, functionalComponent) {
    var options = functionalComponent.options;
    var attrs = props.attrs;
    var data = options.data;

    if (data === undefined) {
        data = {};
    }

    // Merge data with provided props
    var propNames = options.props;
    if (propNames === undefined) {
        data = attrs;
    } else {
        for (var i = 0; i < propNames.length; i++) {
            var prop = propNames[i];
            data[prop] = attrs[prop];
        }
    }

    // Call render function
    return functionalComponent.options.render(m, {
        data: data,
        slots: getSlots(children)
    });
}

它其实就是将一个拥有attrs(props会被合并进去)和data的组件直接render,至于slot,我们稍后再讲。

createComponentFromVNode

它区别于createNodeFromVNode,专门用于产生组件vnode的node:


createComponentFromVNode.jpg

可以看出,它分四步走:

  1. 合并prop到data属性上去
  2. 扩展vnode的事件监听到组件实例上去
  3. 获取slot和实际挂载元素并build产生node
  4. 混入vnode和node然后返回实际node

appendChild/removeChild/replaceChild

在这期间它可能还会遇见一些vnode层面上的增/删/替操作:

// appendChild Check for Component
var component = null;
if ((component = vnode.meta.component) !== undefined) {
    createComponentFromVNode(node, vnode, component);
}

// removeChild Check for Component
var componentInstance = null;
if ((componentInstance = node.__moon__) !== undefined) {
    // Component was unmounted, destroy it here
    componentInstance.destroy();
}

// Check for Component
var componentInstance = null;
if ((componentInstance = oldNode.__moon__) !== undefined) {
    // Component was unmounted, destroy it here
    componentInstance.destroy();
}
// Replace It
parent.replaceChild(newNode, oldNode);
// replaceChild Check for Component
var component = null;
if ((component = vnode.meta.component) !== undefined) {
    createComponentFromVNode(newNode, vnode, component);
}

增的情况最简单,如果有组件就创建一个组件的node,删的情况则是有组件就销毁掉。替的话则是结合了增和删的情况,先删后增。

hydrate

在build->patch的最后几步,还会遇到hydrate里处理组件的代码:

// Check for Component
if (vnode.meta.component !== undefined) {
    // Diff the Component
    diffComponent(node, vnode);

    // Skip diffing any children
    return node;
}

也就是diff组件

diffComponent

diff组件做的事情也很简单:


diffComponent.jpg

若当前node没有被挂载,直接创建一个node。否则获取当前组件实例,对node的props和vnode的attrs进行diff,diff成功就标记componentChanged标志位为真并在最后build一次这个实例。
还有一件事情要处理:如果vnode还有子元素,就要给组件实例加上slot。

slot与getSlots

slot是组件内嵌的元素,思想来源于shadow dom
Moon中关于slot的关键函数式getSlots:

var getSlots = function(children) {
    var slots = {};

    // Setup default slots
    var defaultSlotName = "default";
    slots[defaultSlotName] = [];

    // No Children Means No Slots
    if (children.length === 0) {
        return slots;
    }

    // Get rest of the slots
    for (var i = 0; i < children.length; i++) {
        var child = children[i];
        var childProps = child.props.attrs;
        var slotName = "";
        var slotValue = null;

        if ((slotName = childProps.slot) !== undefined) {
            slotValue = slots[slotName];
            if (slotValue === undefined) {
                slots[slotName] = [child];
            } else {
                slotValue.push(child);
            }
            delete childProps.slot;
        } else {
            slots[defaultSlotName].push(child);
        }
    }

    return slots;
}

可以发现它传入的是children参数,先会建立一个默认的slots,然后从children里取每个child,如果slot是不是具名slot就放进默认slot里。

getSlots使用处

  1. createFunctionalComponent
return functionalComponent.options.render(m, {
    data: data,
    slots: getSlots(children)
});
创建函数式组件的时候slot会被提取出来放到创建组件的options里去。
  1. createComponentFromVNode
componentInstance.$slots = getSlots(vnode.children);

类似创建函数式组件,创建实例组件也是把slot提取出来挂载到实例的$slots属性上。

  1. diffComponent
// If it has children, resolve any new slots
if (vnode.children.length !== 0) {
    componentInstance.$slots = getSlots(vnode.children);
    componentChanged = true;
}

diff的过程中也会修改组件实例,可以发现与createComponentFromVNode类似,是把slot提取出来挂载到实例的$slots属性上。

  1. generateNode
else if (node.type === "slot") {
    parent.meta.shouldRender = true;
    parent.deep = true;

    var slotName = node.props.name;
    return ("instance.$slots[\"" + (slotName === undefined ? "default" : slotName.value) + "\"]");
} 

generate的过程中也会处理slot,处理方式自然是把它变成code了。

总结

组件化对我们前端开发工程化、提高代码复用性和可维护性起到了革命式的作用,从此各种UI组件库如雨后春笋似的冒了出来,对我们前端解放生产力来说是非常大的一个进步。相信有一天,HTML5会支持原生定义组件和Slot。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容