Js 组件化基础
需求: 我们需要制作一个输入框,并显示出输入字符的个数
Step1:
- 最初始者的写法(使用各种全局变量 缺点:变量混乱难以维护)
$(function(){
var input = $('#j-input');
var num = $('#j-num');
function addEvent(){
input.on('keyup', function(){
render();
});
};
function getNum(){
return input.val().length;
}
//渲染变化的数字
function render(){
num.html(getNum());
}
function init(){
addEvent();
};
init();
})()
Step2:
- 使用单个变量作为命名空间,这时候我们会把我们的控制写在一个一个单独的变量里面 (优点:结构清晰 缺点:没有私有方法,私有变量的概念) Js bin 查看地址
$(function(){
var inputNumController = {
init: function(){
this.getNode();
this.bindEvent();
},
bindEvent: function(){
this.input.on('keyup', function(){
var num = this.input.val().length;
this.num.html(num);
}.bind(this));
},
getNode: function(){
this.input = $('#j-input');
this.num = $('#j-num');
}
};
inputNumController.init();
}());
Step4:
- 通过函数闭包实现私有变量,公有变量的控制, return 出公有接口 (优点:大部分jquery的插件写法 可以满足大部分的需求 缺点:适用于单个组件的写法,但是要做一套组件时,很难维护,因为每个人的风格会不一样,且不利于重用) Js bin 查看地址
<input type="text" id="j-input-1"/>
<input type="text" id="j-input-2"/>
//利用闭包
var TextNumCom = (function(){
//私有方法
var _bindNumControl = function(that){
that.input.after('统计:<span class="j-num"></span>');
that.input.on('keyup', function(){
that.render();
});
};
var TextCountFun = function(config){
this.input = config.input;
};
TextCountFun.prototype = {
//暴露出去的公有接口
init: function(){
_bindNumControl(this);
return this;
},
render: function(){
this.num = this.input.next('.j-num');
this.num.html(this.input.val().length);
}
};
return TextCountFun;
}());
$(function(){
var text1 = new TextNumCom({
input:$('#j-input-1')
});
var text2 = new TextNumCom({
input:$('#j-input-2')
});
text1.init().render();
text2.init().render();
});
Step5:
- 使用面向对象的方式去写组件,让代码有统一的风格
- 假设使用 John Resig 的 class
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 ($('#J_input_count').length == 0) {
this.input.after('<span id="J_input_count"></span>');
};
$('#J_input_count').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:"#J_input"
});
})
Step6:
- 抽象出Base代码,组件有些方法,是大部分组件都会有的
- init 初始化属性
- render 渲染
- bind 事件绑定
- destroy 销毁
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(){
}
})
Step7:
自定义事件可以参考
- 引入事件机制 观察者模式 fire on
- 通过on监听
- 通过fire触发
//辅组函数,获取数组里某个元素的索引 index
var _indexOf = function(array,key){
if (array === null) return -1
var i = 0, length = array.length
for (; i < length; i++) if (array[i] === item) return i
return -1
}
var Event = Class.extend({
//添加监听
on:function(key,listener){
//this.__events存储所有的处理函数
if (!this.__events) {
this.__events = {}
}
if (!this.__events[key]) {
this.__events[key] = []
}
if (_indexOf(this.__events,listener) === -1 && typeof listener === 'function') {
this.__events[key].push(listener)
}
return this
},
//触发一个事件,也就是通知
fire:function(key){
if (!this.__events || !this.__events[key]) return
var args = Array.prototype.slice.call(arguments, 1) || []
var listeners = this.__events[key]
var i = 0
var l = listeners.length
for (i; i < l; i++) {
listeners[i].apply(this,args)
}
return this
},
//取消监听
off:function(key,listener){
if (!key && !listener) {
this.__events = {}
}
//不传监听函数,就去掉当前key下面的所有的监听函数
if (key && !listener) {
delete this.__events[key]
}
if (key && listener) {
var listeners = this.__events[key]
var index = _indexOf(listeners, listener)
(index > -1) && listeners.splice(index, 1)
}
return this;
}
})
Step8: 希望Base拥有事件机制
var Base = Class.extend(Event,{
init: function(){},
get: function(){},
set: function(){},
...
destroy:function(){
//去掉所有的事件监听
this.off()
}
})
Step9: 需要进一步扩展Base 形成RichBase
- 事件代理:不需要用户自己去找dom元素绑定监听直接绑定在parentNode身上,也不需要用户去关心什么时候销毁。
- 模板渲染:用户不需要覆盖render方法,而是覆盖实现setUp方法。可以通过在setUp里面调用render来达到渲染对应html的目的。
- 单向绑定:通过setChuckdata方法,更新数据,同时会更新html内容,不再需要dom操作。