blob: 3938f0762fa765a89f0731146911b440ff677c94 [file] [log] [blame]
/**
* @file observable、data-driven 机制相关
* @author sushuang(sushuang@baidu.com)
*/
define(function (require) {
var base = require('./base');
var $ = require('jquery');
var throttle = require('./throttle');
var arraySlice = Array.prototype.slice;
// 虽然 knockout(ko、mvvm)会带来较清洁的程序结构和编码方便,
// 但是决定不从底层使用其data-bind和template,原因是:
// 当 ko 和 jq/其他已有控件 混用时,不容易避免内存泄露。
// 当不使用ko提供的dom操作删除dom时,内存泄露可能发生在:
// · 为dom节点建立的computed的难于析构(尽管有默认的disposeWhen,也不是一定会触发)。
// · 在dom节点中建立的bindingContext的引用的难于解除。
// 但是 ko 的 observer 机制,以及数据驱动的方式,是可以利用的,
// 认为会对组件交互较多的程序结构带来益处。
var lib = {};
// ko只提供了observable和computed的区分,
// 没有提供observable和observableArray的区分,所以加标志。
var OB_TYPE_PROP = '\x0E__ob__type__';
var PROP = '\x0E__prop__'; // 为了避免错误的编码方式:扩展时访问私有变量
var DEFAULT_EVENT = 'change';
var ARRAY_CHANGE_EVENT = 'arrayChange';
var UNDEFINED;
// ---------------------------------------------------------
// subscribable
// ---------------------------------------------------------
/**
* 加事件功能。重写了这个因为需要一些改变
*
* @public
*/
lib.subscribable = function (o) {
base.assign(o, subscribableMethods);
o._subscriptions = {};
};
var subscribableMethods = {
subscribe: function (callback, scope, event) {
event = event || DEFAULT_EVENT; // 缺省则是change事件
var subscList = this._subscriptions[event]
|| (this._subscriptions[event] = []);
var subscription = new Subscription(
callback,
scope,
function () {// disposer
base.arrayRemoveItem(subscList, subscription);
}
);
subscList.push(subscription);
return subscription;
},
notify: function (args, event) {
event = event || DEFAULT_EVENT; // 缺省则是change事件
if (!this.hasSubscriptionsForEvent(event)) {
return;
}
// 克隆一份因为过程中可能有dispose
var subscList = this._subscriptions[event].slice();
for (var i = 0, subsc; subsc = subscList[i]; i++) {
subsc.callback(args);
}
},
hasSubscriptionsForEvent: function (event) {
return this._subscriptions[event] && this._subscriptions[event].length;
},
isDifferent: function (oldValue, newValue) {
return !this.equalityComparer || !this.equalityComparer(oldValue, newValue);
},
equalityComparer: valuesArePrimitiveAndEqual
};
var primitiveTypes = {'undefined': 1, 'boolean': 1, 'number': 1, 'string': 1};
// 对于非primitive的值,无论如何都会different,从而触发change事件。
// 如果不这样处理,非primitive的值修改后触发change事件会有困难。
function valuesArePrimitiveAndEqual(a, b) {
var oldValueIsPrimitive = (a === null) || ($.type(a) in primitiveTypes);
return oldValueIsPrimitive ? (a === b) : false;
}
// ---------------------------------------------------------
// subscription
// ---------------------------------------------------------
/**
* @class
*/
var Subscription = function (callback, callbackScope, disposeCallback) {
this[PROP + 'callback'] = callback;
this[PROP + 'callbackScope'] = callbackScope;
this[PROP + 'disposeCallback'] = disposeCallback;
this[PROP + 'isDisposed'] = false;
};
var subscriptionProto = Subscription.prototype;
subscriptionProto.callback = function (args) {
if (!this[PROP + 'isDisposed']) {
this[PROP + 'callback'].apply(this[PROP + 'callbackScope'], args || []);
}
return this;
};
// 设置callback
subscriptionProto.setCallback = function (callback) {
this[PROP + 'callback'] = callback;
return this;
};
// 得到callback
subscriptionProto.getCallback = function () {
return this[PROP + 'callback'];
};
subscriptionProto.dispose = function () {
this[PROP + 'isDisposed'] = true;
this[PROP + 'disposeCallback']();
return this;
};
// 如果反复调用的间隔低于limit,则会按limit给定的频率执行。
subscriptionProto.throttle = function (limit) {
this[PROP + 'callback'] = throttle.fixedRate(this[PROP + 'callback'], limit);
return this;
};
// 在limit给定的时间间隔内反复调用,只执行最后一次。
subscriptionProto.debounce = function (limit) {
this[PROP + 'callback'] = throttle.debounce(this[PROP + 'callback'], limit);
return this;
};
// ---------------------------------------------------------
// composite subscription
// ---------------------------------------------------------
/**
* 组合的subscription
*
* @class
*/
var CompositeSubscription = function (subscriptions) {
this[PROP + 'children'] = $.extend([], subscriptions);
};
var compositeSubscriptionProto = CompositeSubscription.prototype;
compositeSubscriptionProto.callback = function (args) {
for (var i = 0, child; child = this[PROP + 'children'][i]; i++) {
child.callback(args);
}
};
compositeSubscriptionProto.dispose = function () {
for (var i = 0, child; child = this[PROP + 'children'][i]; i++) {
child.dispose();
}
};
// 如果反复调用的间隔低于limit,则会按limit给定的频率执行。
compositeSubscriptionProto.throttle = function (limit) {
compositeSubscriptionDelay.call(this, limit, throttle.fixedRate);
return this;
};
// 在limit给定的时间间隔内反复调用,只执行最后一次。
compositeSubscriptionProto.debounce = function (limit) {
compositeSubscriptionDelay.call(this, limit, throttle.debounce);
return this;
};
function compositeSubscriptionDelay(limit, delayMethod) {
var fns = [];
var i;
var child;
var children = this[PROP + 'children'];
for (i = 0; child = children[i]; i++) {
fns.push(child.getCallback());
}
fns = delayMethod(fns, limit);
for (i = 0; child = children[i]; i++) {
child.setCallback(fns[i]);
}
}
// ---------------------------------------------------------
// observable
// ---------------------------------------------------------
/**
* 创建一个ob(observable)实例。
* 这个简写“ob”是为了编码方便。ob,在本工程内多处使用。是基础概念。
*
* @public
* @param {(*|ob)} initValue 初始值。
* 如果传入ob,则从此ob中peek出初始值来初始化。
* @param {*} initValueInfo 初始值描述。
* @param {Object=} options
* @param {string=} options.authKey 设置一个authKey,则以后每次更新时必须传入同样authKey才能更新,否则抛出异常。
* @return {ob} observable
*/
lib.ob = lib.observable = function (initValue, initValueInfo, options) {
if (obTypeOf(initValue)) {
initValue = initValue.peek();
initValueInfo = initValue.peekValueInfo();
}
/**
* observable实例
*
* @param {*=} value 要设置的新值。如果缺省,表示要读取值。
* @param {*=} valueInfo 额外的信息,可用于描述value。
* @param {Object=} options
* @param {string=} options.authKey
* @param {boolea=} options.force
* @param {boolea=} options.silent
*/
function observable(value, valueInfo, options) {
// Write
if (arguments.length > 0) {
observable[PROP + 'writer'](value, valueInfo, options);
return;
}
// Read
else {
return observable[PROP + 'currValue'];
}
}
// Private Members
observable[PROP + 'currValue'] = initValue; // 当前
observable[PROP + 'currValueInfo'] = initValueInfo; // 当前
observable[PROP + 'authKey'] = options ? options.authKey : null;
lib.subscribable(observable);
base.assign(observable, obMethods);
observable[OB_TYPE_PROP] = 'ob';
/**
* 原始的写入器。
*
* @private
* @param {*} value
* @param {*=} valueInfo
* @param {Object=} options
* @param {string=} options.authKey
* @param {boolea=} options.force 默认false
* @param {boolea=} options.silent 默认false
* @param {Array=} options.volatiles 例如['aa'],表示valueInfo中aa字段不持久化,事件触发完即删除。
*/
observable[PROP + 'writer'] = function (value, valueInfo, options) {
observable.validateAuthKey(options ? options.authKey : null);
// value info 每次都会写入
base.assert(valueInfo == null || $.isPlainObject(valueInfo));
observable[PROP + 'currValueInfo'] = valueInfo;
// 如果值没有变化,则不会设值,除非extend了always
var force = options && options.force;
if (force || observable.isDifferent(observable[PROP + 'currValue'], value)) {
(!options || !options.silent) && observable.valueWillMutate();
observable[PROP + 'currValue'] = value;
(!options || !options.silent) && observable.valueHasMutated();
}
deleteVolatiles(this, options);
};
return observable;
};
var obMethods = {
/**
* 强制写入,忽视isDifferent判断
*
* @public
* @param {*} value
* @param {*=} valueInfo
* @param {Object=} options
* @param {string=} options.authKey
* @param {boolean=} options.silent
* @param {Array=} options.volatiles
*/
force: function (value, valueInfo, options) {
options = options || {};
options.force = true;
this[PROP + 'writer'](value, valueInfo, options);
},
/**
* 得到当前值
*
* @public
* @return {*}
*/
peek: function () {
return this[PROP + 'currValue'];
},
/**
* 得到ob的value info。
*
* @public
* @param {string=} key 如果给定key,则认为valueInfo是object,并从中取出value。
* @return {*} valueInfo
*/
peekValueInfo: function (key) {
if (arguments.length === 0) {
var obj = this[PROP + 'currValueInfo'];
if (obj) {
obj = $.extend({}, obj);
}
return obj;
}
else if (base.isObject(this[PROP + 'currValueInfo'])) {
return this[PROP + 'currValueInfo'][key];
}
},
/**
* 扩展writer方法。
*
* @public
* @param {Function} fn 扩展的方法
* 参数为 {Function} writer, {*} value, {*} valueInfo, {boolean} force;
* this为 ob 自身。
* 扩展方法中,在合适的地方可以调用 writer(value, valueInfo),真正设值。
* @return {ob}
*/
extendWriter: function (fn) {
var observable = this;
var old = observable[PROP + 'writer'];
this[PROP + 'writer'] = function (value, valueInfo, options) {
observable.validateAuthKey(options ? options.authKey : null);
fn.call(observable, old, value, valueInfo, options);
};
return this;
},
/**
* value 被更新后调用。有时在对ob的行为进行扩展的场景会手动调用。
*
* @public
*/
valueHasMutated: function () {
this.notify([this[PROP + 'currValue'], this]);
},
/**
* 较少使用,value 被更新前调用。有时在对ob的行为进行扩展的场景会手动调用。
*
* @pubilc
*/
valueWillMutate: function () {
this.notify([this[PROP + 'currValue'], this], 'beforeChange');
},
/**
* @public
*/
validateAuthKey: function (inputAuthKey) {
var authKey = this[PROP + 'authKey'];
if (authKey != null && inputAuthKey !== authKey) {
throw new Error('AuthKey error: ' + inputAuthKey);
}
}
};
/**
* 创建 obHash 实例。
* 使用obHash.prop()方法来set值,则会触发change事件。
*
* @public
* @param {Object} v 原始对象
* @return {obHash} observableHash
*/
lib.obHash = function (v) {
var ob = lib.ob(v || {});
ob[OB_TYPE_PROP] = 'obHash';
// 增补了prop方法,用于get set属性(并触发change事件)
ob.prop = obHashProp;
return ob;
};
/**
* 设置 或者 获取 obHash 的属性值。
* 设置属性值的时候,如果于源属性值不同,则会触发change事件。
*
* @public
* @param {string} key 属性名
* @param {*=} value 如不传,表示取值
* @param {Object=} options
* @param {boolean=} options.force
* @param {boolean=} options.silent
* @param {Array=} options.volatiles
* @return {*} 取到的值,或者设置的值回传
*/
function obHashProp(key, value, valueInfo, options) {
// Write
if (arguments.length > 1) {
this[PROP + 'currValueInfo'] = valueInfo;
var innerObj = this();
if ((options && options.force) || this.isDifferent(innerObj[key], value)) {
(!options || !options.silent) && this.valueWillMutate();
innerObj[key] = value;
(!options || !options.silent) && this.valueHasMutated();
}
deleteVolatiles(this, options);
}
// Read
else {
return this()[key];
}
}
/**
* 创建 subscribable dependency 包装。
* 不推荐使用此方法。推荐使用 ob.subscribe + lib.fixedRate or lib.debounce。
* 严重注意:
* (1)慎用computed,除非你能明确控制 fn 中对ob进行的求值。
* 因为每次求值都会建立对ob的依赖关系。
* 注意,fn的代码中的函数调用里是否又有对ob的求值,并不那么容易发现,也不易于代码可读。
* (2)对ob的监听函数,如果对执行顺序有要求,则尽量不用computed。
* 因为其不完全能保证执行顺序(详情参见ko源码)。
* (3)创建computed时,fn 会首先自动执行一次。
*
* @param {Function} fn 依赖描述函数
* @param {Object} scope
* @return {ob} dependentObservable
*/
// lib.obComputed = function (fn, scope) {
// var o = ko.dependentObservable(fn, scope);
// o[OB_TYPE_PROP] = 'obComputed';
// return o;
// };
/**
* 创建 obArray 实例
*
* @public
* @param {Array} v 原始数组
* @return {obArray} observableArray
*/
lib.obArray = function (v) {
base.assert(v === UNDEFINED || $.isArray(v));
var o = lib.ob(v || []);
base.assign(o, obArrayMethods);
o[OB_TYPE_PROP] = 'obArray';
return o;
};
var obArrayMethods = {
/**
* 删除所有value
*
* @public
* @param {*|Function} valueOrPredicate 要被删除的值,或匹配函数。
* 如果为匹配函数,则输入为每项,输入为true则表示删除。
* @return {Array} 被删除的内容
*/
remove: function (valueOrPredicate) {
var arr = this();
var removed = [];
var willInvoked;
var indexes = [];
for (var i = 0, len = arr.length; i < len;) {
if (typeof valueOrPredicate === 'function'
? valueOrPredicate(arr[i])
: arr[i] === valueOrPredicate
) {
if (!willInvoked) {
this.valueWillMutate();
willInvoked = true;
}
removed.push(arr.splice(i, 1));
indexes.push(i);
len--;
}
else {
i++;
}
}
if (removed.length) {
var op = {key: ChangeKey.REMOVE, indexes: indexes};
this.notify([op, this], ARRAY_CHANGE_EVENT);
this.valueHasMutated();
}
return removed;
},
/**
* 删除所有
*
* @public
* @return {Array} 被删除的内容
*/
removeAll: function () {
return this.splice(0, this.count());
},
/**
* 从前向后的第一个value的index。未找到返回-1。
*
* @public
*/
indexOf: function (value) {
return base.arrayIndexOf(this(), value);
},
/**
* @public
*/
pop: function () {
return this.splice(this.count() - 1, 1);
},
/**
* @public
* @param {*...} value 可多个value
*/
push: function () {
return this.splice.apply(
this, [this.count(), 0].concat(arraySlice.call(arguments))
);
},
/**
* @public
*/
shift: function () {
return this.splice(0, 1);
},
/**
* @public
*/
unshift: function (value) {
return this.splice(0, 0, value);
},
/**
* 参数并不完全按照Array splice的规约,如果没有满足如下限定,则什么也不做。
*
* @public
* @param {number} index 起始,范围是:0到当前length。
* @param {number=} removeCount 删除长度,如果非大于等于0则表示0,如果缺省表示全删除。
* @param {*...} ... 要增加的项
* @return {Array} 被删除的项
*/
splice: function (index, removeCount) {
var arr = this();
var len = arr.length;
var argLen = arguments.length;
// 什么也不做的情况
if (!argLen
|| !(index >= 0 && index <= len)
) {
return [];
}
// 修正removeCount
if (argLen === 1) {
removeCount = len;
}
else if (!(removeCount >= 0)) {// jshint ignore:line
removeCount = 0;
}
else if (removeCount > len - index) {
removeCount = len - index;
}
this.valueWillMutate();
var ret = arr.splice.apply(arr, arguments);
var op = {
key: ChangeKey.SPLICE,
index: index,
removeCount: removeCount,
added: arraySlice.call(arguments, 2)
};
this.notify([op, this], ARRAY_CHANGE_EVENT);
this.valueHasMutated();
return ret;
},
/**
* @public
*/
slice: function () {
return arraySlice.apply(this(), arguments);
},
/**
* 增补方法,批量push
*
* @public
* @param {Array} arr
*/
pushArray: function (arr) {
return this.push.apply(this, arr || []);
},
/**
* 增补方法,得到长度
*
* @public
* @return {number} 当前长度
*/
count: function () {
return this().length;
},
/**
* 增补方法,交换两个元素
*
* @public
* @param {number} originIndex
* @param {number} finalIndex
*/
move: function (originIndex, finalIndex) {
var arr = this();
var len = arr.length;
// 什么也不做的情况
if (originIndex < 0
|| originIndex >= len
|| finalIndex < 0
|| finalIndex >= len
|| originIndex === finalIndex
) {
return;
}
// 修正removeCount
this.valueWillMutate();
var value = arr.splice(originIndex, 1)[0];
arr.splice(finalIndex, 0, value);
var op = {
key: ChangeKey.MOVE,
originIndex: originIndex,
finalIndex: finalIndex
};
this.notify([op, this], ARRAY_CHANGE_EVENT);
this.valueHasMutated();
}
};
/**
* 用于arrayChange事件
*/
var ChangeKey = lib.obArray.ChangeKey = {
REMOVE: 'remove', // 参数:indexes
SPLICE: 'splice', // 参数:index, removeCount, added
MOVE: 'move' // 参数:originIndex, finalIndex
};
// ---------------------------------------------------------
// utilities
// ---------------------------------------------------------
/**
* 注册监听。
* 可传入多个ob,意思任意一个ob都可以触发这个监听。
* 并且同样可以在返回的subscription对象上施加 throttle 或 debounce
*
* @param {(Object|Array.<Object>)} ob 如果不是ob,则什么也不做。
* @param {Function} fn 回调函数
* @param {Object} scope
* @param {string=} event 如果缺省,则取ob.subscribe(...)的默认行为
* @return {Object=} subscription对象
*/
lib.obSubscribe = function (ob, fn, scope, event) {
// 单个ob的情况
if (obTypeOf(ob)) {
return ob.subscribe(fn, scope, event);
}
// 多个ob的情况
else if ($.isArray(ob)) {
var subscList = [];
for (var i = 0, len = ob.length; i < len; i++) {
if (obTypeOf(ob[i])) {
subscList.push(ob[i].subscribe(fn, scope, event));
}
}
return new CompositeSubscription(subscList);
}
};
/**
* 得到值。自动类型判断。适应ko等情况
*
* @public
* @param {*} target
* @return {*} value
*/
lib.value = function (target) {
return obTypeOf(target) ? target() : target;
};
/**
* 得到值。
* 和val区别是:如果在lib.obComputed中执行,不会建立依赖。
*
* @public
* @param {*} target
* @return {*} value
*/
lib.peek = function (target) {
return obTypeOf(target) ? target.peek() : target;
};
/**
* 向ob设置值。一般使用方式:
* some = lib.setValue(some, 'zzzzzz');
* some = lib.setValue(some, anotherOb);
* 这样不论some是否是ob,都可以被成功设值
*
* @public
* @param {*} target
* @param {*} newVal
* @param {*=} valueInfo
* @return {*} target or newVal
*/
lib.setValue = function (target, newVal, valueInfo) {
newVal = lib.peek(newVal);
return isValueOb(target)
? (target(newVal, valueInfo), target) : newVal;
};
/**
* 标准化的 valueInfo
*
* @public
* @param {(Object|string)} infoOrType 如果是Object,表示info对象。如果是string表示type
* @param {*=} info 例如可为控件的uid。
*/
lib.valueInfo = function (infoOrType, info) {
if (base.isObject(infoOrType)) {
return base.assign({}, infoOrType, ['type', 'info']);
}
else {
return {type: infoOrType, info: info};
}
};
/**
* valueInfo类型,作为常量是为了统一概念
*/
lib.valueInfo.CONFIRMED = 'confirmed'; // 确认值变更,从而可以向server端发起存储
/**
* 一个常用简写
*
* @public
* @param {*=} uid 控件的uid。
* @param {Object=} extraData 额外的信息
*/
lib.valueInfoForConfirmed = function (uid, extraData) {
var valueInfo = lib.valueInfo(lib.valueInfo.CONFIRMED, uid);
if (extraData) {
base.assign(valueInfo, extraData, null, ['type', 'info']);
}
return valueInfo;
};
/**
* 检查valueInfo的方便方法
*
* @public
* @param {(Object|string)} obOrValueInfo。为空时总返回false。
* @param {string=} type 参见lib.valueInfo的类型,不传则不检查此项
* @param {*=} info 不传则不检查此项
* @return {boolean} 是否通过检查
*/
lib.checkValueInfo = function (obOrValueInfo, type, info) {
var valueInfo = lib.obTypeOf(obOrValueInfo)
? obOrValueInfo.peekValueInfo()
: obOrValueInfo;
return base.isObject(valueInfo) // 须是标准的valueInfo
&& (type == null || valueInfo.type === type)
&& (info == null || valueInfo.info === info);
};
/**
* 一个常用简写
*
* @public
* @param {Object} ob observable object
* @param {*=} uid 控件的uid。如果不传,则不检查uid。
* @return {boolean} whether confirmed.
*/
lib.checkValueInfoForConfirmed = function (ob, uid) {
return lib.checkValueInfo(ob, lib.valueInfo.CONFIRMED, uid);
};
/**
* 得到ob类型
*
* @public
* @param {*} o
* @return {*} 值
*/
var obTypeOf = lib.obTypeOf = function (o) {
return Object(o) === o && o[OB_TYPE_PROP];
};
/**
* 是否是值型的ob
*
* @private
* @param {string=} o 如果为'value',表示值型的ob。一般内部使用。
* @return {boolean}
*/
function isValueOb(o) {
return ({ob: 1, obArray: 1, obHash: 1}).hasOwnProperty(obTypeOf(o));
}
/**
* @private
*/
function deleteVolatiles(ob, options) {
var volatiles = options && options.volatiles || [];
var valueInfo = ob[PROP + 'currValueInfo'];
for (var i = 0, len = volatiles.length; i < len; i++) {
delete valueInfo[volatiles[i]];
}
}
return lib;
});