經過兩個星期的性能優化,avalon終於實現在一個頁面綁定達到上萬個的時候不卡頓的目標(angular的限制是2000)。現在稍作休息,總結一下avalon遇到的一些難題。
首先是如何監控的問題。所有MVVM要將VM中的屬性與視圖中的綁定屬性關聯起來大抵有如下三種方式:angular是對函數體取toString進行預編譯,將里面的賦值語句,取值語句替換為set,get方法,然后通過特定方法進行臟檢測觸發,或手動觸發;ko是對VM的屬性用監控函數外包一層,全事件驅動觸發;avalon是通過Object.defineProperties重寫內部set,get函數,全事件驅動觸發。此外還有emberjs,它是統一使用上帝set,get方法接觸所有取值賦值的入口,全事件驅動觸發,算是angular的改良。從用戶體驗來說,avalon的實現是最好的,因為它是不用改變用戶習慣,emberjs次之,強制使用set,get方法,ko讓函數、數組變成了函數,讓用戶感到非常違和,angular最差,一大堆惡心的限制,無法直接操作VM。出現這局面是因為Object.defineProperty不好兼容,它雖然是IE8支持,但那只在元素節點上存在。除非你摒棄IE8了。直接我找到VBScript,這問題才不算問題。我的優勢是,我至一開始就知道有VBS這東西存在,在avalon實現之初就開始動用這東西。
ms-if的實現,說到底是生命周期的設計問題,如何銷毀一個綁定及在特殊情況還讓它繼續存活。VM與視圖的關聯點在於綁定屬性,綁定屬性會轉換為求值函數,求值函數將與它的上下文環境(比如它所在的元素節點,它原來的綁定屬性的名字,值,類型,過濾器定義情況等等)組成一個對象,放到一個數組中。這就是訂閱者數組。數VM中的屬性發生變化時(通過內部set方法被調動時得知),就會執行這個求值函數將其他東西一起執行,從而實現視圖的最小化局部刷新。問題是,我們的頁面有時很大,上面擁有許多綁定屬性,這意味着這些中間生成的求值函數與對象將一直放在各個訂閱者數組中,占用着大量內存。如果再出現像瀑布流或或定時刷新的情況,這內存占用將越來越大,讓頁面運行緩慢。因此就必需考慮回收內存的情況了。avalon給出的方案時,當某一個節點將出DOM樹,它自身或底下的節點的原來所有綁定屬性所生成的求值函數將從訂閱者數組中移除。
function notifySubscribers(accessor) { //通知依賴於這個訪問器的訂閱者更新自身
var list = accessor[subscribers]
if (list && list.length) {
var args = aslice.call(arguments, 1)
for (var i = list.length, fn; fn = list[--i]; ) {
var el = fn.element,
remove
if (el && !avalon.contains(ifSanctuary, el)) {
if (typeof el.sourceIndex == "number") { //IE6-IE11
remove = el.sourceIndex === 0
} else {
remove = !avalon.contains(root, el)
}
if (remove) { //如果它沒有在DOM樹
list.splice(i, 1)
log("Debug: remove " + fn.name)
}
}
if (typeof fn === "function") {
fn.apply(0, args) //強制重新計算自身
} else if (fn.getter) {
fn.handler.apply(fn, args) //處理監控數組的方法
} else {
fn.handler(fn.evaluator.apply(0, fn.args || []), el, fn)
}
}
}
}
上面就是這一操作的實現,VM要對視圖進行同步都必須經過此方法notifySubscribers。每次執行時,它都會取得求值函數上的元素節點,然后判定它是否在DOM樹上。在IE6-11中,我們可以判定sourceIndex 屬性是否為零得知,標准瀏覽器可以通過根節點是否包含當前元素得知(這個contains方法內部需要做一番兼容)。
但ms-if的出現打破這和諧局面,要考慮此綁定主動移出DOM樹的情況,還要判定考慮此元素什么時候插回DOM樹。在循環綁定中,元素節點在一開始是在文檔碎片中動態生成的,這個更麻煩。從0.6-1.1,我一直陷入這噩夢中。之前我有的方案是采用定時器,不斷輪詢此節點是否插入DOM,插入了才開始對它掃描。后來又發掘出DOMNodeInserted這個事件,對一些高級一點的瀏覽器做一些優化。到了0.982,干脆就直接假設它們一開始都沒加入DOM,添加一個類名,防止移入移出時顫動,再把當前的VM列表綁定在元素上,然后判定元素是否在DOM樹(又是輪詢操作)。再后來是改寫循環綁定部分,在ms-each, ms-repeat等執行后,再執行一個回調,掃描當前部分,這樣就可以消去輪詢操作了。再再后來的改進是,確保循環生成時,元素都集中一個文檔碎片中,然后整體插入DOM時,這時才進行掃描。換言之,第一次它總是在DOM樹里。於是就能消去contains判定,ms-if的代碼大大減少。現在的ms-if今非昔比,還加入了按需加載功能。它的子元素掃描被它的綁定屬性所控制,對大頁面的性能優化非常有用。
批量生成與監控數組的實現,這倆是相輔相成的。早期的監控是直接在原數組中改,因此原工廠函數非常龐大。后來直接把這些要覆蓋的函數放到一個對象上,然后工廠方法里直接mix一下就行了。還簡接讓所有監控數組共享了這些方法,節省內存。在綁定的實現,之前是有許多分支,什么push, unshift, pop, shift, set, reroder, splice, clear一大堆,那個視圖刷新函數太苦逼了。后來對數組的操作進行深入分析,發現所有操作無疑是做以下幾種操作,添加元素,刪除元素,改寫元素對應的索引值,移動元素到某一位置,直接替換元素。於是改寫監控數組的方法,根據add. del, index, move這四種操作進行組合(0.9.0),后來還加了clear,因為批量處理一個數組或一個子對象都用到此操作。這些操作里面都會通過notifySubscribers方法,將操作名與相應參數傳到視圖刷新函數,從而分配到不同分支上做DOM處理。這算是成功了一大步。內部其實還涉及到代理VM的生成算是處理,於是有了createItemModel的內部函數,然后出現了ms-with,於是它們改名了createWithModel, createEachModel。這兩個方法的實現也不斷改進,后來更名為createEachProxy, creatWithProxy,在ms-with里還使用了對象池技術(withMapper ,0.96),重用所有同名的鍵值對生成的代理對象。
到0.9.8,偷偷引入一個ms-repeat綁定。avalon早期的參考對象是knockout,它實現循環綁定時需要用到兩個元素,一個父元素作容器,它下面的所有節點作模板,或者用一個虛擬節點(真實名字是兩個一前一后的注釋節點)圈定作用范圍,里面的那些子點作模板。由於注釋節點在IE6-8的UL,OL元素上會發生錯亂,需要手動處理,avalon就沒有更進。但在許多場合,總要外套一個父節點是非常難辦,或做不到,於是移目於angular上。angular的ng-repeat只循環元素自身是一個非常好的方案,加之它又帶來了$first, $last, $remove等好東西,於是avalon開始模仿。但這工程量與難度非常大,一直跌跌撞撞,在1.2時才基本算完工。其間要處理的問題是,如何讓ms-repeat如何同時遍歷數組與對象,對象的鍵值對的輸出順序(data-with-sorted回調的引進),批處理后的回調(data-*-rendered回調),回滾機制(rollback函數),如何判定子元素已經被渲染(需要在元素上添加一個標記,放便在scanAttr時執行一個回調)。回調是同事在做私自人項目提出的,最初沒參數,現在能明確是add, del, index等操作了。生成代理VM與綁定標記后來抽象成一個shimController,實現批量插入與批量處理。對象池(更名為withProxyPool)也大大優化,它在一開始時就生成所有鍵值對代理VM,不再在求值函數里判定了。並且VM加了一個withProxyCount,進行優化。
最后一個也是最難一個至少也沒有搞定,只在不斷改良中,這就是UI綁定的設計。之前有一個綁定叫ms-ui,已經夭折。現在的ms-widget還是不夠好。有時我想參考angular的那種方式,但又嫌它添加了太多莫名其妙的符號。但主要是因為我的框架對用戶是非常放縱,不喜歡那種改配置的設計。不過就是放着現在的不管,它還有一個重大的缺陷,沒有生命周期管理。這個在項目中已經暴露出來,需要用戶自己定義一個destroy方法,手動銷毀。我認為這是框架的份內事。接下來幾星期,我就着手這方面的改進,希望能把這痛點解決掉。
最后總結一下:
- VM實現,如何內置與V的同步機制。
- ms-if,ng-if, data-bind="if:xxx"這樣插入移除的綁定,會中斷之前無懸念一直掃到底的思路,之前想好的生命周期管理也要出岔子了!
- ms-each, ms-repeat, ng-repeat,data-bind="foreach:xxx"這樣的批量生成的綁定,這種綁定最容易引起性能問題,並且需要界定其作用范圍
- ms-widget, <widget></widget>這樣轉換元素為一個控件的綁定,這也最復雜最麻煩的綁定,需要有通盤的設計觀。
當然對於剛接觸這領域的人可以還有許多麻煩事,如不使用jQuery的情況如何擺平那一大堆兼容問題,如何寫一個parser解析綁定屬性的值,加載器,路由器,動畫引擎等一大堆配套設施……這要你自求多福,好之為之了,但跨過這道坎,你就是另一個級別的人物了!
