let`s rock
Bazinga!Bazinga!Bazinga! 「 The Big Bang Theory 」Sheldon
基础类, 集成了
Class
,Attrs
,Events
,Aspect
这四部分的所有特性. 一旦自定义类继承了Base
, 你就可以使用这四部分提供的所有 API.接下来, 简单地介绍下这四部分.
提供简洁的 OO 实现.
// 1) 创建一个类
var Class = FNX.include('class/class');
var SomeClass = Class.create({
// 扩展自哪个类
Extends: Animal,
// 把别人的方法和属性搬过来放到 SomeClass.prototype 上
Implements: Flyable,
// 类的静态属性, 放到 SomeClass 上
Statics: {
COLOR: 'red'
},
// 初始化, 会在实例化对象时自动调用
initialize: function() {
}
});
// 2) Class.create(parent, {}) 的 parent 参数等价于上面例子中的 Extends
var SomeClass = Class.create(Animal, {});
// 3) Class 创建出来的类具有 extend 方法, 可再次扩展出其他子类
var OtherClass = SomeClass.extend({
// 覆盖父类方法
initialize: function() {
// 注意: 需要手工调用父类的初始化过程
OtherClass.superclass.initialize.call(this);
// do my stuff
},
// 也可以覆盖父类的 Implements, Statics 等
Implements: Swimming,
Statics: {
COLOR: 'green',
WIDTH: 1
}
});
// 4) Class 创建出来的类具有 implement 方法, 作用是和定义类时的 Implements 相同,
// 将其他对象的方法原样搬过来.
SomeClass.implement({
method1: function () {}
});
// 5) Class(fn) 将现有 Function 对象转成 Class 类.
var EmptyClass = Class(function() {});
Base 类就是用 Class.create
创建, Implements
了 Events
, Attrs
, Aspect
这三部分. 另外, Base 增加了 destroy
方法, 相对于 initialize
, 主要用于处理在实例销毁时的扫尾工作.
提供基本的事件添加, 移除和触发功能.
继承自 Base 的类, 自动具有 Events 的这三个 API:
obj.on(event, callback, [context])
添加事件obj.off([event], [callback], [context])
移除事件obj.trigger(event, [*args])
触发事件简单提供了两个 API:
obj.before(methodName, callback, [context])
在 obj.methodName 执行之前先执行 callbackobj.after(methodName, callback, [context])
在 obj.methodName 执行之后再执行 callback注意点:
false
, 会阻止原函数的执行.对类属性的统一管理.
首先, 类的 attrs 和 property 虽然都可翻译成属性, 但我们要着重说明下两者的不同概念和使用场景:
attrs 是指存储在类 attrs 对象中的, 通过 get/set 接口来读写的属性, 一般是可以外部传入的配置属性;
property 是指直接放在实例上的, 可直接读写的公共或私有属性, 可以是临时变量, 如 obj._originStyle, 也可以是需要暴露出去的关键属性, 如 obj.element, obj.length 等;
推荐是使用 attrs 来统一定义和管理类属性.
attrs
对象上.Base.extend({
attrs: {
attr1: 1
}
});
{
// 方式一: 直接设置默认值
attr1: "aString",
// 方式二: 通过 value 设置默认值, 等价于方式一
attr2: {
value: "bString"
},
// 方式三: 设置 setter
attr3: {
value: "cString",
// setter 会在实例调用 set() 时触发, 可以在此时做些处理,
// 比如强制类型转换
// 即当 obj.set("attr3", 1) 后, 会调用 setter, 转换成 '1'
// setter 的 this 为当前实例
setter: function(v) {
return v + ""
}
},
// 方式四: 设置 getter
attr4: {
value: 10,
// getter 会在实例调用 get() 时触发, 同样可以在此时做些处理,
// 比如存的是美元, 转成人民币
// 即当 obj.get("attr4") 后, 会调用 getter
// getter 的 this 为当前实例
getter: function(v) {
// 美元 * 汇率 = 人民币
return v * 6.8
}
},
// 方式五: readOnly 只读属性
attr5: {
value: 0,
// 设置 readOnly 之后, 没法通过 obj.set() 的方式设置值, 即不可更改属性值
// 但可以设置 getter 来调整
// 默认 readOnly 为 false
readOnly: true,
getter: function() {
return Math.ceil(this.get('panels').length / this.get('step'));
}
}
}
propsInAttrs
之后, 会把 attrs
中同名的 attribute
直接添加到 this
上.Base.extend({
propsInAttrs: ['element'],
attrs: {
element: null
}
});
// 即当初始化后 this.element = this.get('element')
// 注意: 初始化后 this.element 等于 this.get('element'), 但之后再设置各自值, 不会相互同步
obj.set(key, value, options)
设置某个属性的值, 并触发change:x
事件.options.silent: 默认为 false
. true
时不会触发 change:x
事件.
options.override: 默认为 false
. 当 value 为一个复杂对象, 默认会递归 merge. 为 true
时, 强制覆盖原值, 不进行 merge.
obj.set('attr1', {
'name': 'fnx',
'version': '1.0'
});
obj.set('attr1', {
'family': 'alipay'
}, {
override: true
});
obj.get(key)
获取某个属性的值.在实例化时, 会:
change:x
事件_onChangeX
方法到 change:x
事件的回调队列中这些操作对于使用者来说, 是透明的, 所以你不用关心去初始化属性.
哪些情况下使用 Base?
满足以下条件的两个即可使用 Base:
哪些不需要?
Messager
, Position
建议你直接使用 Class.create
创建类, 再 Implements 你需要的 Attrs
, Events
, Aspect
. 或者, 更加简单的功能, 直接 function
定义就好.
其他一些使用建议
尽可能统一管理变量, 作为 attrs
中的 attribute, 而不是 property
推荐使用 _onChangeX
, 不需要用户手工绑定 change:x
事件
var MyClass = Base.extend({
attrs: {
element: $('input#name'),
name: {
value: '',
setter: function(val) {
return val || ''
},
getter: function(val) {
return val + ''
}
}
},
propsInAttrs: ['element'],
initialize: function(options) {
MyClass.superclass.initialize.call(this, options);
var that = this;
this._async = function() {
that.set('name', that.element.val(), {
silent: true
});
};
this.element.on('change', this._async);
},
say: function() {
alert(this.get('name'));
},
destroy: function() {
this.element.off('change', this._async);
MyClass.superclass.destroy.call(this);
},
_onChangeName: function(val) {
this.element.val(val);
}
});
// use MyClass like:
my = new MyClass();
my.before('say', function() {
if (!this.get('name')) return false;
});
my.set('name', 'fnx');
setTimeout(function() {
my.element.val('change name with fnx js');
my.element.change();
my.say();
my.element.val('');
my.element.change();
my.say();
my.destroy();
}, 3000);
Widget 是 UI 组件的基础类, 约定了组件的基本生命周期. 继承自 Base, 拥有 Base 的一切功能外, 还提供了其他一些通用功能. Widget 组件具有四个要素: 描述状态的 attribute 和 property, 描述行为的 event 和 method.
Widget 有一套完整的生命周期, 控制着组件从创建, 到正式渲染, 再到销毁的整个过程.
整个过程如下图所示:
该值为组件实例在当前页面上的唯一标识. 在之后的渲染, 赋值在 this.element
的 data-widget-cid
属性上.
在解析 data-api 之后, 执行父类 Base 的 attribute 的初始化, 包含设置属性和绑定属性事件.
一个正常的 UI 组件在 DOM 中一定有个根 DIV, 这就是 this.element.
它可以是已有的 DOM 元素, 也可以是根据 template 创建的一个新 DOM 元素.
// 1) 使用已有 DOM 元素
var o1 = new Widget({
element: '#b'
});
console.log(o1.element);
// 2) 从 template 中创建
var o2 = new Widget({
template: '<div style="width: 100px;color: red;">目标元素1</div>',
parentNode: '#c'
});
console.log(o2.element);
在 Widget 初始化结束后, this.element 已经存在, 但未必在 DOM 树中. 不存在 DOM 中的 this.element 只有在调用 this.render()
后才真正插入到文档流. 默认插入到 document.body
, 可以通过 parentNode
指定.
负责 properties 的初始化, 提供给子类覆盖.
var MyWidget = Widget.extend({
attrs: {
width: 100,
height: 200
},
initProps: function() {
this.length = this.get('width') * this.get('height');
}
});
如果你设置了 this.events
, 那么会在初始化时默认绑定这些事件.
var MyWidget = Widget.extend({
attrs: {
inputClass: 'ui-input',
itemClass: 'ui-form-item'
},
events: {
'mouseenter .{{attrs.inputClass}}': 'mouseenter',
'mouseleave .{{attrs.inputClass}}': 'mouseleave',
'focus .{{attrs.itemClass}} input,textarea,select': 'focus',
'blur .{{attrs.itemClass}} input,textarea,select': 'blur'
}
});
专为子类提供的初始化接口, 可以直接覆盖定义.
var MyWidget = Widget.extend({
attrs: {
inputClass: 'ui-input',
itemClass: 'ui-form-item'
},
events: {
'mouseenter .{{attrs.inputClass}}': 'mouseenter',
'mouseleave .{{attrs.inputClass}}': 'mouseleave',
'focus .{{attrs.itemClass}} input,textarea,select': 'focus',
'blur .{{attrs.itemClass}} input,textarea,select': 'blur'
},
setup: function() {
// todo: init sth
}
});
渲染的过程:
_onRenderXXX
到 change:attr 事件上var MyWidget = Widget.extend({
attrs: {
class: 'ui-form-item'
},
_onRenderClass: function(val) {
this.element.addClass(val);
}
});
var my = new MyWidget({
element: '#b'
});
my.render();
var sth = new SomeWidget();
sth.render();
为何 new 的时候不直接 render ? 非得调用一次 render()
?
render 这一步操作从初始化中独立出来, 是因为考虑到有些组件在初始化的时候并不想操作 DOM, 比如 Dialog. 初始化只是准备, 只有当真正显示(渲染)时才需要插入到 DOM 中.
如果希望实例化的时候就渲染到页面上, 可在 setup()
里直接调用 this.render()
.
为了避免同组件引入样式的冲突问题, 在 this.render()
时, 会给 this.element
包裹一个 DIV, 并给这个 DIV 添加形如 famliy-name-version
的样式类, 定义的 CSS 也会加此前缀. 这样就能避免样式冲突问题. 这步操作对开发者是透明的
如果你的类都定义了 _onChangeX
与 _onRenderX
, 当 obj.set('x', 'a new value')
之后, 触发 change:x
后都会执行这两个处理函数. 但两者的区别是:
绑定时机不同: _onChangeX
是实例初始化后绑定的, _onRenderX
是在实例 render 之后才绑定的
用途也稍有不同: _onRenderX
是对 x 改变后触发 UI 的变化, 而 _onChangeX
范围更广些.
var MyWidget = Widget.extend({
attrs: {
class: null,
count: 0
},
events: {
'click': 'click'
},
click: function() {
this.set('class', 'class-three');
console.log(this.get("count"), my.get("class"));
},
_onChangeClass: function(val) {
this.set('count', this.get('count') + 1);
},
_onRenderClass: function(val) {
this.element.addClass(val);
}
});
var my = new MyWidget({
template: '<div style="background: #efefef;width: 100px;height: 100px;">text</div>'
});
my.set("class", "class-one");
console.log(my.get("count"), my.get("class"));
setTimeout(function() {
my.render();
console.log(my.get("count"), my.get("class"));
setTimeout(function() {
my.set("class", "class-two");
console.log(my.get("count"), my.get("class"));
}, 6000);
}, 6000);
Widget 默认绑定了 window 的 unload
事件, 当页面 unload
时, 触发当前页面上所有 Widget 实例的 destroy()
.
在 Widget 根元素 this.element 下查找符合选择器的元素
前文提到的在初始化过程中, 可以通过 this.events
在初始化时绑定事件. 在初始化之后, 可以通过 this.delegateEvents()
代理事件. 事件默认绑定在 this.element
上.
var myWidget = new Widget();
// 1) 事件代理在 `element` 上
myWidget.delegateEvents({
'click p': 'fn1',
'click li': function() {}
})
// 2) 事件代理在 `element` 上
myWidget.delegateEvents('click p', fn1)
myWidget.delegateEvents('click p', function() {})
// 3) 代理在 `element` 以外的 DOM 上
myWidget.delegateEvents('#trigger', 'click p', fn1)
// 等价于 `$('#trigger').on('click', 'p', fn1)`
myWidget.delegateEvents($('#trigger'), 'click', function() {})
// 4) 删除事件代理
myWidget.undelegateEvents();
myWidget.undelegateEvents(events);
myWidget.undelegateEvents(element, events);
提供此 API, 是为了统一管理组件的所有事件. 这在销毁组件时, 可以方便地解绑所有事件以防止内存泄露. 强烈推荐使用此 API 来绑定元素事件.
var dlg = Widget.query('selector')
用于查找对应 selector 的 DOM 节点关联起来的 Widget 对象.
如何选择使用 Base 还是 Widget?
var MyWidget = Widget.extend({
attrs: {
name: ''
},
setup: function() {
this.delegateEvents('click', this.say);
this.render();
this.before('show', function() {
if (!this.get('name')) return false;
});
this.before('say', function() {
if (!this.get('name')) return false;
});
},
show: function() {
this.element.show();
},
hide: function() {
this.element.hide();
},
say: function() {
alert(this.get('name'));
},
_onRenderName: function() {
this.element.html(this.get('name'));
}
});
var my = new MyWidget({
template: '<div class="name-title" style="display: none;"></div>'
});
my.show();
setTimeout(function() {
my.set('name', 'hi, fnx');
my.show();
}, 5000);