上个周,并肩作战的田老师离职了,尽管在一起愉快玩耍的时间不到一年,自己仍然还是从其身上学到、体会到了好多关于知识、理想的东西。对于大多数年轻人关于“晚上想想千条路,早上起来走原路”的现状,他那种敢于甩掉一切去做自己感兴趣、梦想的事的勇气是我所钦佩的。在此,祝愿田老师一切顺利。
在最后一次交接会议上,田老师阐述了一个观点,“当你学会了用‘分层思想’去看待事情,任何的问题都不是问题,都可以实现”。当然,这里说的是在程序设计方面。自己觉的很有道理,但是体会不是很深。
紧跟着,这个周期盼已久的“重构版热图”上线了,“低bug率、高速度”等在各方面指标瞬间秒杀“旧版热图”,让大家眼前一亮。随即,我们组织了分享讨论会,让匡哥讲述其重构过程中的设计思路。
大致思想如下:将每个功能点最小颗粒化、然后将其封装成模块;创建数据中心,使各个模块不在互相调用嵌套,所有的依赖和调用全部通过数据中心(这里使用自定义事件实现的观察者模式);所有的网状的需求点,划点成线,最终形成操作流。
这不就是“分层思想”的一种体现吗?我陷入了沉思~~~
现在,大前端流行组件化、模块化。然而,我们的模块又该如何设计实现呢?
下面的实例,参考自【javascript组件开发方式】【GitHub示例地址】
<div id="container">
id="content" />
div>
$(function() {
var $content = $("#content");
// 获取字数
function getNum() {
return $content.val().length;
}
// 渲染元素
function render() {
var num = getNum();
if($("#contentCount").length === 0) {
// 不存在统计字符的DOM元素
$content.after("");
}
$("#contentCount").html(num + "个字");
}
// 监听时间
$content.on("keyup", function() {
render();
});
});
缺点:变量混乱,没有很好的隔离作用域,当页面变得复杂的时候,很难维护。
var textCount = {
input: null,
init: function(config) {
this.input = $(config.id);
this.bind();
return this; // 方便实现链式调用
},
bind: function() {
var self = this;
this.input.on("keyup", function() {
self.render();
});
},
render: function() {
var num = this.getNum();
if($("#contentCount").length === 0) {
this.input.after("");
}
$("#contentCount").html(num + "个字");
},
getNum: function() {
return this.input.val().length;
}
};
$(function() {
textCount.init({id: '#content'}).render();
});
缺点:这种写法没有私有的概念。其他代码可以很随意的改动这些,容易出现变量重复,或被修改的问题。
把所有的东西都包在了一个自动执行的闭包里面,所以不会受到外面的影响,并且只对外公开了TextCountFun构造函数,生成的对象只能访问到init,render方法。事实上大部分的jQuery插件都是这种写法。
var textCount = (function() {
// 私有方法
var _bind = function(that) {
that.input.on("keyup", function() {
that.render();
});
};
var _getNum = function(that) {
return that.input.val().length;
};
var TextCountFun = function() {};
TextCountFun.prototype.init = function(config) {
this.input = $(config.id);
_bind(this);
return this;
};
TextCountFun.prototype.render = function() {
var num = _getNum(this);
if($("#contentCount").length === 0) {
this.input.after("");
}
$("#contentCount").html(num + "个字");
};
// 返回构造函数
return TextCountFun;
})();
$(function() {
new textCount().init({id: '#content'}).render();
});
var Class = (function() {
var _mix = function(r, s) {
for (var p in s) {
if (s.hasOwnProperty(p)) {
r[p] = s[p];
}
}
}
var _extend = function() {
//开关 用来使生成原型时,不调用真正的构成流程init
this.initPrototype = true;
var prototype = new this();
this.initPrototype = false;
var items = Array.prototype.slice.call(arguments) || [];
var item;
//支持混入多个属性,并且支持{}也支持Function
while (item = items.shift()) {
_mix(prototype, item.prototype || item);
}
// 这边是返回的类,其实就是我们返回的子类
function SubClass() {
if (!SubClass.initPrototype && this.init) {
this.init.apply(this, arguments); //调用init真正的构造函数
}
}
// 赋值原型链,完成继承
SubClass.prototype = prototype
// 改变constructor引用
SubClass.prototype.constructor = SubClass
// 为子类也添加extend方法
SubClass.extend = _extend
return SubClass
}
//超级父类
var Class = function() {};
//为超级父类添加extend方法
Class.extend = _extend;
return Class;
})();
var TextCount = Class.extend({
init: function(config){
this.input = $(config.id);
this._bind();
this.render();
},
render: function() {
var num = this._getNum();
if ($('#contentCount').length == 0) {
this.input.after('');
}
$('#contentCount').html(num + '个字');
},
_getNum: function(){
return this.input.val().length;
},
_bind: function(){
var self = this;
self.input.on('keyup', function() {
self.render();
});
}
});
$(function() {
new TextCount({id:"#content"});
});
缺点:当一个页面特别复杂,当我们需要的组件越来越多,当我们需要做一套组件。仅仅用这个就不行了。首先的问题就是,这种写法太灵活了,写单个组件还可以。如果我们需要做一套风格相近的组件,而且是多个人同时在写。那真的是噩梦。
下述创建对象采用《构造函数和原型模式组合使用》,此方式最广泛、认同度最高。
function Event(config) {
// 私用,外部不允许直接调用
this._config = config; // 存储相关配置信息
this._events = {}; // 存储所有处理函数
};
Event.prototype = {
constructor: Event,
// 监听事件 key:事件类型,listener:事件处理函数(可以同时绑定多个不同类型事件)
on: function(keys, listener) {
var keyList = keys.split(/[\,\s\;]/); // 支持同时绑定多个事件,用【逗号、分号或空格隔开】
var index = keyList.length;
while (index) {
index--;
var key = keyList[index];
// 不存在当前类型的事件
if (!this._events[key]) {
this._events[key] = []; // 这里指定为数组,可以多次绑定同一事件
}
this._events[key].push(listener);
}
},
// 只能移除指定类型事件(一个)
off: function (key, listener) {
// 不指定事件类型,移除全部事件
if (!key) {
this._events = {};
return;
}
var event = this._events[key];
// 不存在要移除的事件,直接返回
if (!event) {
return;
}
// 不指定事件处理程序,移除指定类型
if (!listener) {
delete this._events[key];
} else {
var length = event.length;
while (length > 0) {
length--;
if (event[length] === listener) {
event.splice(length, 1); // 移除指定类型、指定处理程序的事件
}
}
}
},
// 触发对应类型的事件,私有,外部不允许调用(为达到统一出口目的)
_emit: function (key, args) {
var event = this._events[key];
if (event) {
var length = event.length;
var i = 0;
while (i < length) {
event[i](args);
i++;
}
}
}
}
Event.prototype.setConfig = function(config) {
this._config = config;
};
Event.prototype.getConfig = function() {
return this._config;
};
Event.prototype.setInput = function(input) {
this.setConfig(input)
// input信息改变,触发自定义change事件
this._emit("inputChange");
}
var customerEvent = new Event();
// 监听自定义inputchange事件
customerEvent.on("inputChange", function() {
var $input = customerEvent.getConfig();
var num = $input.val().length;
if ($('#contentCount').length == 0) {
$input.after('');
}
$('#contentCount').html(num + '个字');
});
$("#content").on("keyup", function() {
customerEvent.setInput($(this));
});
说明:由于功能比较单一,所以不能很好的体会到上述“观察者模式”的好处。试想,将上述抽离为两个业务模块,即当input内容长度发生改变(模块A),要通知另一个业务模块去改变对应显示(模块B)。如果不采用上述模式,很容易造成模块之间的互相调用。很容易造成在不知情的情况下修改了模块A导致了模板B不能正常使用。而上述方式,提供了一种分层的方式。A模块处理A的任务、B模块处理B的任务。模块之间的调用和耦合全局交给中间控制层(上述Event所在层)去控制。
注意:所有的时间触发,都在中间控制层;而相关的事件监听和引起事件触发的动作则在相关模块。为了正常通信,相关模块需要共享同一个中间控制层实例。
// Base封装组件的各个过程,并具有时间机制
var Base = Class.extend({
init:function(config){
//自动保存配置项
this.__config = config
this.bind()
this.render()
},
//可以使用get来获取配置项
get:function(key){
return this.__config[key]
},
//可以使用set来设置配置项
set:function(key,value){
this.__config[key] = value
},
bind:function(){},
render:function() {},
//定义销毁的方法,一些收尾工作都应该在这里
destroy:function(){}
});
/**
* 加强版Base
* 事件代理:不需要用户自己去找dom元素绑定监听,也不需要用户去关心什么时候销毁。
* 模板渲染:用户不需要覆盖render方法,而是覆盖实现setUp方法。可以通过在setUp里面调用render来达到渲染对应html的目的。
* 单向绑定:通过setChuckdata方法,更新数据,同时会更新html内容,不再需要dom操作。
*/
var RichBase = Base.extend({
EVENTS: {},
template: '',
init: function(config){
//存储配置项
this.__config = config;
//解析代理事件
this._delegateEvent();
this.setUp();
},
//循环遍历EVENTS,使用jQuery的delegate代理到parentNode
_delegateEvent: function(){
var self = this;
var events = this.EVENTS || {};
var eventObjs, fn, select, type;
var parentNode = this.get('parentNode') || $(document.body);
for (select in events) {
eventObjs = events[select];
for (type in eventObjs) {
fn = eventObjs[type];
parentNode.delegate(select,type,function(e){
fn.call(null,self,e);
})
}
}
},
//支持underscore的极简模板语法
//用来渲染模板,这边是抄的underscore的。非常简单的模板引擎,支持原生的js语法
_parseTemplate: function(str,data){
/**
* http://ejohn.org/blog/javascript-micro-templating/
* https://github.com/jashkenas/underscore/blob/0.1.0/underscore.js#L399
*/
var fn = new Function('obj',
'var p=[],print=function(){p.push.apply(p,arguments);};' +
'with(obj){p.push(\'' + str.replace(/[\r\t\n]/g, " ")
.split("<%").join("\t")
.replace(/((^|%>)[^\t]*)'/g, "$1\r")
.replace(/\t=(.*?)%>/g, "',$1,'")
.split("\t").join("');")
.split("%>").join("p.push('")
.split("\r").join("\\'") +
"');}return p.join('');");
return data ? fn(data) : fn;
},
//提供给子类覆盖实现
setUp: function(){
this.render();
},
//用来实现刷新,只需要传入之前render时的数据里的key还有更新值,就可以自动刷新模板
setChuckdata: function(key,value){
var self = this;
var data = self.get('__renderData');
//更新对应的值
data[key] = value;
if (!this.template) return;
//重新渲染
var newHtmlNode = $(self._parseTemplate(this.template,data));
//拿到存储的渲染后的节点
var currentNode = self.get('__currentNode');
if (!currentNode) return;
//替换内容
currentNode.replaceWith(newHtmlNode);
self.set('__currentNode',newHtmlNode);
},
//使用data来渲染模板并且append到parentNode下面
render: function(data){
var self = this;
//先存储起来渲染的data,方便后面setChuckdata获取使用
self.set('__renderData', data);
if (!this.template) return;
//使用_parseTemplate解析渲染模板生成html
//子类可以覆盖这个方法使用其他的模板引擎解析
var html = self._parseTemplate(this.template,data);
var parentNode = this.get('parentNode') || $(document.body);
var currentNode = $(html);
//保存下来留待后面的区域刷新
//存储起来,方便后面setChuckdata获取使用
self.set('__currentNode',currentNode);
parentNode.append(currentNode);
},
destroy: function(){
var self = this;
//去掉自身的事件监听
self.off();
//删除渲染好的dom节点
self.get('__currentNode').remove();
//去掉绑定的代理事件
var events = self.EVENTS || {};
var eventObjs,fn,select,type;
var parentNode = self.get('parentNode');
for (select in events) {
eventObjs = events[select];
for (type in eventObjs) {
fn = eventObjs[type];
parentNode.undelegate(select,type,fn);
}
}
},
//可以使用get来获取配置项
get: function(key){
return this.__config[key]
},
//可以使用set来设置配置项
set: function(key, value){
this.__config[key] = value
}
});
/**
* (1)事件的解析跟代理,全部代理到parentNode上面。
* (2)render抽出来,用户只需要实现setUp方法。如果需要模板支持就在setUp里面调用render来渲染模板
* (3)可以通过setChuckdata来刷新模板,实现单向绑定。
*/
var TextCount = RichBase.extend({
//事件直接在这里注册,会代理到parentNode节点,parentNode节点在下面指定
EVENTS: {
//选择器字符串,支持所有jQuery风格的选择器
'input':{
//注册keyup事件
keyup:function(self,e){
//单向绑定,修改数据直接更新对应模板
self.setChuckdata('count',self._getNum());
}
}
},
//指定当前组件的模板
template: '<%= count %>个字',
//私有方法
_getNum: function(){
return this.get('input').val().length || 0
},
//覆盖实现setUp方法,所有逻辑写在这里。最后可以使用render来决定需不需要渲染模板
//模板渲染后会append到parentNode节点下面,如果未指定,会append到document.body
setUp: function(){
var self = this;
var input = this.get('parentNode').find('#content');
self.set('input', input);
var num = this._getNum();
//赋值数据,渲染模板,选用。有的组件没有对应的模板就可以不调用这步。
self.render({
count: num
});
}
});
$(function() {
//传入parentNode节点,组件会挂载到这个节点上。所有事件都会代理到这个上面。
new TextCount({parentNode: $("#container")});
});