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

时间:2024-01-19 12:44:14

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> 这个标签转换成如下这个标签呢?

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

答:恩,原理其实很简单撒,就是先把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)
};
});
};
}

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

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框架学习之旅 - 组件 及原理说明

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

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

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

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

ps:

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