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应用,需要嵌套定义的组件来完成所有的功能和展示,所以组件很重要。