magix
magix copied to clipboard
如何开发一个view
通过继承Magix.View来实现自己的view
通过Magix.View.extend方法来实现
let Magix = require('magix');
module.exports = Magix.View.extend({
tmpl:'@demo.html',
render(){
console.log('render ui')
}
});
view的生命周期
显式的init、render方法
每个view默认都有一个init(初始化时调用,只会调用一次)和render(需要更新界面时被调用,可能会调用多次)
let Magix = require('magix');
module.exports = Magix.View.extend({
tmpl:'@demo.html',
init(data){
console.log('init data',data);
},
render(){
console.log('render ui')
}
});
隐式的destroy,domready等事件接口
为了便于插件及监控的开发,每个view会在适当的时候派发这些事件
let Magix = require('magix');
module.exports = Magix.View.extend({
tmpl:'@demo.html',
init(data){
console.log('init data',data);
this.on('destroy',()=>{
console.log('view destroy');
});
this.on('domready',()=>{
console.log('ui ready');
});
},
render(){
console.log('render ui')
}
});
全局的htmlchanged事件
除了view自身派发的事件外,任意view对html的修改,均会通过document派发htmlchanged事件
view的使用及分类
自身完整型
我们以一个calendar组件为例来说明,先看使用时的html代码
<--我们通过自定义标签的形式来使用magix-gallery中的calendar组件-->
<mx-calendar
selected="2018-01-01"
mx-change="showDate()"/>
对于calendar组件来说,输入是定义好的类似selected这样的参数,输出则是自身日期有变化时,通过change事件向外派发数据。
在外部来看要想改变calendar组件,只能通过参数,没有其它的途径
calendar的实现来看(view组成的三大件:js、html、css,其中js是必有的),html也是要有的,因为这样才方便构建带有ui的组件
内部必有js及html,且只接收参数控制显示的我们把它定义为自身完整型组件
附加行为型
我们以一个拖动排序dragsort为例来说明,先看使用时的html代码
<ul mx-view="app/gallery/mx-dragsort/index" class="left fl" view-selector="span">
<li><span>move</span>123</li>
<li><span>move</span>456</li>
<li><span>move</span>123</li>
<li><span>move</span>456</li>
<li><span>move</span>123</li>
<li><span>move</span>456</li>
<li><span>move</span>123</li>
<li><span>move</span>456</li>
</ul>
对于dragsort组件,它自身没有html,不向界面输出任何的html,而是利用页面上原有的节点。如该示例中,dragsort组件并不关心dom节点是什么,处于该节点下的所有子节点均可以被拖动
内部没有html,且对现有dom节点做增强的组件,我们把它定义为附加行为型组件
混合型
一些组件如dropdown,即接收一个list参数来表示要渲染的下拉列表,同时也可以读取已渲染的dom节点来做为下拉框显示的数据源,如
<mx-dropdown
searchbox="true"
empty-text="请选择用户"
text-key="name"
value-key="id"
selected="<%@ userSelected %>"
list="<%@ userList %>"
mx-change="showUser()"
class="fl" style="width:200px;">
</mx-dropdown>
或
<mx-dropdown
searchbox="true"
empty-text="请选择日期"
mx-change="showWeek()"
class="fl" style="width:150px;">
<mx-dropdown.item value="mon">周一</mx-dropdown.item>
<mx-dropdown.item value="wed">周三</mx-dropdown.item>
<mx-dropdown.item value="thu">周四</mx-dropdown.item>
<mx-dropdown.item value="fri">周五</mx-dropdown.item>
<mx-dropdown.item value="sat">周六</mx-dropdown.item>
</mx-dropdown>
差异化更新
当数据有变化,界面也要更新时,magix目前有2种差异化更新的实现方式
html片断拆分的形式
该方式需要配合magix-combine工具对开发者编写的html模板做线下分析,自动把模板拆分成n多子模板片断,每个片断关联着对应的数据key,当数据有变化时,会找到相应的子模板片断,最小化的更新界面
如模板
<div>
<%for(let i=0;i<list.length;i++){%>
<span><%=list[i]%></span>
<%}%>
</div>
<div>
<%for(let i=0;i<list1.length;i++){%>
<span><%=list1[i]%></span>
<%}%>
</div>
会被处理成这样的片断对象
{
"html": "<div mx-guid=\"g0\u001f\">1\u001d</div><div mx-guid=\"g1\u001f\">2\u001d</div>",
"subs": [{
"keys": ["list"],
"path": "div[mx-guid=\"g0\u001f\"]",
"tmpl": "<%for(let _=0;_<$$.list.length;_++){%><span><%=$$.list[_]%></span><%}%>",
"s": "1\u001d"
}, {
"keys": ["list1"],
"path": "div[mx-guid=\"g1\u001f\"]",
"tmpl": "<%for(let a=0;a<$$.list1.length;a++){%><span><%=$$.list1[a]%></span><%}%>",
"s": "2\u001d"
}]
}
这样如果只有list这个数据发生变化时,只有第一个div会被重新渲染 这种方式受dom结构的影响,无法再做进一步的细粒度的拆分,有时候刷新区域仍然较大
真实dom节点比对更新
主流的前端开发框架都有虚拟dom,通过虚拟dom比对前后的差异变化,从而最小化的更新界面。
因为magix一直使用的是字符串模板,如果直接转成虚拟dom的形式,要想性能最优,jsx是最理想的方式(通过工具直接把类似字符串的形式转成方法的调用)。这样一来开发者需要做很大的转变,再一个目前也没有人和精力来做这个事情。
关于dom diff网上的方案非常多,虚拟和虚拟的,虚拟和真实的,真实和真实等。考虑到成本问题,目前采用的是真实dom与真实dom的对比(1.不需要考虑浏览器兼容2.不需要做转换3.不需要自己实现),只做好diff即可。https://github.com/patrick-steele-idem/morphdom
差异化更新与组件的问题
组件销毁、重建问题
考虑这样的html代码
<mx-calendar
selected="<%=currentDate%>"
mx-change="showDate()"/>
mx-calendar组件的日期选中受currentDate的控制,如果第一次currentDate是2018-01-01,数据变化后currentDate是2018-04-01
magix在差异化更新时,由于组件所在的节点是一个特殊节点,比较到该节点时,因为不知道组件会如何变化,所以会销毁旧组件,更新完dom节点后,再实例化新组件。
这样一来因为一个小小的数据变化,需要销毁旧组件,再渲染新组件,显得比较笨重了。
magix中节点diff的步骤如下:
- 如果新旧节点一样(即同样的标签) [是2否13]
- 如果旧节点是一个组件所在的节点 [是3否11.12]
- 如果旧节点上一次渲染的innerHTML与本次渲染的一致 [是4否6]
- 如果旧节点上的组件上一次的数据与本次一致 [是5否6]
- 如果旧节点上的组件是不带模板(即附加行为型)的组件 [是6]
- 如果旧节点上的组件和新节点上的待渲染的组件一样(即同类型的组件),且有assign方法 [是7否9]
- 调用组件的assign方法,如果返回值为true则继续调用组件的render方法 [是8]
- 跳过更新
- 销毁旧组件
- 渲染新组件
- 更新节点属性
- 更新子节点
- 替换节点
流程中重要的一点是组件即view有没有assign方法,如果有该方法,则调用该方法把数据传递进去,组件在该方法内完成数据的更新,如果该方法的返回值是true,则再调用组件的render方法完成更新,从而不需要销毁组件
自己添加的属性被删除问题
因为是真实dom的比对,开发者如果不通过view提供的updater来更新界面,而是自己改变了dom节点上的属性,则界面在下次刷新时,这些自己添加的属性会被删除。目前的解决方案是最好不要自己操作dom,如果要操作dom,则写一个带有assign方法的组件来操作dom,因为magix遇到带有assign方法的组件所在的节点时,全权交与组件处理。
其它问题
dom比对的方式千万不要自己创建、删除任何节点!!!
更高效的view更新
从3.8版本之后加入了dom比对,虽然粒度更细,但性能上并不乐观.如果能从组件的角度通过数据来识别是否需要更新,则能大幅提升性能,基于这个前提,现给出书写view的通用模板
let Magix = require('magix');
module.exports = Magix.View.extend({
tmpl: '@view.html',
init(extra) {
//初始化时保存一份当前数据的快照
this.updater.snapshot();
//该处是否可以由magix自动调用
this.assign(extra);
},
assign(data) {
let me = this;
//赋值前先进行数据变化的检测,首次assign是在init方法中调用,后续的调用是magix自动调用,这个检测主要用于在首次调用后,magix自动调用前有没有进行数据的更新
let altered = me.updater.altered();
//你可以在这里对数据data进行加工,然后通过set方法放入到updater中
me.updater.set(data);
//如果数据没变化,则设置新的数据后再次检测
if (!altered) altered = me.updater.altered();
if (altered) {//如果有变化,则再保存当前的快照,然后返回true告诉magix当前view需要更新
me.updater.snapshot();
return true;
}
return false;//如果数据没变化,则告诉magix当前view不用更新
},
render() {
//view首次渲染及后续数据有变化时进行更新
console.log('render');
this.updater.digest();
}
});
代码改善
前面的过于复杂,也可用下面这段代码进行简化。外部数据有变化后通过
view
的assign
方法通知到当前view
后,view
内部不管数据有没有变化都更新
let Magix = require('magix');
module.exports = Magix.View.extend({
tmpl: '@view.html',
init(extra) {
this.assign(extra);
},
assign(data) {
let me = this;
//你可以在这里对数据data进行加工,然后通过set方法放入到updater中
me.updater.set(data);
//不管数据有没有变化都更新当前view
return true;
},
render() {
console.log('render');
this.updater.digest();
}
});
nb啊