我的MVVM框架 v3發布!


人們總是愛探求完美的東西,醫學界追求長生不死之葯,煉金術師追求賢者之石,物理學家追求永動機……編程界也有自己的追求,完美架構什么的,什么從MVC,到MVP,到MVVM……當然MVC,MVP,MVVM有他們不同的場景,但MVVM在微軟試水后已被證實為界面開發最好的方案了。於前端而言,一直糾纏於模板與組件的優劣。其實答案大家都知道,web page用模板, web app用組件,問題是如何將它們統合起來。頁面之所以能交互,是因為它存在狀態。因此核心問題是如何將這兩者管轄的狀態統合在一起。狀態的由來有兩個,直接從模型得到,比如后端傳送過來的JSON與XML,或比較悲劇地把PHP序列化的字段還原出來,第二種是在程序中控制流程時不生的中間量。我們可以這些后端數據或中間量整成一個數據源,或作為模板的填充數據,或作為組件的傳參最后成為它的屬性。MVVM的出現就很好解決這問題,數據是數據,最好作為貧血模型而存在,對這些數據的操作以及基於這些操作的操作獨立成另一個東西,ViewModel。View是設計師與頁面重構師的陣地。於是我們能實現並行開發。View與ViewModel的連結是數據綁定,ko稱之為聲明式綁定。綁定可以分解成綁定器與相應的參數,它們調用ViewModel的東西,ViewModel操作Model。綁定器通常是非常薄的一層,很少被人提起,但意義重大。它實現了數據再加工與事件綁定(WPF稱之為命令),從而讓數據可視,操作可用!整個流程是非常清晰的,得到Model,抽象出ViewModel,在View中聲明綁定,就完了。覺得框架提供的功能不足,就添加自定綁定器,在ViewModel添加命令(它可以作為事件回調,或數據的過濾器,驗證函數,格式函數)。格式是固定的,像后端那梓,哪個頁面應該由哪個action負責都有規可循。這正是我們前端夢寐以來的究極解決方案。

但思路是有,實現起來不輕松,因此前端的MVVM框架也林林總總,每個月都冒出一兩個出來。開源的世界類似於免費的世界,很容易引起馬太效應,強者愈強,弱者愈弱,很易產生壟斷。jQuery的一枝獨秀也證明了這一點。MVVM框架的混亂說明還沒有出現足夠稱得上強大的東西。現在比較拉風的是knouckoutjs,emberjs,angularjs,backbone(它也有數據綁定插件讓它改裝成真正的MVVM)。個人是比較看好knouckoutjs,畢竟是由MVVM的發源地微軟的人搞的,是最正統的派系。但它的綁定也一直被人詬病,太復雜難用。后端的WPF由微軟的強大工具撐着,因此人們覺得不怎么。但一旦要你們手寫這些綁定時就慘了,加之前端經過jQuery那極簡主義的DSL式API洗禮后,很多人無法接受這樣復雜的用法。emberjs與angularjs對IE6支持不佳,因此在大陸沒有銷路,加之提供的API太多了,對應的概念也多,學習曲線陡峭。backbone是太笨重,沒有干什么活,卻要寫一大堆代碼,與jQuery反向而行。

我的MVVM框架avalon v3兩個重要借鑒者為knouckoutjs與rivetsjs。從knouckoutjs得到它的雙向依賴鏈的架構,從學習到消化經歷兩個版本,v2的實現完全原創。從rivetsjs得到它的聲明式綁定的API設計,但實現完全是自己的。v3對v2的雙向依賴鏈的架構進行一些改進,只要是重命名,讓這些概念更讓人接受。

avalon v3的雙向綁定鏈架構圖

    // ViewModel              框架              View
    //屬性訪問器  ┓
    //組合訪問器 ┫→→→綁定器 ←←← DOM訪問器 ←←← 數據綁定
    //集合訪問器 ┫
    //命令       ┛

