Omi框架学习之旅 - 组件 及原理说明

hello world demo看完后其实基本的写法就会了。

但是omi中的组件是神马鬼?其实我也不知道组件是啥。

百度百科是这么说的: 是对数据和方法的简单封装。es6中,一个类其实也可以做到对方法和数据的封装。然后new出来的实例共享原型上的方法,至于属性最好不要共享啦,

如果需要共享,自己写静态属性,或者Object.assign到原型上去。这里有点扯远了。

我的理解是一个组件就是一个类,至于组件嵌套,其实就是父类和子类,无非就是挂载到对应的属性下

(父类会主动帮我们自动的new 子类(被嵌套的组件)的实例并且添加些相应的属性,然后和父组件中render中的html合并一下)。

接下来看看一个组件的demo。

老规矩:先上demo代码, 然后提出问题, 之后解答问题, 最后源码说明。

        // 组件嵌套抽出List
        class List extends Omi.Component {
            constructor(data) {
                super(data);
            }

            render() {
                return `
                    <ul>
                        {{#items}}
                            <li>
                                {{.}}
                            </li>
                        {{/items}}
                    </ul>
                `;
            }
        };

        Omi.makeHTML('List2', List);    // 使用Omi.makeHTML把List类制作成可以声明式的标签List2,在render方法中就能直接使用该标签

        class Todo extends Omi.Component {
            constructor(data) {
                super(data);
                this.data.length = this.data.items.length;    // 给data添加个length属性
                this.listData = {items: this.data.items};    // listData属性名和下面的data="listData"中的listData对应
            }

            style() {
                return `
                    h3 {
                        color: red;
                    }
                    button {
                        color: green;
                    }
                `;
            }

            handleChange(target, evt) {
                this.data.text = target.value;    
                console.log(this.data.text);
            }

            add(evt) {
                evt.preventDefault();
                this.instance_list.data.items.push(this.data.text);    // this.instance_list这个其实就是下面name="instance_list"的instance_list(他其实就是List类的实例)
                this.data.length = this.listData.items.length;    // 跟新属性
                this.data.text = '';
                this.update();
                console.log(this.data);
                console.log(this.instance_list.data);    // this.instance_list中的data属性其实是一级浅拷贝this.listData这个的
                console.log(this.listData);
            }

            render() {
                return `
                    <div>
                        <h3>TODO</h3>
                        <List2 name="instance_list" data="listData"></List2>    <!--name 对应List2标签对应的List类的实例,  data的listData属性其实浅拷贝到instance_list实例的data上-->
                        <form>
                            <input type="text" onchange="handleChange(this, event)" value="{{text}}" />
                            <button onclick="add(event)">add #{{length}}</button>
                        </form>
                    </div>
                `;
            }
        };

        var todo = new Todo({
            items: [1, 2],
            text: ''
        });
        Omi.render(todo, '#app');
        console.log(todo.instance_list);

先看看omi中文文档的说明:

额,因为demo源码是我自己敲的,有稍微变化,所以说明就挑重要的说明,

 通过makeHTML方法把组件制作成可以在render中使用的标签。使用Omi.makeHTML('List2', List);即可
 在父组件上定义listData属性用来传递给子组件。
 在render方法中使用List2组件。
      其中name方法可以让你在代码里通过this快速方法到该组件的实例。
      data="listData"可以让你把this.listData传递给子组件。
  需要注意的是,父组件的this.listData会被通过Object.assign浅拷贝到子组件。
  这样做的目的主要是希望以后DOM的变更都尽量修改子组件自身的data,然后再调用其update方法,而不是去更改父组件的listData。

  文档地址https://alloyteam.github.io/omi/website/docs-cn.html#

 

接下来说说这个demo的疑问和疑问的说明:

疑问1:
Omi.makeHTML('List2', List);这个语句是干啥的,参数类型分别是啥?

