只有158行代码的 observerjs 确实很吸引人,奔着如何观察复杂对象和数组的变化的目的啃了一遍源码,确实有收获

直接贴加了注释的源码~~

/* observejs --- By dnt http://kmdjs.github.io/
* Github: https://github.com/kmdjs/observejs
* MIT Licensed.
*/
;
(function(win) {
var observe = function(target, arr, callback) {
/**
* 构造函数
* @param target 观察的对象
* @param arr 观察的属性
* @param callback 属性变化时触发的回调函数
*/
var _observe = function(target, arr, callback) {
if (!target.$observer) {
target.$observer = this;
}
var $observer = target.$observer;
var eventPropArr = []; // 保存 target 中需要观察的属性
if (observe.isArray(target)) {
if (target.length === 0) {
target.$observeProps = {}; // 用于储存 target 的路径和各个属性的值
target.$observeProps.$observerPath = "#";
}
// 在数组的原型方法上注入钩子
$observer.mock(target);
}
// 遍历 target 的属性,可以把数组元素作为属性对待
for (var prop in target) {
if (target.hasOwnProperty(prop)) {
if (callback) {
// arr 为数组
if (observe.isArray(arr) && observe.isInArray(arr, prop)) {
eventPropArr.push(prop);
$observer.watch(target, prop);
} else if (observe.isString(arr) && prop == arr) {
// arr 为字符串
eventPropArr.push(prop);
$observer.watch(target, prop);
}
} else {
// 缺少 callback 参数(实际为 arr)
eventPropArr.push(prop);
$observer.watch(target, prop);
}
}
}
$observer.target = target;
// 保存 target 变化时触发的处理函数的堆栈
if (!$observer.propertyChangedHandler) {
$observer.propertyChangedHandler = [];
}
// 参数修正
var propChanged = callback ? callback : arr;
// 把当前回调函数和观察的属性压入 target 的回调队列
$observer.propertyChangedHandler.push({
all: !callback,
propChanged: propChanged,
eventPropArr: eventPropArr
});
}
_observe.prototype = {
/**
* target 变化时触发,通过在数组原型方法注入钩子和 definePorprty 实现
*/
"onPropertyChanged": function(prop, value, oldValue, target, path) {
if (value !== oldValue && this.propertyChangedHandler) {
var rootName = observe._getRootName(prop, path); // 得到根路径名,允许在回调函数中提供属性名(path)参数
for (var i = 0, len = this.propertyChangedHandler.length; i < len; i++) {
var handler = this.propertyChangedHandler[i];
if (handler.all || observe.isInArray(handler.eventPropArr, rootName) || rootName.indexOf("Array-") === 0) {
// 这里触发回调函数
handler.propChanged.call(this.target, prop, value, oldValue, path);
}
}
}
// 如果通过数组的原型方法触发,则重新观察数组,这里的 prop 为 "Array-slice" 等
if (prop.indexOf("Array-") !== 0 && typeof value === "object") {
this.watch(target, prop, target.$observeProps.$observerPath);
}
},
/**
* Array 的原型方法注入钩子,在调用时触发 onPropertyChanged
*/
"mock": function(target) {
var self = this;
observe.methods.forEach(function(item) {
target[item] = function() {
var old = Array.prototype.slice.call(this, 0);
var result = Array.prototype[item].apply(this, Array.prototype.slice.call(arguments));
if (new RegExp("\\b" + item + "\\b").test(observe.triggerStr)) {
for (var cprop in this) {
if (this.hasOwnProperty(cprop) && !observe.isFunction(this[cprop])) {
self.watch(this, cprop, this.$observeProps.$observerPath);
}
}
// todo
// 这里作者应该是打算做优化,避免短时间内触发多次回调函数
self.onPropertyChanged("Array-" + item, this, old, this, this.$observeProps.$observerPath);
}
return result;
};
});
},
/**
* 观察对象
*/
"watch": function(target, prop, path) {
if (prop === "$observeProps" || prop === "$observer") return;
if (observe.isFunction(target[prop])) return;
if (!target.$observeProps) target.$observeProps = {};
if (path !== undefined) {
target.$observeProps.$observerPath = path;
} else {
target.$observeProps.$observerPath = "#";
}
var self = this;
var currentValue = target.$observeProps[prop] = target[prop];
// get, set 操作的是 target.$observeProps[prop]
Object.defineProperty(target, prop, {
get: function() {
return this.$observeProps[prop];
},
set: function(value) {
var old = this.$observeProps[prop];
this.$observeProps[prop] = value;
// 值改变时触发回调
self.onPropertyChanged(prop, value, old, this, target.$observeProps.$observerPath);
}
});
if (typeof currentValue == "object") {
if (observe.isArray(currentValue)) {
// 如果值是数组,则注入钩子,处理同构造函数一致
this.mock(currentValue);
if (currentValue.length === 0) {
if (!currentValue.$observeProps) currentValue.$observeProps = {};
if (path !== undefined) {
currentValue.$observeProps.$observerPath = path;
} else {
currentValue.$observeProps.$observerPath = "#";
}
}
}
for (var cprop in currentValue) {
if (currentValue.hasOwnProperty(cprop)) {
this.watch(currentValue, cprop, target.$observeProps.$observerPath + "-" + prop);
}
}
}
}
}
return new _observe(target, arr, callback)
}
// 数组的原型方法
observe.methods = ["concat", "every", "filter", "forEach", "indexOf", "join", "lastIndexOf", "map", "pop", "push", "reduce", "reduceRight", "reverse", "shift", "slice", "some", "sort", "splice", "unshift", "toLocaleString", "toString", "size"]
// 会造成数组改变的原型方法
observe.triggerStr = ["concat", "pop", "push", "reverse", "shift", "sort", "splice", "unshift", "size"].join(",")
observe.isArray = function(obj) {
return Object.prototype.toString.call(obj) === '[object Array]';
}
observe.isString = function(obj) {
return typeof obj === "string";
}
observe.isInArray = function(arr, item) {
for (var i = arr.length; --i > -1;) {
if (item === arr[i]) return true;
}
return false;
}
observe.isFunction = function(obj) {
return Object.prototype.toString.call(obj) == '[object Function]';
}
observe.twoWay = function(objA, aProp, objB, bProp) {
if (typeof objA[aProp] === "object" && typeof objB[bProp] === "object") {
observe(objA, aProp, function(name, value) {
objB[bProp] = this[aProp];
})
observe(objB, bProp, function(name, value) {
objA[aProp] = this[bProp];
})
} else {
observe(objA, aProp, function(name, value) {
objB[bProp] = value;
})
observe(objB, bProp, function(name, value) {
objA[aProp] = value;
})
}
}
observe._getRootName = function(prop, path) {
if (path === "#") {
return prop;
}
return path.split("-")[1];
}
observe.add = function(obj, prop, value) {
obj[prop] = value;
var $observer = obj.$observer;
$observer.watch(obj, prop);
}
Array.prototype.size = function(length) {
this.length = length;
}
if (typeof module != 'undefined' && module.exports && this.module !== module) {
module.exports = observe
} else if (typeof define === 'function' && define.amd) {
define(observe)
} else {
win.observe = observe
};
})(Function('return this')());

读后感

  • getset 方法观察对象,值保存在 .$observer 内避免重复 set
  • hook 所有会改变数组的原型方法,如果重新引用一个数组则需要重新 watch
  • 值改变后会马上触发回调函数,如果同时触发多个则会造成性能问题,可以通过加入队列和触发轮询优化