从DOM操作看Vue&React的前端组件化,顺带补齐React的demo_html/css_WEB-ITnose
前言
接上文: 谈谈我对前端组件化中“组件”的理解,顺带写个Vue与React的demo
上次写完博客后,有朋友反应第一内容有点深,看着迷迷糊糊;第二是感觉没什么使用场景,太过业务化,还不如直接写Vue&react的源码分析,我感觉这里有必要说下我的认识。
首先,要写源码分析很难,第一是他本来就很难,所以一般我们是想了解他实现的思路而不是代码;
第二每个开发者有自己发风格,所以你要彻底读懂一个人的代码不容易,除非你是带着当时作者同样的问题不断的寻找解决方案,不断的重构,才可能理解用户的意图。
我们上一次做的事情其实就是根据自己实际的工作经验做了和外面框架类似的事情,虽然代码的健壮、优雅程度跟不上,但却和其它作者一样为解决同样的问题思考得出的方案,上次做的太晚了,后面就草草结束,事实上在我Demo过程中发现一个事实:业务代码都是差不多的,只是一些细节点不一样,所以决定产品质量的依旧是开发者的业务代码能力,框架只是助力而已。
不能了解作者的意图,不能提高本身的编程水平,就算用上了React&Vue这类组件化的框架,也组织不好代码;事实上这类代码因为是面向大型应用的,反而更考验一个人的架构能力,所以大家要多注重内在修养的提升哦。
下面我们进入今天的正题,这里依旧提供一些帮助理解的资料:
github
代码地址: https://github.com/yexiaochai/module/
演示地址: http://yexiaochai.github.io/module/me/index.html
如果对文中的一些代码比较疑惑,可以对比着看看这些文章:
【一次面试】再谈javascript中的继承
【移动前端开发实践】从无到有(统计、请求、MVC、模块化)H5开发须知
【组件化开发】前端进阶篇之如何编写可维护可升级的代码
预览
使用Vue的思考
因为第一个demo是Vue的,React应该也类似,对比之前的代码发现一个重要差异是:
DOM操作真的完全没有了!!!
对,是完全没有DOM操作了,这个是很牛逼的一件事情,因为我觉得有两个地方要摆脱DOM操作很难:
① 我的组件应该放到哪个容器内,我需要一个定位的元素,比如:
1 this.sortModule = new SortModule({2 view: this,3 selector: '.js_sort_wrapper',4 sortEntity: this.sortEntity5 });
明确的告诉了组件所属的容器
② 我比较疑惑像这类列表类型的事件该如何处理,因为一些必要参数是根据event获取的,比如:
1 listItemClick: function (e) {2 var el = $(e.currentTarget);3 //根据el做一些事情4 }
关于这个Vue的作者认为应该将事件处理程序内联,做显示声明:
你可能注意到这种事件监听的方式违背了传统理念 “separation of concern”。不必担心,因为所有的 Vue.js 事件处理方法和表达式都严格绑定在当前视图的 ViewModel 上,它不会导致任何维护困难。实际上,使用 v-on 有几个好处:扫一眼 HTML 模板便能轻松定位在 JavaScript 代码里对应的方法。因为你无须在 JavaScript 里手动绑定事件,你的 ViewModel 代码可以是非常纯粹的逻辑,和 DOM 完全解耦,更易于测试。当一个 ViewModel 被销毁时,所有的事件处理器都会自动被删除。你无须担心如何自己清理它们。
1 methods: {2 say: function (msg, event) {3 // 现在我们可以访问原生事件对象4 event.preventDefault()5 }6 }
还有种常用的操作,比如radioList,点击当前选项便选择项目,我们一般的做法是这样的:
1 setIndex: function (i) {2 this.index = i;3 this.$('li').removeClass(this.curClass);4 this.$('li[data-index="' + i + '"]').addClass(this.curClass);5 }
这样做比较简单,但是会有一个问题,便是数据与dom表现的流程变了,正确的流程是index 变了,dom便根据数据做更新,比如Vue:
1 setIndex: function (i) {2 this.index = i;3 //这部分逻辑Vue会自动实现4 //this.$('li').removeClass(this.curClass);5 //this.$('li[data-index="' + i + '"]').addClass(this.curClass);6 }
之前,不考虑性能,我们会直接根据数据重新渲染整个列表,就为一个简单的选中功能,而Vue&React却做到了局部渲染,这个是否牛逼,我相信这个将会是一个核心算法部分,后面有时间一定要深入了解。
根据以上局部解读,我们得到一个结论,只要达成两个条件,就能摆脱DOM操作:
① 知道组件所处容器
② 根据数据渲染页面
PS:我们这里是很简单的一环,没有考虑组件嵌套,组件通信等过于复杂的问题
那么如果达成了以上条件,我们能否做到业务逻辑中不包含dom操作呢?我们下面就来试试。
如何摆脱DOM操作
这里真的是demo类尝试,思维验证,便不使用之前过于复杂的业务逻辑了,这里将me目录拷贝一块出来,依旧以原来的代码做底层依赖,只要列表与顶部排序部分功能,这里为了简化实现,保持代码重用,我们这里直接想将entity模块复用,要求data中的对象必须是一个entity实例,这里第一步是抽象出来了list module模块,于是主控制器变成这样了,事实上这个时候已经没dom操作了:
1 initEntity: function () { 2 //实例化排序的导航栏的实体 3 this.sortEntity = new SortEntity(); 4 this.sortEntity.subscribe(this.renderList, this); 5 }, 6 7 initModule: function () { 8 //view为注入给组件的根元素 9 //selector为组件将要显示的容器10 //sortEntity为注入给组件的数据实体,做通信用11 //这个module在数据显示后会自动展示12 this.sortModule = new SortModule({13 view: this,14 selector: '.js_sort_wrapper',15 sortEntity: this.sortEntity16 });17 this.listModule = new ListModule({18 view: this,19 selector: '.js_list_wrapper',20 entity: this.sortEntity21 });22 },23 24 propertys: function ($super) {25 $super();26 27 this.initEntity();28 this.initModule();29 this.viewId = 'list';30 this.template = layoutHtml;31 this.events = {};32 }
这里简单看看列表组件的实现,其实就是将原来根View的代码换个位置:
1 define([ 2 'ModuleView', 3 'pages/list.data', 4 'text!pages/tpl.list.html' 5 6 ], function (ModuleView, 7 listData, 8 tpl) { 9 return _.inherit(ModuleView, { 10 11 //此处若是要使用model,处实例化时候一定要保证entity的存在,如果不存在便是业务BUG 12 initData: function () { 13 14 this.template = tpl; 15 this.entity.subscribe(this.render, this); 16 17 }, 18 19 _timeSort: function (data, sort) { 20 data = _.sortBy(data, function (item) { 21 item = item.from_time.split(':'); 22 item = item[0] + '.' + item[1]; 23 item = parseFloat(item); 24 return item; 25 }); 26 if (sort == 'down') data.reverse(); 27 return data; 28 }, 29 30 _sumTimeSort: function (data, sort) { 31 data = _.sortBy(data, function (item) { 32 return parseInt(item.use_time); 33 }); 34 if (sort == 'down') data.reverse(); 35 return data; 36 }, 37 38 _priceSort: function (data, sort) { 39 data = _.sortBy(data, function (item) { 40 return item.min_price; 41 }); 42 if (sort == 'down') data.reverse(); 43 return data; 44 }, 45 46 //获取导航栏排序后的数据 47 getSortData: function (data) { 48 var tmp = []; 49 var sort = this.entity.get(); 50 51 for (var k in sort) { 52 if (sort[k].length > 0) { 53 tmp = this['_' + k + 'Sort'](data, sort[k]) 54 return tmp; 55 } 56 } 57 }, 58 59 //复杂的业务数据处理,为了达到产品的需求,这段代码逻辑与业务相关 60 //这段数据处理的代码过长(超过50行就过长),应该重构掉 61 formatData: function (data) { 62 var item, seat; 63 var typeMap = { 64 'g': 'g', 65 'd': 'd', 66 't': 't', 67 'c': 'g' 68 }; 69 70 //出发时间对应的分钟数 71 var fromMinute = 0; 72 73 //获取当前班车日期当前的时间戳,这个数据是动态的,这里写死了 74 var d = 1464192000000; 75 var date = new Date(); 76 var now = parseInt(date.getTime() / 1000); 77 date.setTime(d); 78 var year = date.getFullYear(); 79 var month = date.getMonth(); 80 var day = date.getDate(); 81 var toBegin; 82 var seatName, seatIndex, iii; 83 84 //处理坐席问题,仅显示二等座,一等座,特等座 无座 85 // 二等座 一等座 商务座 无座 动卧 特等座 86 var my_seats = {}; 87 var seatSort = ['二等座', '一等座', '硬座', '硬卧', '软卧', '商务座', '无座', '动卧', '特等座', '软座']; 88 89 for (var i = 0, len = data.length; i seat[j].seat_price) data[i].min_price = parseFloat(seat[j].seat_price);112 data[i].sum_ticket += parseInt(seat[j].seat_yupiao);113 114 //坐席问题如果坐席不包括上中下则去掉115 seatName = seat[j].seat_name;116 //去掉上中下117 seatName = seatName.replace(/上|中|下/g, '');118 if (!my_seats[seatName]) {119 my_seats[seatName] = parseInt(seat[j].seat_yupiao);120 } else {121 my_seats[seatName] = my_seats[seatName] + parseInt(seat[j].seat_yupiao);122 }123 }124 //这里myseat为对象,需要转换为数组125 //将定制坐席转为排序后的数组126 data[i].my_seats = [];127 for (iii = 0; iii就这种简单的改变,貌似便摆脱了DOM操作,页面所有的状态事实上是可以做到由数据控制的,但是这里没有形成“标签化”,似乎不太好,于是我们来试试是否能改造为标签化的代码。
我们这里的业务代码(module与entity)没有什么需要改动的,这里主要在底层做改造,这里在我看来是提供了一种“语法糖”的东西,这里的具体概念后续阅读Vue源码再深入了解,这里先照着做,这里看结果想实现,也是我们常用的一种设计方案,首先我们的index编程了这个样子:
12 56 1 (function () { 2 require.config({ 3 paths: { 4 'text': 'libs/require.text', 5 6 'AbstractView': 'js/view', 7 'AbstractEntity': 'js/entity', 8 'ModuleView': 'js/module' 9 }10 });11 12 require(['pages/list.label'], function (List) {13 var list = new List();14 list.show();15 });16 })();PS:里面的js钩子基本无用了
这里标签化带来的好处是,根View中有一段实例代码可以不用与选择器映射了,比如这个:
1 this.sortModule = new SortModule({2 //view: this,3 //selector: '.js_sort_wrapper',4 //sortEntity: this.sortEntity5 });因为处于组件中,其中所处位置已经定了,view实例或者entity实例全部是跟View显示注入的,这里根View中参考Vue的使用,新增一个$components与$entities属性,然后增加一$watch对象。
大家写底层框架时,私有属性或者方法使用_method的方式,如果是要释放的可以是$method这种,一定要“特殊化”防止被实例或者继承覆盖1 define([ 2 'AbstractView', 'pages/en.sort', 'pages/mod.sort', 'pages/mod.list' 3 ], function (AbstractView, SortEntity, SortModule, ListModule) { 4 return _.inherit(AbstractView, { 5 propertys: function ($super) { 6 $super(); 7 this.$entities = { 8 sortEntity: SortEntity 9 };10 this.$components = {11 mySortBar: SortModule,12 listModule: ListModule13 };14 this.$watch = {15 16 };17 this.viewId = 'list';18 this.template = layoutHtml;19 this.events = {};20 }21 });22 });他这种做法,需要组件在显示后框架底层将刚刚的业务代码实现,使用组件生成的html代码将原来标签的占位符给替换掉。
这里在组件也需要明示根View需要注入什么给自己:
PS:事实上这个可以不写,写了对后续属性的计算有好处
//记录需要根View注入的属性props:[sortEntity],PS:底层什么时候执行替换这个是有一定时机的,我们这里暂时放到根View展示后,这里更好的实现,后续我们在Vue与React中去找寻
因为我们这里是demo类实现,为降低难度,我们为每一个组件动态增加一个div包裹层,于是,我们在跟View中,在View展示后,我们另外多加一段逻辑:
1 //实例化实体,后面要用2 this._initEntity();3 //新增标签逻辑4 this._initComponent();然后将实体与组件的实例化放到框架底层,这里实体的实例化比较简单(如果有特殊数据需求再说,这里只考虑最简单情况):
1 _initEntity: function() {2 var key, entities = this.$entities;3 //这里没有做特殊化,需要注意4 for(key in entities) {5 this[key] = new entities[key]();6 }7 },而实例化组件的工作复杂许多,因为他需要将页面中的自定义标签替换掉,还需要完成很多属性注入操作:
1 _initComponent: function() {2 var key, components = this.$components;3 for(key in components) {4 //这里实例化的过程有点复杂,首先将页面的标签做一个替换5 var s = ''6 }7 },1 _initComponent: function() { 2 var key, components = this.$components; 3 var el, attributes, attr, param, clazz, i, len, tmp, id, name; 4 5 //这里实例化的过程有点复杂,首先将页面的标签做一个替换 6 for(key in components) { 7 param = {}; 8 clazz = components[key]; 9 //由原型链上获取根元素要注入给子组件的属性(这个实现好像不太好)10 attributes = clazz.prototype.props;11 12 //首先获取标签dom元素,因为html是不区分大小写的,这里将标签小写13 el = this.$(key.toLowerCase());14 if(!el[0]) continue;15 16 if(attributes) {17 for (i = 0, len = attributes.length; i