手记

【单页应用】view与model相关梳理

前情回顾

根据之前的学习,我们形成了一个view与一个messageCenter
view这块来说又内建了一套mvc的东西,我们这里来理一下
首先View一层由三部分组成:
① view
② dataAdpter
③ viewController

view一块两个重要数据是模板以及对应data,一个状态机status
这里view只负责根据状态取出对应的模板,而后根据传入的数据返回组装好的html
这里一个view多种状态是什么意思呢?
比如我有一个组件,但是里面有一圈数据是需要Ajax请求的,所以我的view可能就分为两个状态了
init->ajaxSuccess 这样的话首次加载默认的dom结构,数据加载结束后便再次渲染
PS:这里再次渲染的时候暂时图方便是采用将整个DOM结构换掉的手法,虽然简单粗暴却不合适,这块后期优化

这里数据的变化不由view负责,负责他的是dataAdapter
dataAdpter属于一个独立的模块,可用与多个viewController,dataAdpter内部首先维护着一个观察者数组,
然后是两个关键的datamodel以及viewmodel
datamodel用于操作,viewmodel会根据datamodel生成最终,然后使用viewmodel进行页面render,这个就是传入的data
若是我某一个datamodel对象发生变化便会通知观察者们,然后对应的view就会得到更新,该过程的发生点控制于viewController

viewController是连接view与dataAdpter的枢纽
viewController必须具有view,却可以没有dataAdpter,因为不是所有view都需要data才能渲染
我们实际工作中的大量业务逻辑会在viewController中定义完成,然后viewController也分了几个事件点
① create 触发onViewBeforeCreate、onViewAfterCreate事件
② show会实际将dom结构转入并且显示出来 触发onViewBeforeShow、onViewAfterShow事件
show的时候会绑定相关事件,事件借鉴于Backbone事件机制,每次注册前会先移除
③ 而后便是hide事件,他会隐藏我们的dom却不会移除,对应会有onViewBeforeHide、onViewAfterHide
④ destroy事件,会移除dom结构,并且删除实例、释放自身资源
以上是主流功能,还有一些功能不一定常用,比如我们任务view隐藏后,其所有状态事件全部应该移除,在show时重新绑定

messageCenter

现在没有什么大问题,却有一个小隐忧,这个消息中心会全局分发,一旦注册后,在触发时皆会触发,这个就有一个问题
我有一个alert组件,我自己内部在初始化时候注册了一个onShow的事件,我在show的时候真正的执行之
这个看上去没有什么问题,但是以下场景会有不一样的感受
我一个页面上有两个alert实例的话,我调用其中一个的时候,另一个alert的onShow也会被触发,这个是我们不愿意看见的
换个例子,我们一个页面上有两个IScroll,我们如使用messageCenter的话,一个滑动结束触发对应键值事件,很有可能两边会同时被触发
所以,这些都是我们需要关注的问题
下面让我们来详细整理

View相关梳理

现在View相关的功能点还不完全成熟,主要纠结点在于modelView改变后,view应该作何反应
若是一小点数据的改变却会引起整个dom结构的重组,这一点也是致命的,
其次一个view不同的状态会组成不同的view,但是一个view组成的html应该有一个容器,此“容器”现阶段我们概念感不是很强
所谓容器,不过是有模板嵌套的场景,后加载出来的html需要放入之前的某一个位置
若是子模板改变只会改变对应部分的dom、若是主模板改变就只能全部dom重组了!!!

于是我们简单整理后的代码如下:

首先来看看我们的view

  1 Dalmatian.View = _.inherit({  2   3   // @description 设置默认属性  4   _initialize: function () {  5   6     var DEFAULT_CONTAINER_TEMPLATE = '<div class="view"></div>';  7     var VIEW_ID = 'dalmatian-view-';  8   9     // @override 10     // @description template集合,根据status做template的map 11     // @example 12     /* 13     { 14     init: '<ul><%_.each(list, function(item){%><li><%=item.name%></li><%});%></ul>'//若是字符串表明全局性 15     ajaxLoading: 'loading', 16     ajaxSuc: 'success' 17     } 18     */ 19     this.templateSet = {}; 20  21     // @override 22     /* 23     ***这块我没有考虑清楚,一般情况下view是不需要在改变的,若是需要改变其实该设置到datamodel中*** 24     这个可以考虑默认由viewController注入给dataModel,然后后面就可操作了...... 25     这里的包裹器可能存在一定时序关系,这块后续再整理 26  27     与模板做映射关系,每个状态的模板对象可能对应一个容器,默认为根容器,后期可能会被修改 28     ajaxLoading: el, 29     ajaxSuc: selector 30     */ 31     this.wrapperSet = {}; 32  33     this.viewid = _.uniqueId(VIEW_ID); 34     this.currentStatus = null; 35     this.defaultContainer = DEFAULT_CONTAINER_TEMPLATE; 36     this.isNoWrapper = false; 37  38     //全局根元素 39     this.root = null; 40     //当前包裹器 41     this.curWrapper = null; 42     //当前模板对应解析后的html结构 43  44   }, 45  46   _initRoot: function () { 47     //根据html生成的dom包装对象 48     //有一种场景是用户的view本身就是一个只有一个包裹器的结构,他不想要多余的包裹器 49     if (!this.isNoWrapper) { 50       this.root = $(this.defaultContainer); 51       this.root.attr('id', this.viewid); 52     } 53   }, 54  55   // @description 构造函数入口 56   initialize: function (options) { 57     this._initialize(); 58     this.handleOptions(options); 59     this._initRoot(); 60  61   }, 62  63   // @override 64   // @description 操作构造函数传入操作 65   handleOptions: function (options) { 66     // @description 从形参中获取key和value绑定在this上 67     // l_wang options可能不是纯净的对象,而是函数什么的,这样需要注意 68     if (_.isObject(options)) _.extend(this, options); 69  70   }, 71  72   //处理包裹器,暂时不予理睬 73   _handleNoWrapper: function (html) { 74     //...不予理睬 75   }, 76  77   //根据状态值获取当前包裹器 78   _getCurWrapper: function (status, data) { 79     //处理root不存在的情况 80     this._handleNoWrapper(); 81  82     //若是以下逻辑无用,那么这个便是根元素 83     if (!data.wrapperSet || !data.wrapperSet[status]) { return this.root; } 84     if (_.isString(data.wrapperSet[status])) { return this.root.find(data.wrapperSet[status]); } 85  86   }, 87  88   // @description 通过模板和数据渲染具体的View 89   // @param status {enum} View的状态参数 90   // @param data {object} 匹配View的数据格式的具体数据 91   // @param callback {functiion} 执行完成之后的回调 92   render: function (status, data, callback) { 93  94     var templateFn, wrapper; 95     var template = this.templateSet[status]; 96  97     //默认将view中设置的默认wrapper注入值datamodel,datamodel会带入viewModel 98     wrapper = this._getCurWrapper(status, data); 99 100     if (!wrapper[0]) throw '包裹器参数错误';101     if (!template) return false;102 103     //解析当前状态模板,编译成函数104     templateFn = Dalmatian.template(template);105     wrapper.html(templateFn(data));106     this.html = wrapper;107 108     this.currentStatus = status;109 110     _.callmethod(callback, this);111     return true;112 113   },114 115   // @override116   // @description 可以被复写,当status和data分别发生变化时候117   // @param status {enum} view的状态值118   // @param data {object} viewmodel的数据119   update: function (status, data) {120 121     if (!this.currentStatus || this.currentStatus !== status) {122       return this.render(status, data);123     }124 125     // @override126     // @description 可复写部分,当数据发生变化但是状态没有发生变化时,页面仅仅变化的可以是局部显示127     //              可以通过获取this.html进行修改128     _.callmethod(this.onUpdate, this);129   }130 });

view基本只负责根据模板和数据生成html字符串,有一个不同的点是他需要记录自己的根元素,这个对我们后续操作有帮助

其中比较关键的是templateSet以及wrapperSet,这里的wrapperSet会被注入给dataAdpter的datamodel,后期便于调整

然后是我们的Adapter

 1 Dalmatian.Adapter = _.inherit({ 2  3   // @description 构造函数入口 4   initialize: function (options) { 5     this._initialize(); 6     this.handleOptions(options); 7   }, 8  9   // @description 设置默认属性10   _initialize: function () {11     this.observers = [];12     //    this.viewmodel = {};13     this.datamodel = {};14   },15 16   // @description 操作构造函数传入操作17   handleOptions: function (options) {18     // @description 从形参中获取key和value绑定在this上19     if (_.isObject(options)) _.extend(this, options);20   },21 22   // @override23   // @description 操作datamodel返回一个data对象形成viewmodel24   format: function (datamodel) {25     return datamodel;26   },27 28   getViewModel: function () {29     return this.format(this.datamodel);30   },31 32   registerObserver: function (viewcontroller) {33     // @description 检查队列中如果没有viewcontroller,从队列尾部推入34     if (!_.contains(this.observers, viewcontroller)) {35       this.observers.push(viewcontroller);36     }37   },38 39   setStatus: function (status) {40     _.each(this.observers, function (viewcontroller) {41       if (_.isObject(viewcontroller))42         viewcontroller.setViewStatus(status);43     });44   },45 46   unregisterObserver: function (viewcontroller) {47     // @description 从observers的队列中剔除viewcontroller48     this.observers = _.without(this.observers, viewcontroller);49   },50 51   notifyDataChanged: function () {52     // @description 通知所有注册的观察者被观察者的数据发生变化53     //    this.viewmodel = this.format(this.datamodel);54     var data = this.getViewModel();55     _.each(this.observers, function (viewcontroller) {56       if (_.isObject(viewcontroller))57         _.callmethod(viewcontroller.update, viewcontroller, [data]);58     });59   }60 });

他只负责更新数据,并在数据变化时候通知ViewController处理变化,接下来就是我们的viewController了

  1 Dalmatian.ViewController = _.inherit({  2   3   // @description 构造函数入口  4   initialize: function (options) {  5     this._initialize();  6     this.handleOptions(options);  7   8     //处理datamodel  9     this._handleDataModel(); 10     this.create(); 11   }, 12  13   // @description 默认属性设置点,根据该函数,我可以知道该类具有哪些this属性 14   _initialize: function () { 15  16     //用户设置的容器选择器,或者dom结构 17     this.containe; 18     //根元素 19     this.$el; 20     //默认容器 21     this.root = $('body'); 22  23     //一定会出现 24     this.view; 25     //可能会出现 26     this.adapter; 27     //初始化的时候便需要设置view的状态,否则会渲染失败,这里给一个默认值 28     this.viewstatus = 'init'; 29  30   }, 31  32   setViewStatus: function (status) { 33     this.viewstatus = status; 34   }, 35  36   // @description 操作构造函数传入操作 37   handleOptions: function (options) { 38     if (!options) return; 39  40     this._verify(options); 41  42     // @description 从形参中获取key和value绑定在this上 43     if (_.isObject(options)) _.extend(this, options); 44   }, 45  46   //处理dataAdpter中的datamodel,为其注入view的默认容器数据 47   _handleDataModel: function () { 48     //不存在就不予理睬 49     if (!this.adapter) return; 50     this.adapter.datamodel.wrapperSet = this.view.wrapperSet; 51     this.adapter.registerObserver(this); 52   }, 53  54   // @description 验证参数 55   _verify: function (options) { 56     //这个underscore方法新框架在报错 57     //    if (!_.property('view')(options) && (!this.view)) throw Error('view必须在实例化的时候传入ViewController'); 58     if (options.view && (!this.view)) throw Error('view必须在实例化的时候传入ViewController'); 59   }, 60  61   // @description 当数据发生变化时调用onViewUpdate,如果onViewUpdate方法不存在的话,直接调用render方法重绘 62   update: function (data) { 63  64     //这样虽然减少回流,但会隐藏页面跳动 65     //    _.callmethod(this.hide, this); 66  67     if (!_.callmethod(this.onViewUpdate, this, [data])) { 68       this.render(); 69     } 70  71     //    _.callmethod(this.show, this); 72   }, 73  74   /** 75   * @override 76   * 77   */ 78   render: function () { 79     // @notation  这个方法需要被复写 80     // var data = this.adapter.format(this.origindata); 81     this.view.render(this.viewstatus, this.adapter && this.adapter.getViewModel()); 82   }, 83  84   // @description 返回基于当前view下的某节点 85   find: function (selector) { 86     if (!this.$el) return null; 87     return this.$el.find(selector); 88   }, 89  90   _create: function () { 91     this.render(); 92  93     //render 结束后构建好根元素dom结构 94     this.$el = $(this.view.html); 95   }, 96  97   create: function () { 98  99     //l_wang 这段代码没有看懂************100     //    var $element = this.find(this.view.viewid);101     //    if ($element) return _.callmethod(this.recreate, this);102     //l_wang 这段代码没有看懂************103 104     // @notation 在create方法调用前后设置onViewBeforeCreate和onViewAfterCreate两个回调105     _.wrapmethod(this._create, 'onViewBeforeCreate', 'onViewAfterCreate', this);106 107   },108 109   /**110   * @description 如果进入create判断是否需要update一下页面,sync view和viewcontroller的数据111   */112   _recreate: function () {113     this.update();114   },115 116   recreate: function () {117     _.wrapmethod(this._recreate, 'onViewBeforeRecreate', 'onViewAfterRecreate', this);118   },119 120   //事件注册点121   bindEvents: function (events) {122     if (!(events || (events = _.result(this, 'events')))) return this;123     this.unBindEvents();124 125     // @description 解析event参数的正则126     var delegateEventSplitter = /^(\S+)\s*(.*)$/;127     var key, method, match, eventName, selector;128 129     //注意,此处做简单的字符串数据解析即可,不做实际业务130     for (key in events) {131       method = events[key];132       if (!_.isFunction(method)) method = this[events[key]];133       if (!method) continue;134 135       match = key.match(delegateEventSplitter);136       eventName = match[1], selector = match[2];137       method = _.bind(method, this);138       eventName += '.delegateEvents' + this.view.viewid;139 140       if (selector === '') {141         this.$el.on(eventName, method);142       } else {143         this.$el.on(eventName, selector, method);144       }145     }146 147     return this;148   },149 150   //取消所有事件151   unBindEvents: function () {152     this.$el.off('.delegateEvents' + this.view.viewid);153     return this;154   },155 156   _show: function () {157     this.bindEvents();158     this.root = $(this.container);159     this.root.append(this.$el);160     this.$el.show();161   },162 163   show: function () {164     _.wrapmethod(this._show, 'onViewBeforeShow', 'onViewAfterShow', this);165   },166 167   _hide: function () {168     this.forze();169     this.$el.hide();170   },171 172   hide: function () {173     _.wrapmethod(this._hide, 'onViewBeforeHide', 'onViewAfterHide', this);174   },175 176   _forze: function () {177     this.unBindEvents();178   },179 180   forze: function () {181     _.wrapmethod(this._forze, 'onViewBeforeForzen', 'onViewAfterForzen', this);182   },183 184   _destory: function () {185     this.unBindEvents();186     this.$el.remove();187     //    delete this;188   },189 190   destory: function () {191     _.wrapmethod(this._destory, 'onViewBeforeDestory', 'onViewAfterDestory', this);192   }193 });

View Code

这个控制器是连接view以及Adapter的桥梁,三者合一便可以处理一些问题,接下来看一个简单的demo

Ajax例子

  1 <!doctype html>  2 <html lang="en">  3 <head>  4   <meta charset="UTF-8">  5   <title>ToDoList</title>  6   <meta name="viewport" content="width=device-width, initial-scale=1.0">  7   <link rel="stylesheet" type="text/css" href="http://designmodo.github.io/Flat-UI/bootstrap/css/bootstrap.css">  8   <link rel="stylesheet" type="text/css" href="http://designmodo.github.io/Flat-UI/css/flat-ui.css">  9   <link href="../style/main.css" rel="stylesheet" type="text/css" /> 10   <style type="text/css"> 11     .cui-alert { width: auto; position: static; } 12     .txt { border: #cfcfcf 1px solid; margin: 10px 0; width: 80%; } 13     ul, li { padding: 0; margin: 0; } 14     .cui_calendar, .cui_week { list-style: none; } 15     .cui_calendar li, .cui_week li { float: left; width: 14%; overflow: hidden; padding: 4px 0; text-align: center; } 16   </style> 17 </head> 18 <body> 19   <article id="container"> 20   </article> 21   <script type="text/underscore-template" id="template-ajax-init"> 22       <div class="cui-alert" > 23         <div class="cui-pop-box"> 24           <div class="cui-hd"> 25             <%=title%> 26           </div> 27           <div class="cui-bd"> 28             <div class="cui-error-tips"> 29             </div> 30             <div class="cui-roller-btns" style="padding: 4px; "><input type="text" placeholder="请设置数据 [{ title: ''},{ title: ''}]" style="margin: 2px; width: 100%; " id="ajax_data" class="txt"></div> 31             <div class="cui-roller-btns"> 32               <div class="cui-flexbd cui-btns-sure"><%=confirm%></div> 33             </div> 34           </div> 35         </div> 36       </div> 37   </script> 38   <script type="text/underscore-template" id="template-ajax-suc"> 39     <ul> 40     <% console.log(ajaxData) %> 41     <%for(var i = 0; i < ajaxData.length; i++) { %> 42       <li><%=ajaxData[i].title %></li> 43     <% } %> 44   </ul> 45   </script> 46    47   <script type="text/underscore-template" id="template-ajax-loading"> 48     loading.... 49   </script> 50  51   <script type="text/javascript" src="../../vendor/underscore-min.js"></script> 52   <script type="text/javascript" src="../../vendor/zepto.min.js"></script> 53   <script src="../../src/underscore.extend.js" type="text/javascript"></script> 54   <script src="../../src/util.js" type="text/javascript"></script> 55   <script src="../../src/message-center-wl.js" type="text/javascript"></script> 56   <script src="../../src/mvc-wl.js" type="text/javascript"></script> 57   <script type="text/javascript"> 58  59     //模拟Ajax请求 60     function getAjaxData(callback, data) { 61       setTimeout(function () { 62         if (!data) { 63           data = []; 64           for (var i = 0; i < 5; i++) { 65             data.push({ title: '我是标题_' + i }); 66           } 67         } 68         callback(data); 69       }, 1000); 70     } 71  72     var AjaxView = _.inherit(Dalmatian.View, { 73       _initialize: function ($super) { 74         //设置默认属性 75         $super(); 76  77         this.templateSet = { 78           init: $('#template-ajax-init').html(), 79           loading: $('#template-ajax-loading').html(), 80           ajaxSuc: $('#template-ajax-suc').html() 81         }; 82  83         this.wrapperSet = { 84           loading: '.cui-error-tips', 85           ajaxSuc: '.cui-error-tips' 86         }; 87       } 88     }); 89  90     var AjaxAdapter = _.inherit(Dalmatian.Adapter, { 91       _initialize: function ($super) { 92         $super(); 93         this.datamodel = { 94           title: '标题', 95           confirm: '刷新数据' 96         }; 97         this.datamodel.ajaxData = {}; 98       }, 99 100       format: function (datamodel) {101         //处理datamodel生成viewModel的逻辑102         return datamodel;103       },104 105       ajaxLoading: function () {106         this.setStatus('loading');107         this.notifyDataChanged();108       },109 110       ajaxSuc: function (data) {111         this.datamodel.ajaxData = data;112         this.setStatus('ajaxSuc');113         this.notifyDataChanged();114       }115     });116 117     var AjaxViewController = _.inherit(Dalmatian.ViewController, {118       _initialize: function ($super) {119         $super();120         //设置基本的属性121         this.view = new AjaxView();122         this.adapter = new AjaxAdapter();123         this.viewstatus = 'init';124         this.container = '#container';125       },126 127       //显示后Ajax请求数据128       onViewAfterShow: function () {129         this._handleAjax();130       },131 132       _handleAjax: function (data) {133         this.adapter.ajaxLoading();134         getAjaxData($.proxy(function (data) {135           this.adapter.ajaxSuc(data);136         }, this), data);137       },138 139       events: {140         'click .cui-btns-sure': function () {141           var data = this.$el.find('#ajax_data').val();142           data = eval('(' + data + ')');143           this._handleAjax(data);144         }145       }146     });147 148     var a = new AjaxViewController();149     a.show();150 151   </script>152 </body>153 </html>

View Code

这段代码的核心在此

 1 //模拟Ajax请求 2 function getAjaxData(callback, data) { 3   setTimeout(function () { 4     if (!data) { 5       data = []; 6       for (var i = 0; i < 5; i++) { 7         data.push({ title: '我是标题_' + i }); 8       } 9     }10     callback(data);11   }, 1000);12 }13 14 var AjaxView = _.inherit(Dalmatian.View, {15   _initialize: function ($super) {16     //设置默认属性17     $super();18 19     this.templateSet = {20       init: $('#template-ajax-init').html(),21       loading: $('#template-ajax-loading').html(),22       ajaxSuc: $('#template-ajax-suc').html()23     };24 25     this.wrapperSet = {26       loading: '.cui-error-tips',27       ajaxSuc: '.cui-error-tips'28     };29   }30 });31 32 var AjaxAdapter = _.inherit(Dalmatian.Adapter, {33   _initialize: function ($super) {34     $super();35     this.datamodel = {36       title: '标题',37       confirm: '刷新数据'38     };39     this.datamodel.ajaxData = {};40   },41 42   format: function (datamodel) {43     //处理datamodel生成viewModel的逻辑44     return datamodel;45   },46 47   ajaxLoading: function () {48     this.setStatus('loading');49     this.notifyDataChanged();50   },51 52   ajaxSuc: function (data) {53     this.datamodel.ajaxData = data;54     this.setStatus('ajaxSuc');55     this.notifyDataChanged();56   }57 });58 59 var AjaxViewController = _.inherit(Dalmatian.ViewController, {60   _initialize: function ($super) {61     $super();62     //设置基本的属性63     this.view = new AjaxView();64     this.adapter = new AjaxAdapter();65     this.viewstatus = 'init';66     this.container = '#container';67   },68 69   //显示后Ajax请求数据70   onViewAfterShow: function () {71     this._handleAjax();72   },73 74   _handleAjax: function (data) {75     this.adapter.ajaxLoading();76     getAjaxData($.proxy(function (data) {77       this.adapter.ajaxSuc(data);78     }, this), data);79   },80 81   events: {82     'click .cui-btns-sure': function () {83       var data = this.$el.find('#ajax_data').val();84       data = eval('(' + data + ')');85       this._handleAjax(data);86     }87   }88 });89 90 var a = new AjaxViewController();91 a.show();

首先定义view

其次定义数据处理层

最后将两者合一

重点放到了数据处理中,实际上的逻辑由Controller处理,真正的html又view生成,整个代码如上......

结语

今天对之前的学习进行了一些整理,由于过程中多数时间在编码,所以描述少了一点,整个这块还是有一些问题,我们留待后期解决吧

0人推荐
随时随地看视频
慕课网APP