答: 这是Omi对象的一个静态方法,作用是把类制作成可以声明式的标签。

       这么说似乎有点难懂。
       简单的说就是一个名字对应一个构造函数,也就是键值对。是存放在Omi.componetConstructor这个对象上的。
       并且也把标签名存到Omi.customTags这个数组中。

       源码感受: 

    Omi.makeHTML= function(name, ctor) {    // name: 组件标签名, ctor: 构造函数也就是类啦
        Omi.componetConstructor[name] = ctor;    // 把标签名对应类放到componetConstructor对象中去
        Omi.customTags.push(name);    // 自定义标签
    };

      那这么做的目的就是就是当我们使用omi.render的时候,他会自动帮我们new List实例然后合并父组件中的html。(这里当然是循环遍历每个孩子标签啦)。继续往下看。

 

疑问2:
我的List2标签制作好了,该怎么用到其他组件中去呢?

标签是使用单标签还是双标签呢?

答:可以在任意一个组件类的render方法中使用制作好的List2标签。可以如下使用:

<List2 name="instance_list" data="listData"></List2>    双标签
//或者 <List2 name="instance_list" data="listData" /> 单标签

 

     这里面的 name="instance_list" data="listData" 又是神马鬼啊,其实是这样的

     属性name对应的值instance_list其实就是 new List()的实例

     属性data对应的值listData就是被一级浅拷贝到instance_list实例上的data上了。

     那么的话,我操作数据的话,可以操作instance_list上的data数据然后instance_list.update()即可(原作者推荐),其实我们也可以在父类上操作listData属性然后this.update(),数据就更新了。

     哇,这么牛逼,怎么做到的呢?继续往下看

 

 疑问3:
上面的
<List2 name="instance_list" data="listData"></List2>这个标签里面的属性是不是涉及到组件通讯了啊?

答:是的,组件通讯有4种,加上一个终极通讯模式(上帝模式),后续会讲解的。

     本demo的通讯其实通过List2标签上的data属性值listData来实现通讯的。

 

疑问4:

这些都写好了,那怎么把<List2 name="instance_list" data="listData"></List2> 这个标签转换成如下这个标签呢?

 

答:恩,原理其实很简单撒,就是先把4种通讯方式中的数据合并,然后实例化 List2标签对应的的类,然后得到实例,之后生成局部css和html,然后替换这个标签,在之后把内置事件

     对应起类中的方法,然后插入到指定的dom中去就完了(组件嵌套组件然后各种嵌套,就变成了各种递归)。说起来很简单,真要是实现起来不容易啊,看看作者是怎么实现的。

     源码感受:

     从用户的代码中可以看到,omi.render方法是主角或者是入口吧。

    Omi.render = function(component , renderTo , incrementOrOption){    // 实例, 渲染到的dom, xx
        component.renderTo = typeof renderTo === "string" ? document.querySelector(renderTo) : renderTo;    // 实例的renderTo属性
        if (typeof incrementOrOption === 'boolean') {
            component._omi_increment = incrementOrOption;    // 实例的_omi_increment 属性(老版)
        } else if (incrementOrOption) {    // 新增
            component._omi_increment = incrementOrOption.increment;
            component.$store = incrementOrOption.store;
            if (component.$store) {
                component.$store.instances.push(component);
            };
            component._omi_autoStoreToData = incrementOrOption.autoStoreToData;
        };
        component.install();    // Component类的install方法(被实例继承了)
        component._render(true);    // Component类的_render方法(被实例继承了)
        component._childrenInstalled(component);    // 给每个实例的孩子执行installed方法
        component.installed();    // Component类的installed方法(被实例继承了)
        return component;    // 返回实例
    };

    这里面对于这个demo最主要的是component._render(true);这个方法,那我们进去看一看

    

    _render(isFirst) {
        if (this._omi_removed ) {    // 实例是否含有_omi_removed属性
            let node = this._createHiddenNode();
            if (!isFirst) {
                this.node.parentNode.replaceChild(node, this.node);
                this.node = node;
            } else if (this.renderTo) {
                this.renderTo.appendChild(node);
            };
            return;
        };
        if (this._omi_autoStoreToData) {    // 新增
            if(!this._omi_ignoreStoreData) {
                this.data = this.$store.data;
            };
        };
        this.storeToData();    // 调用实例的storeToData
        this._generateHTMLCSS();    // 生成 html 和 css 
        this._extractChildren(this);    // 提取孩子(就是提取嵌套标签啦)

        this.children.forEach((item, index) => {     // 遍历孩子
            this.HTML = this.HTML.replace(item._omiChildStr, this.children[index].HTML);    // 替换html中child的嵌套组件的html (把嵌套组件的html和父组件合并)
        });
        this.HTML =  scopedEvent(this.HTML, this.id);    // 把html中的事件函数转成实例对应的函数方法
        if (isFirst) {    // 是否第一次
            if (this.renderTo) {    // 渲染到的dom
                if (this._omi_increment) {  
                    this.renderTo.insertAdjacentHTML('beforeend', this.HTML);    //指定位置在插入
                } else {
                    this.renderTo.innerHTML = this.HTML;    // 把html插入到渲染dom中
                };
            };
        } else {
            if (this.HTML !== "") {    // 不是第一次插入就是用morphdom跟新节点
                morphdom(this.node, this.HTML);
            } else {
                morphdom(this.node ,this._createHiddenNode());
            };
        };
        // get node prop from parent node
        if (this.renderTo) {    // 有渲染到的节点
            this.node = document.querySelector("[" + this._omi_scoped_attr + "]");    // render()html字符串中的根节点
            this._queryElements(this);    // 查询dom元素
            this._fixForm();
        };
    }

这里面对于此demo最重要的是this._extractChildren(this);方法了,进去看看

    _extractChildren(child) {    // child: Component类的实例(一般是Component类子类的实例)
        if (Omi.customTags.length > 0) {    // 自定义标签名集合的长度大于0
            child.HTML = this._replaceTags(Omi.customTags, child.HTML);    // 返回 组件嵌套被替换成child开头的标签    看_replaceTags方法
        };
        let arr = child.HTML.match(/<child[^>][\s\S]*?tag=['|"](\S*)['|"][\s\S]*?><\/child>/g);    // 寻找child的标签放到数组中

        if(arr){
            arr.forEach( (childStr, i) =>{    // 遍历每一个child标签
                let json = html2json(childStr);    // 把标签转换成对象
                let attr = json.child[0].attr;    // 取出 child标签的所有属性
                let name = attr.tag;    // 组件标签名
                delete attr.tag;    // 删除 attr对象的tag属性(即删除了标签名, 省的循环)
                let cmi = this.children[i];    // 当前实例的第i的孩子
                //if not first time to invoke _extractChildren method(如果不是第一次调用_extractchildren方法)
                if (cmi && cmi.___omi_constructor_name === name) {    // 有孩子 且 孩子的函数名等于组件标签名
                    cmi._childRender(childStr);    // 实例渲染组件标签
                } else {
                    let baseData = {};    // 基础数据    (on-)
                    let dataset = {};    // 数据设置    (data-)
                    let dataFromParent = {};    // 从实例中获取标签属性data值对应的实例属性值    (data)
                    let groupData = {};    // 组数据    (group-data)
                    let omiID = null;    // omiId    (omi-id)
                    let instanceName = null;    // 标签名类的实例名    (name)
                    Object.keys(attr).forEach(key => {    // 遍历嵌套组件标签中的每一个属性
                        const value = attr[key];    // 属性值
                        if (key.indexOf('on') === 0) {    // 应该是事件
                            let handler = child[value];
                            if (handler) {
                                baseData[key] = handler.bind(child);
                            };
                        } else if (key === 'omi-id'){    // omi-id
                            omiID = value;
                        }else if (key === 'name'){    // name
                            instanceName = value;
                        }else if (key === 'group-data') {    // group-data
                            if (child._omiGroupDataCounter.hasOwnProperty(value)) {
                                child._omiGroupDataCounter[value]++;
                            } else {
                                child._omiGroupDataCounter[value] = 0;
                            };
                            groupData = this._extractPropertyFromString(value,child)[child._omiGroupDataCounter[value]];

                        } else if(key.indexOf('data-') === 0){    // 以data-开头的属性
                            dataset[this._capitalize(key.replace('data-', ''))] = value;
                        }else if(key === 'data'){    // data
                            dataFromParent =  this._extractPropertyFromString(value,child);    //获取在child上的value属性值
                        };
                    });

                    let ChildClass = Omi.getClassFromString(name);    // 根据标签名获取组件类 (name: 标签名)
                    if (!ChildClass) throw "Can't find Class called [" + name+"]";    // 没找到组件类
                    let sub_child = new ChildClass( Object.assign(baseData,child.childrenData[i],dataset,dataFromParent,groupData ),false);
                    sub_child._omiChildStr = childStr;    // 子组件的_omiChildStr属性 值为组件标签
                    sub_child.parent = child;    // 子组件的parent属性 值为子组件的父组件
                    sub_child.$store = child.$store;    // 存储数据
                    if(sub_child.$store){
                        sub_child.$store.instances.push(sub_child);
                    };
                    sub_child.___omi_constructor_name = name;    // 添加这个属性, 2310用到
                    sub_child._dataset = {};    // _dataset属性
                    sub_child.install();    // 子组件的install方法

                    omiID && (Omi.mapping[omiID] = sub_child);    // omi-id对应的值, 然后给omi的mapping对象添加omi-id对应的值:嵌套组件的实例
                    instanceName && (child[instanceName] = sub_child);    // 给实例添加name的值instanceName为属性 值为嵌套组件的实例

                    if (!cmi) {    // 没有cmi就把嵌套组件的实例添加到child.children中
                        child.children.push(sub_child);
                    } else {    // 否则替换
                        child.children[i] = sub_child;
                    };

                    sub_child._childRender(childStr,true);    // 嵌套组件渲染 参数(转换后的标签, true)
                };
            });
        };
    }

1. 那里就是帮我们自动实例化了。

2.这里我们进去看看

    _childRender(childStr,isFirst) {    //childStr: 转换后的标签  isFirst: true
        if (this._omi_removed ) {
            this.HTML = '<input type="hidden" omi_scoped_'+this.id+' >';
            return this.HTML;
        };
        //childStr = childStr.replace("<child", "<div").replace("/>", "></div>")
        this._mergeData(childStr);    // 数据合并
        if(this.parent._omi_autoStoreToData) {
            this._omi_autoStoreToData = true;
            if (!this._omi_ignoreStoreData) {
                this.data = this.$store.data;
            };
        };
        this.storeToData();
        this._generateHTMLCSS();    // 生成 html 和 css 
        this._extractChildren(this);    // 子组件提取孩子标签(就是提取嵌套标签啦)

        this.children.forEach((item, index) => {    // 遍历孩子
            this.HTML = this.HTML.replace(item._omiChildStr, this.children[index].HTML);    // 替换html中child的嵌套组件的html
        });
        this.HTML =  scopedEvent(this.HTML, this.id);    // 把html中的事件函数转成实例对应的函数方法
        return this.HTML;
    }

这里其实已经帮我们把List实例化的对象instance_list生成了css和html还有内置事件也绑定好了。那怎么和父组件合并呢?其实代码类似了

断点回到了_render方法中,见下图

在之后回到Omi.render方法中,

最后返回实例,前2个语句是组件的生命周期,后续再讲。

至此,组件的demo就讲完了。

 

ps:

   来句官网的话:绝大部分Web网页或者Web应用,需要嵌套定义的组件来完成所有的功能和展示,所以组件很重要。

    

posted @ 2017-03-21 18:05  Sorrow.X  阅读(5137)  评论(2编辑  收藏  举报