Skip to content
This repository has been archived by the owner on Nov 12, 2024. It is now read-only.

如何开发一个view #47

Open
xinglie opened this issue Jan 21, 2018 · 3 comments
Open

如何开发一个view #47

xinglie opened this issue Jan 21, 2018 · 3 comments

Comments

@xinglie
Copy link
Member

xinglie commented Jan 21, 2018

通过继承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
Copy link
Member Author

xinglie commented Feb 2, 2018

更高效的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
Copy link
Member Author

xinglie commented Feb 27, 2019

代码改善

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

@dt1109dt
Copy link

nb啊

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants