magix icon indicating copy to clipboard operation
magix copied to clipboard

如何开发一个view

Open xinglie opened this issue 7 years ago • 3 comments

通过继承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的步骤如下:

  1. 如果新旧节点一样(即同样的标签) [是2否13]
  2. 如果旧节点是一个组件所在的节点 [是3否11.12]
  3. 如果旧节点上一次渲染的innerHTML与本次渲染的一致 [是4否6]
  4. 如果旧节点上的组件上一次的数据与本次一致 [是5否6]
  5. 如果旧节点上的组件是不带模板(即附加行为型)的组件 [是6]
  6. 如果旧节点上的组件和新节点上的待渲染的组件一样(即同类型的组件),且有assign方法 [是7否9]
  7. 调用组件的assign方法,如果返回值为true则继续调用组件的render方法 [是8]
  8. 跳过更新
  9. 销毁旧组件
  10. 渲染新组件
  11. 更新节点属性
  12. 更新子节点
  13. 替换节点

流程中重要的一点是组件即view有没有assign方法,如果有该方法,则调用该方法把数据传递进去,组件在该方法内完成数据的更新,如果该方法的返回值是true,则再调用组件的render方法完成更新,从而不需要销毁组件

自己添加的属性被删除问题

因为是真实dom的比对,开发者如果不通过view提供的updater来更新界面,而是自己改变了dom节点上的属性,则界面在下次刷新时,这些自己添加的属性会被删除。目前的解决方案是最好不要自己操作dom,如果要操作dom,则写一个带有assign方法的组件来操作dom,因为magix遇到带有assign方法的组件所在的节点时,全权交与组件处理。

其它问题

dom比对的方式千万不要自己创建、删除任何节点!!!

xinglie avatar Jan 21 '18 14:01 xinglie

更高效的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();
    }
});

xinglie avatar Feb 02 '18 14:02 xinglie

代码改善

前面的过于复杂,也可用下面这段代码进行简化。外部数据有变化后通过viewassign方法通知到当前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();
    }
});

xinglie avatar Feb 27 '19 07:02 xinglie

nb啊

dt1109dt avatar Apr 20 '20 05:04 dt1109dt