前情回顾
根据之前的学习,我们形成了一个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生成,整个代码如上......
结语
今天对之前的学习进行了一些整理,由于过程中多数时间在编码,所以描述少了一点,整个这块还是有一些问题,我们留待后期解决吧