迷你MVVM框架 avalonjs 實現上的幾個難點

通過兩個星期的性能優化,avalon終於實如今一個頁面綁定達到上萬個的時候不卡頓的目標(angular的限制是2000)。如今稍做休息,總結一下avalon遇到的一些難題。git

首先是如何監控的問題。全部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實現之初就開始動用這東西。github

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),重用全部同名的鍵值對生成的代理對象。app

到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,進行優化。框架

目前批處理涉及到的內部方法與對象
  • createWithProxy 建立循環對象時的代理VM
  • createEachProxy建立循環數組時的代理VM
  • updateWithProxy 更新某一鍵值對的代理VM
  • withProxyPool createWithProxy生成的對象統一放在這裏管理,防止重複生成
  • removeView 批量移除一堆節點
  • getLocatedNode 定位要插入的位置
  • shimController 爲ms-each, ms-with, ms-repeat要循環的元素外包一個msloop臨時節點,ms-controller的值爲代理VM的$id,同時是實現批量插入插量移除臨時節點的關鍵
  • removeFromSanctuary 將經過ms-if移出DOM樹放進ifSanctuary的元素節點移出來,以便垃圾回收
  • queryComments 獲得某一元素節點或文檔碎片對象下的全部註釋節點
  • iteratorCallback 經過它執行data-*-rendered回調

最後一個也是最難一個至少也沒有搞定,只在不斷改良中,這就是UI綁定的設計。以前有一個綁定叫ms-ui,已經夭折。如今的ms-widget仍是不夠好。有時我想參考angular的那種方式,但又嫌它添加了太多莫名其妙的符號。但主要是由於個人框架對用戶是很是放縱,不喜歡那種改配置的設計。不過就是放着如今的無論,它還有一個重大的缺陷,沒有生命週期管理。這個在項目中已經暴露出來,須要用戶本身定義一個destroy方法,手動銷燬。我認爲這是框架的分內事。接下來幾星期,我就着手這方面的改進,但願能把這痛點解決掉。 函數

最後總結一下:oop

  1. VM實現,如何內置與V的同步機制。
  2. ms-if,ng-if, data-bind="if:xxx"這樣插入移除的綁定,會中斷以前無懸念一直掃到底的思路,以前想好的生命週期管理也要出岔子了!
  3. ms-each, ms-repeat, ng-repeat,data-bind="foreach:xxx"這樣的批量生成的綁定,這種綁定最容易引發性能問題,而且須要界定其做用範圍
  4. ms-widget, <widget></widget>這樣轉換元素爲一個控件的綁定,這也最複雜最麻煩的綁定,須要有通盤的設計觀。

固然對於剛接觸這領域的人能夠還有許多麻煩事,如不使用jQuery的狀況如何擺平那一大堆兼容問題,如何寫一個parser解析綁定屬性的值,加載器,路由器,動畫引擎等一大堆配套設施……這要你自求多福,好之爲之了,但跨過這道坎,你就是另外一個級別的人物了!性能

相關文章
相關標籤/搜索