ViewModel是一個由訪問器與命令組成的對象。訪問器即accessor, 取義自ruby的attr_accessor,是attr_writer, attr_reader的結合,用於對某個數據進行讀寫操作。比如Model中有個aaa屬性,ViewModel就會對應生成一個叫aaa的函數,我們可以傳參修改這個aaa的值,也可以從它那時得到aaa的值。之所以這么大周折,是因為IE9才支持用Object.defineProperty描述對象的屬性的訪問機制,它是否可遍歷啊,可配置啊,讀取時應該返回什么,寫入時會進行什么處理。如果aaa屬性與bbb屬性有關聯,我們可以在訪問aaa時修改bbb,直接obj.aaa = "xxx"就行了。但為了兼並IE6,我們唯有obj.aaa("xxx")。emberjs就是基於Object.defineProperty構建它的雙向綁定鏈,因此對IE9-支持不好。


//IE9+ FF4+, safari5+, opera11+, chrome5(IE8只支持DOM)
var obj = {}, aValue = 0;
Object.defineProperty(obj, "aaa", {
    get : function(){
        return aValue;
    },
    set : function(newValue){ 
        obj.bbb += newValue
        aValue = newValue; 
    },
    enumerable : true,
    configurable : true
})
Object.defineProperty(obj, "bbb", {
    value :10,
    writable : true,
    enumerable : true,
    configurable : true
});
                
console.log(obj.aaa)//0
console.log(obj.bbb)//10
obj.aaa = 7
console.log(obj.aaa)//7
console.log(obj.bbb)//17
//IE6+
var aValue = 0, bValue = 10;
var obj = {
    aaa: function(newValue){
        if(arguments.length){
            bValue += newValue
            aValue = newValue; 
            
        }
        console.log("xxxxxxxxx")
        return aValue
    },
    bbb: function(newValue){
        if(arguments.length){
            bValue = newValue; 
        }
          
        return bValue
    }
}
    
console.log(obj.aaa())//0
console.log(obj.bbb())//10
obj.aaa(7)
console.log(obj.aaa())//7
console.log(obj.bbb())//17

訪問器又分四種,存在於ViewModel中的有三種。最簡單的是屬性訪問器,它是對Model中某一個屬性進行操作,相當於ko的監控屬性。如果一個字段由模型中的兩個屬性,或兩個以上,或要對這屬性進行一下加工才產生它的值呢,這就要用到組合訪問器,相當於ko的依賴監控屬性,或emberjs中的computed。像程序中許多表示狀態的中間量都可以抽象成一個組合訪問器。組合訪問器換言之,對已有的東西重新組合而成的屬性的監控函數。集合訪問器,是Model中的數組進行監控,如果它發生排序增刪,它會通知雙向依賴鏈的兩端來刷新自身。集合訪問器是個特殊的數組,它的方法都被重寫了,雖然用法一樣,但調用了它們會同步到對應的節點區域上!

ViewModel中還存在一種叫命令的東西,打個比方,綁定器相當於MVC中的action,命令相當於helpers。它只是一個普通的函數,框架不會再對它加工。框架對命令與訪問器的區分是,訪問器是用$type 與 "$"+(new Date - 0)這兩個屬性。說得可能有點復雜,比如有個對象var model = {aaa:1, bbb:1},然后$.ViewModel( obj )就得到它對應的ViewModel了。

接着我們看綁定部分。要實現事件綁定。knouckoutjs實現如下:

<div>
    <div data-bind="event: { mouseover: enableDetails, mouseout: disableDetails }">
        Mouse over me
    </div>
    <div data-bind="visible: detailsEnabled">
        Details
    </div>
</div>
 
<script type="text/javascript">
    var viewModel = {
        detailsEnabled: ko.observable(false),
        enableDetails: function() {
            this.detailsEnabled(true);
        },
        disableDetails: function() {
            this.detailsEnabled(false);
        }
    };
    ko.applyBindings(viewModel);
</script>

