大凡复杂的框架都通过观察者模式来解耦它里面各个对象的联动关系,avalon也不例外。并且观察者模式也是其双向绑定链的实现关键。
我们回顾一下其双向绑定链。VM中的属性变动会通知视图进行局部刷新并会触发一些回调。在avalon中,视图刷新的实现是用绑定实现,以angular时麾的名词唤做directive;另外的一些回调,在avalon还是angular 都统称为$watch回调,因此都是在VM上绑定$watch方法进行添加的。
视图的刷新终归交给函数来完成,而不是视图中的某个奇怪的属性或符号,这个函数就叫做视图刷新函数,这些函数的主体基本在框架内已经定义好,就需要用户给它传进要处理的元素节点,属性名,属性值,涉及的VM等等东西……
$watch回调则是做数据验证,加工,对视图的再微调等工作,虽然是配角,但绝非可有可无。
这两个结合起来,才是完成我们对视图的所有操作。
V的变动也会引起VM的变动。这个在avalon是ms-duplex, angular是ng-model,唯一的服务对象是表单元素,终端用户也只有这个途径修改数据了。
现在回过头来,看我们的观察者模式。观察者模式最经典的结构是,用一个个数组或列队放置要处理的实体或函数,在不远的未来以某个方式遍历它们,逐个调用。现在我们要操作的东西也很明确是视图刷新函数与$watch回调。
avalon自2013年5月1日发布的0.5版,经过了两次演变。最初的0.5版,VM中的每个需要放到视图中的属性,都要用Object.defineProperty或VBS转换为访问器属性,在avalon中,这些属性叫做监控属性,JS术语则称之为访问器属性。访问器属性其实是setter、getter函数的统一体。在avalon里面,每个监控属性的setter、getter是同一个函数,不是为了别的,只求简单。这函数上面都有一个叫subscribers数组,里面是一个个对象,对象里有视图刷新函数及其传参及其元素节点及其他乱七儿糟的东西。此外,VM上有一个$events对象,每个监控属性都在这里对象一个个数组,里面装着的是$watch回调。
接下来就是经典的观察者模式的做法了。VM中的一个a属性,然后视图中有两上P元素都要显示a的值。
<p>{{a}}<p>
<p>{{a}}<p>
换言之,这两个P元素都依赖于a属性,而a属性又是一个监控属性, vm.a = 4时,就进入其getter,首先它与之前的值进行比较,发现不一样,就先触发视图刷新。在avalon里面,这个行为都是由一个叫notifySubscribers的方法实现, 它相当于上面类图的Update方法。 接着触发所有$watch回调,在avalon里面,也统一交由$fire方法进行处理,也相当于上面类图的Update方法。为什么要两个Update方法呢,因此视图的操作实体是一个个元素节点,元素节点会有被移除出DOM的可能性,说明其已经没用了,就应该被Detach掉,实际情形可能更复杂,这里就不展开了,有心人可以直接看源码。相对而言,$fire方法则简单些。从性能角度来考量,$watch回调就没有必要经过是否在DOM树这些检测了。
但这还没有完,如果VM的某个属性是一个对象,它也会转换为一个VM,里面也存在对应的$events对象,及一大堆监控属性及监控属性上面的数组。就是这样一层层递归下去,构成0.5以后的第一代观察者模式。
之后过了N久N久,这架构一直没变,毕竟这是历经考虑的设计模式之一。把两种不同的数据分别放在不同的数组内也是非常合理,各自fire各自的。到了1.35时,开了一个新分支,叫做avalon.$events,主要目的是实现静态收集依赖功能与改善CG回收机制。其中CG回收,就是将无用的数据从数组中移除,为了减少要处理的数组,才决定把视图刷新函数所在的数组与$watch回调的数组合而为一。然后在notifySubscribers与$fire方法里面各加了一点判定逻辑。这就是第二代。
第二代,就是在Detach上大做文章,每个监控属性对应的小数组,会汇总到一个叫$$subscribers的大数组中。
function collectSubscribers(list) { //收集依赖于这个访问器的订阅者
var data = Registry[expose]
if (list && data && avalon.Array.ensure(list, data) && data.element) { //只有数组不存在此元素才push进去
addSubscribers(data, list)
}
}
function addSubscribers(data, list) {
data.$uuid = data.$uuid || generateID()
list.$uuid = list.$uuid || generateID()
var obj = {
data: data,
list: list,
toString: function() {
return data.$uuid + " " + list.$uuid
}
}
if (!$$subscribers[obj]) {
$$subscribers[obj] = 1
$$subscribers.push(obj)
}
}
var $$subscribers = [],
$startIndex = 0,
$maxIndex = 200,
beginTime = new Date(),
removeID
function removeSubscribers() {
for (var i = $startIndex, n = $startIndex + $maxIndex; i < n; i++) {
var obj = $$subscribers[i]
if (!obj) {
break
}
var data = obj.data
var el = data.element
var remove = el === null ? 1 : (el.nodeType === 1 ? typeof el.sourceIndex === "number" ?
el.sourceIndex === 0 : !root.contains(el) : !avalon.contains(root, el))
if (remove) { //如果它没有在DOM树
$$subscribers.splice(i, 1)
delete $$subscribers[obj]
avalon.Array.remove(obj.list, data)
//log("debug: remove " + data.type)
if (data.type === "if" && data.template && data.template.parentNode === ifGroup) {
ifGroup.removeChild(data.template)
}
for (var key in data) {
data[key] = null
}
obj.data = obj.list = null
i--
n--
}
}
obj = $$subscribers[i]
if (obj) {
$startIndex = n
} else {
$startIndex = 0
}
beginTime = new Date()
}
function notifySubscribers(list) { //通知依赖于这个访问器的订阅者更新自身
clearTimeout(removeID)
if (new Date() - beginTime > 444) {
removeSubscribers()
} else {
removeID = setTimeout(removeSubscribers, 444)
}
if (list && list.length) {
var args = aslice.call(arguments, 1)
for (var i = list.length, fn; fn = list[--i]; ) {
var el = fn.element
if (el && el.parentNode) {
if (fn.$repeat) {
fn.handler.apply(fn, args) //处理监控数组的方法
} else if (fn.type !== "on") { //事件绑定只能由用户触发,不能由程序触发
var fun = fn.evaluator || noop
fn.handler(fun.apply(0, fn.args || []), el, fn)
}
}
}
}
}
第三代观察者模式,现在放在一个avalon.repeat的分支上,目的是改善ms-repeat的性能。ms-repeat 对应的一个数组,数组越大,那么框架内部生成的订阅者数组就越多,并且它们是分散在N多个$events对象上,维护成本非常高。因此第三代做了重大改变!
下面是第二代的订阅函数储放图:
var obj = {
$events: {
a: [fn, fn, fn, fn, fn],
b: [fn, fn, fn, fn, fn],
c: [fn]
},
a: prop,
b: prop,
c: {
$events: {
d: [fn, fn, fn, fn],
e: [fn]
},
d: prop,
e: {
$events:{
f: [fn, fn, fn, fn]
},
f: prop
}
}
}
到了第三代,分散到各子孙VM的“权力”全部回收上来了。只有顶层VM才能储放视图刷新函数,各级子VM只能放$watch回调。
var obj = {
$events: {
a: [fn, fn, fn, fn, fn],
b: [fn, fn, fn, fn, fn],
c: [fn, fn, fn, fn, fn],
"c.d": [fn, fn, fn, fn],
"c.e": [fn, fn, fn, fn],
"c.e.f": [fn, fn, fn, fn]
},
a: prop,
b: prop,
c: {
$events: {
d: [fn],//只剩下$watch回调
e: [fn] //只剩下$watch回调
},
d: prop,
e: {
$events:{
f: [fn]//只剩下$watch回调
},
f: prop
}
}
}
同时,为了让子VM的属性变动能迅速定位到顶层VM对应的数组,内部通过一个通用getPath进行定位,而不是一级级地往上fire!
经过这次改进后,子VM上的订阅者数组就会大量减少,到时性能也会大大提升。
不过话说回来,avalon的每次升级,在内部大改特改,没有影响到用户使用,也全是因为avalon没有把这些内部对象,方法暴露出去。这也是avalon比angular高明之处,升级不能给用户带来麻烦。暗地里持续改进,很快avalon将迎来1.3.8!
嘿嘿嘿
兄弟间VM如何传递数据
目前1.3.7.2还不支持第三代的模式吧……