簡介
avalon是國內 司徒正美 寫的MVVM框架,相比同類框架它的特點是:
- 使用 observe 模式,性能高。
- 將原始對象用object.defineProperty重寫,不需要用戶像用knockout時那樣顯示定義各種屬性。
- 對低版本的IE使用了VBScript來兼容,一直兼容到IE6。
需要看基礎介紹的話建議直接看司徒的博客。在網上搜了一圈,發現已經有了avalon很好的源碼分析,這里也推薦一下:地址。 avalon在圈子里一直被詬病不夠規范的問題,請各位不必再留言在我這里,看源碼無非是取其精華去其糟粕。可以點評,但總是討論用哪個框架好哪個不好就沒什么意義了,若是自己把握不住,用什么都不好。
今天的分析以 avalon.mobile 1.2.5 為准,avalon.mobile 是專門為高級瀏覽器准備的,不兼容IE8以下。
入口
還是先看啟動代碼
avalon.ready(function() {
avalon.define("box", function(vm) {
vm.w = 100;
vm.h = 100;
vm.area = function(){
get : function(){ return this.w * this.h }
}
vm.logW =function(){ console.log(vm.w)}
})
avalon.scan()
})
還是兩件事:定義viewModel 和 執行掃描。 翻到define 定義:
avalon.define = function(id, factory) {
if (VMODELS[id]) {
log("warning: " + id + " 已經存在於avalon.vmodels中")
}
var scope = {
$watch: noop
}
factory(scope) //得到所有定義
var model = modelFactory(scope) //偷天換日,將scope換為model
stopRepeatAssign = true
factory(model)
stopRepeatAssign = false
model.$id = id
return VMODELS[id] = model
}
其實已經可以一眼看明白了。這里只提一點,為什么要執行兩次factory?建議讀者先自己想一下。我這里直接說出來了: 因為modelFactory中,如果屬性是函數,就會被直接復制到新的model上,但函數內的vm卻仍然指向的原來的定義函數的中的vm,因此發生錯誤。所以通過二次執行factory來修正引用錯誤。
那為什么不在modelFactory中直接就把通過Function.bind或其他方法來把引用給指定好呢?而且可以在通過scope獲得定以后就直接把 scope 對象修改成viewModel就好了啊?
這里的代碼寫法其實是直接從avalon兼容IE的完整版中搬出來的,因為對老瀏覽器要創造VBScript對象,所以只能先傳個scope進去獲取定義,在根據定義去創造。並且老的瀏覽器也不支持bind等方法。 還是老規矩,我們先看看整體機制圖:

雙工引擎
接下來就是直接一探 modelFactory 內部了。翻到代碼 324 行。
function modelFactory(scope, model) {
if (Array.isArray(scope)) {
var arr = scope.concat()//原數組的作為新生成的監控數組的$model而存在
scope.length = 0
var collection = Collection(scope)
collection.push.apply(collection, arr)
return collection
}
if (typeof scope.nodeType === "number") {
return scope
}
var vmodel = {} //要返回的對象
model = model || {} //放置$model上的屬性
var accessingProperties = {} //監控屬性
var normalProperties = {} //普通屬性
var computedProperties = [] //計算屬性
var watchProperties = arguments[2] || {} //強制要監聽的屬性
var skipArray = scope.$skipArray //要忽略監控的屬性
for (var i = 0, name; name = skipProperties[i++]; ) {
delete scope[name]
normalProperties[name] = true
}
if (Array.isArray(skipArray)) {
for (var i = 0, name; name = skipArray[i++]; ) {
normalProperties[name] = true
}
}
for (var i in scope) {
loopModel(i, scope[i], model, normalProperties, accessingProperties, computedProperties, watchProperties)
}
vmodel = Object.defineProperties(vmodel, descriptorFactory(accessingProperties)) //生成一個空的ViewModel
for (var name in normalProperties) {
vmodel[name] = normalProperties[name]
}
watchProperties.vmodel = vmodel
vmodel.$model = model
vmodel.$events = {}
vmodel.$id = generateID()
vmodel.$accessors = accessingProperties
vmodel[subscribers] = []
for (var i in Observable) {
vmodel[i] = Observable[i]
}
Object.defineProperty(vmodel, "hasOwnProperty", {
value: function(name) {
return name in vmodel.$model
},
writable: false,
enumerable: false,
configurable: true
})
for (var i = 0, fn; fn = computedProperties[i++]; ) { //最后強逼計算屬性 計算自己的值
Registry[expose] = fn
fn()
collectSubscribers(fn)
delete Registry[expose]
}
return vmodel
}
前面聲明了一對變量作為容器,用來保存轉換過的 控制屬性(相當於ko中的observable) 和 計算屬性(相當於ko中的computed) 等等。往下翻到最關鍵的352行,這個 loopModel 函數就是用來生成好各個屬性的入口了。繼續深入:
function loopModel(name, val, model, normalProperties, accessingProperties, computedProperties, watchProperties) {
model[name] = val
if (normalProperties[name] || (val && val.nodeType)) { //如果是元素節點或在全局的skipProperties里或在當前的$skipArray里
return normalProperties[name] = val
}
if (name[0] === "$" && !watchProperties[name]) { //如果是$開頭,並且不在watchProperties里
return normalProperties[name] = val
}
var valueType = getType(val)
if (valueType === "function") { //如果是函數,也不用監控
return normalProperties[name] = val
}
var accessor, oldArgs
if (valueType === "object" && typeof val.get === "function" && Object.keys(val).length <= 2) {
var setter = val.set,
getter = val.get
accessor = function(newValue) { //創建計算屬性,因變量,基本上由其他監控屬性觸發其改變
var vmodel = watchProperties.vmodel
var value = model[name],
preValue = value
if (arguments.length) {
if (stopRepeatAssign) {
return
}
if (typeof setter === "function") {
var backup = vmodel.$events[name]
vmodel.$events[name] = [] //清空回調,防止內部冒泡而觸發多次$fire
setter.call(vmodel, newValue)
vmodel.$events[name] = backup
}
if (!isEqual(oldArgs, newValue)) {
oldArgs = newValue
newValue = model[name] = getter.call(vmodel)//同步$model
withProxyCount && updateWithProxy(vmodel.$id, name, newValue)//同步循環綁定中的代理VM
notifySubscribers(accessor) //通知頂層改變
safeFire(vmodel, name, newValue, preValue)//觸發$watch回調
}
} else {
if (avalon.openComputedCollect) { // 收集視圖刷新函數
collectSubscribers(accessor)
}
newValue = model[name] = getter.call(vmodel)
if (!isEqual(value, newValue)) {
oldArgs = void 0
safeFire(vmodel, name, newValue, preValue)
}
return newValue
}
}
computedProperties.push(accessor)
} else if (rchecktype.test(valueType)) {
accessor = function(newValue) { //子ViewModel或監控數組
var realAccessor = accessor.$vmodel, preValue = realAccessor.$model
if (arguments.length) {
if (stopRepeatAssign) {
return
}
if (!isEqual(preValue, newValue)) {
newValue = accessor.$vmodel = updateVModel(realAccessor, newValue, valueType)
var fn = rebindings[newValue.$id]
fn && fn()//更新視圖
var parent = watchProperties.vmodel
withProxyCount && updateWithProxy(parent.$id, name, newValue)//同步循環綁定中的代理VM
model[name] = newValue.$model//同步$model
notifySubscribers(realAccessor) //通知頂層改變
safeFire(parent, name, model[name], preValue) //觸發$watch回調
}
} else {
collectSubscribers(realAccessor) //收集視圖函數
return realAccessor
}
}
accessor.$vmodel = val.$model ? val : modelFactory(val, val)
model[name] = accessor.$vmodel.$model
} else {
accessor = function(newValue) { //簡單的數據類型
var preValue = model[name]
if (arguments.length) {
if (!isEqual(preValue, newValue)) {
model[name] = newValue //同步$model
var vmodel = watchProperties.vmodel
withProxyCount && updateWithProxy(vmodel.$id, name, newValue)//同步循環綁定中的代理VM
notifySubscribers(accessor) //通知頂層改變
safeFire(vmodel, name, newValue, preValue)//觸發$watch回調
}
} else {
collectSubscribers(accessor) //收集視圖函數
return preValue
}
}
model[name] = val
}
accessor[subscribers] = [] //訂閱者數組
accessingProperties[name] = accessor
}
源碼的注釋其實已經寫得非常清楚了,如果你看過我上一篇對knockout源碼的解讀,你會發現avalon這里面的機制和knockout幾乎是一樣的。函數無非就是根據定義函數中各個屬性的類型來生成讀寫器(accessor),這個讀寫器會用在后面的 defineProperty 中。這里唯一值得提一下的就是那個 updateWithProxy 函數。只有一種情況需要用到它,就是當頁面上使用了 ms-repeat 或者其他循環綁定來處理 數組或對象 時,會生為循環中的對象生成一個代理對象,這個代理對象記錄除數據本身外和作用於相關的一些變量,和knockout的bindingContext有些像。 好了,到這里源碼基本上沒什么難度,我們來做一點有意思的事情。還記得之前我們提出的關於 執行兩次 factory的 疑問嗎?第二次執行主要是為了修正函數屬性中的引用,我們看上面這代碼中,但屬性的類型是function時,就直接復制,如果我們對這個函數執行一下bind的方法呢,是不是就不用使用factory修正引用了?來試一下,先將 318 行的二次執行factory注釋掉。再loopModel函數中 424 行改成
return normalProperties[name] = val.bind(model)
我們寫個頁面載入改過的avalon,然后跑一下這段測試:
var vma = avalon.define('a',function(vm){
vm.a = "a"
vm.b = "b"
vm.c = {
get : function(){return this.a+this.b}
}
vm.c2 = {
get : function(){return vm.a+vm.b}
}
vm.d = function(){
return this.a+this.b //注意這里用的是 this
}
})
vma.a = "c"
console.log(vma.c == vma.a+vma.b)
console.log(vma.d() == vma.a+vma.b)
有沒有驗證,結果大家最好自己試驗一下。 這里可以看到,如果只是針對現代瀏覽器,avalon的內核還是有很多可以重構的地方的。
viewModel的內部實現已經搞清,接下來就只剩看看如何處理和頁面元素的綁定了。翻到 1214 行scan函數的定義,主要是執行了 scanTag 。再看,主要是執行了 scanAttr。再看,終於找到了和 knockout 看起來一樣的 bindingHandlers 了,再往下翻翻就會發現和 knockout 是一樣的綁定機制了。讀者可以自己看,看不懂的地方翻翻我上一篇中ko的同樣部分看看就知道了。
其他
最后還是講講對數組的處理。之前在ko中我們看到ko為對象專門准備了一個observableArray,里面重寫pop等方法,以保證在處理函數時能只通知改動元素相關的綁定,而不用修改整個數組綁定的視圖。在avalon中,我們看到在 loopModel 467行的 rchecktype.test(valueType) 這個語句。rchecktype 是個正則 /^(?:object|array)$/ ,也就是判斷該屬性是不是對象或數組。如果是,在 491 行 的
accessor.$vmodel = val.$model ? val : modelFactory(val, val)
又生成一個modelFactory,這時傳入modelFactory的第一個參數就可能是數組了,再看modelFacotry 定義,當第一個函數為數組時,將其變成了一個Collection對象,而Collection也是重寫了各種數組方法。果然,機制大家都差不多。不過司徒在博客中強調了它的數組處理效率更高,大家可以自己看看。
最后推薦兩篇作者的博客文章,看看他在寫MVVM中更多技術細節
迷你MVVM框架 avalonjs 實現上的幾個難點
迷你MVVM框架avalon在兼容舊式IE做的努力
還是那句話,取其精華。明天將帶來MVVM新貴 vue.js 源碼分析,敬請期待。