avalon v3參考了rivetsjs的綁定語法,實現如下:

        <div>
            <div data-on-mouseover="enableDetails" data-on-mouseout="disableDetails" >
                Mouse over me
            </div>
            <div data-display="detailsEnabled">
                Details
            </div>
        </div>
        <script type="text/javascript">
            require("avalon,ready", function($) {
                var VM = $.MVVM.convert({
                    detailsEnabled: false,
                    enableDetails: function() {
                        VM.detailsEnabled(true);
                    },
                    disableDetails: function() {
                        VM.detailsEnabled(false);
                    }
                });
                $.MVVM.render(VM)
            })
        </script>

要實現循環綁定,knouckoutjs實現如下

<table>
    <thead>
        <tr><th>First name</th><th>Last name</th></tr>
    </thead>
    <tbody data-bind="foreach: people">
        <tr>
            <td data-bind="text: firstName"></td>
            <td data-bind="text: lastName"></td>
        </tr>
    </tbody>
</table>
 
<script type="text/javascript">
    ko.applyBindings({
        people: [
            { firstName: 'Bert', lastName: 'Bertington' },
            { firstName: 'Charles', lastName: 'Charlesforth' },
            { firstName: 'Denise', lastName: 'Dentiste' }
        ]
    });
</script>

avalon v3實現如下:(data-each-[item]-[index],item, index是可選,名字任取,只要符合變量命名規則就行)

<script type="text/javascript">
    require("avalon,ready", function($) {
        $.MVVM.render({
            people: [
                { firstName: 'Bert', lastName: 'Bertington' },
                { firstName: 'Charles', lastName: 'Charlesforth' },
                { firstName: 'Denise', lastName: 'Dentiste' }
            ]
        });

    })

</script> 
<table>
    <thead>
        <tr><th>First name</th><th>Last name</th></tr>
    </thead>
    <tbody data-each-p="people">
        <tr>
            <td data-text="p.firstName"></td>
            <td data-text="p.lastName"></td>
        </tr>
    </tbody>
</table>

avalon v3的優勢在於,它完全DSL,我們可以通過點號來查找VM中某一個可用的訪問器或命令,作為數據綁定的值。而且實現起來很簡單,不需要像knouckoutjs那樣編寫復雜的JSON編譯器。復雜的東西就難維護,不易升級。有關數據綁定以后我與一系列教程介紹它的。

在數據綁定中,我們借助於一種特殊的屬性來指引MVVM干活,格式為data-binding-[param]-[param]。以“-”斷開,第二個字符串為綁定器的名字,剩余的為它的參數。比如事件綁定,data-on-click。

在avalon v3中,它提供了以下默認綁定器,可以通過$.ViewModel.bindings訪問到。v3彌合了v2的傷口,完美支持事件綁定與事件代理。

  • data-text
  • data-html
  • data-class
  • data-css-[class]
  • data-attr
  • data-value
  • data-display
  • data-on-[event]
  • data-enable
  • data-disable
  • data-options
  • data-each-[item]-[index]
  • data-with-[value]-[key]
  • data-if
  • data-unless

avalon v3會將這個屬性的名字分解成綁定器與其他參數,再將它的值得到VM中對應的訪問器與命令,最后把它們構建成一個叫DOM訪問器的東西,作為雙向綁定鏈的頂層,專門與DOM打交道。

在jQuery時代,ID是我們命中元素最可靠的基點,以此為起點八爪魚般處理周遭的節點。行為層上,我們通過事件綁定,幾乎可以用根據代理一切事件。但jQuery是函數式編程,狀態如果在連續在多個回調中使用時,它就要寫在回調外面。當然我們可以緩存於某個節點上(data),在另一個回調中通過選擇器得到那個節點再重新data出來。但整體上,jQuery代碼都是以事件分割成一段段,中間夾雜着一些中間量與處理函數。它們是否能很好工作完全看編程人員的技術水平了。在MVVM中,數據綁定與元素是一體的,因此絕沒有偏差。處理交互上,事件以命令的新身份登場, 回調被集合管理於VM,狀態也被收籠於VM中,我們不再為如何組織代碼傷腦筋,所有都有章而循,新手接力也易上手。MVVM減少對選擇器的依賴,將數據與操作綁定在堅固的支點上。

鏈接地址

過幾天寫些教程,介紹如何用。完!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM