avalon是一個簡單易用迷你的MVVM框架,它最先發佈於2012.09.15,爲解決同一業務邏輯存在各類視圖呈現而開發出來的。 事實上,這問題其實也能夠簡單地利用通常的前端模板加jQuery 事件委託 搞定,但隨着業務的膨脹, 代碼就充滿了各類選擇器與事件回調,難以維護。所以完全的將業務與邏輯分離,就只能求助於架構。 最初想到的是MVC,嘗試過backbone,但代碼不降反升,很偶爾的機會,碰上微軟的WPF, 優雅的MVVM架構當即吸引住我,我以爲這就是我一直追求的解決之道。javascript
MVVM將全部前端代碼完全分紅兩部分,視圖的處理經過綁定實現(angular有個更炫酷的名詞叫指令), 業務邏輯則集中在一個個叫VM的對象中處理。咱們只要操做VM的數據,它就天然而然地神奇地同步到視圖。 顯然全部神祕都有其內幕,C#是經過一種叫訪問器屬性的語句實現,那麼JS也有沒有對應的東西。 感謝上帝,IE8最先引入這東西(Object.defineProperty),惋惜有BUG,但帶動了其餘瀏覽器實現它,IE9+便能安全使用它。 對於老式IE,我找了很久,實在沒有辦法,使用VBScript實現了。css
Object.defineProperty或VBS的做用是將對象的某一個屬性,轉換一個setter與getter, 咱們只要劫持這兩個方法,經過Pub/Sub模式就能偷偷操做視圖。爲了記念WPF的指引,我將此項目以WPF最初的開發代號avalon來命名。 它真的能讓前端人員脫離DOM的苦海,來到數據的樂園中!html
絕對的優點就是下降了耦合, 讓開發者從複雜的各類事件中掙脫出來。 舉一個簡單地例子, 同一個狀態可能跟若干個事件的發生順序與發生時的附加參數都有關係, 不用 MVC (包括 MVVM) 的狀況下, 邏輯可能很是複雜並且脆弱。 而且一般須要在不一樣的地方維護相關度很是高的一些邏輯, 稍有疏忽就會釀成 bug 不能自拔。使用這類框架能從根本上下降應用開發的邏輯難度, 而且讓應用更穩健。前端
除此以外, 也免去了一些重複的體力勞動, 一個 {value} 就代替了一行 $(selector).text(value)。 一些個經常使用的 directive 也能快速實現一些本來可能須要較多代碼才能實現的功能。java
咱們從一個完整的例子開始認識 avalon :git
<!DOCTYPE html> <html> <head> <title></title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <script src="avalon.js"></script> </head> <body> <div ms-controller="box"> <div style=" background: #a9ea00;" ms-css-width="w" ms-css-height="h" ms-click="click"></div> <p>{{ w }} x {{ h }}</p> <p>W: <input type="text" ms-duplex="w" data-duplex-event="change"/></p> <p>H: <input type="text" ms-duplex="h" /></p> </div> <script> var vm = avalon.define({ $id: "box", w: 100, h: 100, click: function() { vm.w = parseFloat(vm.w) + 10; vm.h = parseFloat(vm.h) + 10; } }) </script> </body> </html>
<input data-duplex-event="change" ms-duplex="h"> 該代碼的做用是當input控件失去焦點時,更新vm對象的h值。輸入修改。。
上面的代碼中,咱們能夠看到在JS中,沒有任何一行操做DOM的代碼,也沒有選擇器,很是乾淨。在HTML中, 咱們發現就是多了一些以ms-開始的屬性與{{}}標記,有的是用於渲染樣式, 有的是用於綁定事件。這些屬性或標記,實質就是avalon的綁定系統的一部分。綁定(有的框架也將之稱爲指令), 負責幫咱們完成視圖的各類操做,至關於一個隱形的jQuery。正由於有了綁定,咱們就能夠在JS代碼專一業務邏輯自己, 寫得更易維護的代碼!github
不過上面的代碼並不完整,它能工做,是由於框架默認會在DOMReady時掃描DOM樹,將視圖中的綁定屬性與{{}}插值表達式抽取出來,轉換爲求值函數與視圖刷新函數。ajax
上面的JS代碼至關於:數組
avalon.ready(function() { var vm = avalon.define({ $id: "box", w: 100, h: 100, click: function() { vm.w = parseFloat(vm.w) + 10; vm.h = parseFloat(vm.h) + 10; } }) avalon.scan() })
avalon.scan是一個很是重要的方法,它有兩個可選參數,第一個是掃描的起點元素,默認是HTML標籤,第2個是VM對象。瀏覽器
//源碼 avalon.scan = function(elem, vmodel) { elem = elem || root var vmodels = vmodel ? [].concat(vmodel) : [] scanTag(elem, vmodels) }
視圖模型,ViewModel,也常常被略寫成VM,是經過avalon.define方法進行定義。生成的對象會默認放到avalon.vmodels對象上。 每一個VM在定義時必須指定$id。若是你有某些屬性不想監聽,能夠直接將此屬性名放到$skipArray數組中。
var vm = avalon.define({ $id: "test", a: 111, b: 222, $skipAarray: ["b"], $c: 333, firstName: "司徒", lastName: "正美", fullName: {//一個包含set或get的對象會被當成PropertyDescriptor, set: function(val) {//裏面必須用this指向scope,不能使用scope var array = (val || "").split(" "); this.firstName = array[0] || ""; this.lastName = array[1] || ""; }, get: function() { return this.firstName + " " + this.lastName; } }, array: [1,2,3], array2:[{e: 1}, {e: 2}] d: { k: 111, $skipArray: ["f"], f: 2222 } })
接着咱們說一些重要的概念:
$skipArray 是一個字符串數組,只能放當前對象的直接屬性名,想禁止子對象的某個屬性的監聽,在那個子對象上再添加一個$skipAray數組就好了。
視圖裏面,咱們可使用ms-controller, ms-important指定一個VM的做用域。
此外,在ms-each, ms-with,ms-repeat綁定屬性中,它們會建立一個臨時的VM,咱們稱之爲代理VM, 用於放置$key, $val, $index, $last, $first, $remove等變量或方法。
另外,avalon不容許在VM定義以後,再追加新屬性與方法
var vm = avalon.define({ $id: "test", test1: "點擊測試按鈕沒反應 綁定失敗"; }); vm.one = function() { vm.test1 = "綁定成功"; };
也不容許在define裏面直接調用方法或ajax
avalon.define("test", function(vm){ alert(111) //這裏會執行兩次 $.ajax({ //這裏會發出兩次請來 async:false, type: "post", url: "sdfdsf/fdsfds/dsdd", success: function(data){ console.log(data) avalon.mix(vm, data) } }) })
應該改爲:
var vm = avalon.define({ $id: "test", aaa: "", //這裏應該把全部AJAX都返回的數據都定義好 bbb: "", }) $.ajax({ //這裏會發出兩次請來 async:false, type: "post", url: "sdfdsf/fdsfds/dsdd", success: function(data){ for(var i in data){ if(vm.hasOwnProperty(i)){ vm[i] = data[i] } } } })
isPrototypeOf是用來判斷對象是否存在於另外一個對象的原型鏈當中。
hasOwnProperty判斷一個對象是否擁有指定屬性
咱們再看看如何更新VM中的屬性(重點):
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> <script src="js/avalon.js" type="text/javascript" charset="utf-8"></script> </head> <body> <script> var model = avalon.define({ $id: "update", aaa : "str", bbb : false, ccc : 1223, time : new Date, simpleArray : [1, 2, 3, 4], objectArray : [{name: "a"}, {name: "b"}, {name: "c"}, {name: "d"}], object : { o1: "k1", o2: "k2", o3: "k3" }, simpleArray : [1, 2, 3, 4,5], objectArray : [{name: "a", value: "aa"}, {name: "b", value: "bb"}, {name: "c", value: "cc"}, {name: "d", value: "dd"}], object : { o1: "k1", o2: "k2", o3: "k3" } }) setTimeout(function() { //若是是更新簡單數據類型(string, boolean, number)或Date類型 model.aaa = "這是字符串" model.bbb = true model.ccc = 999999999999 var date = new Date model.time = new Date(date.setFullYear(2005)) }, 2000) setTimeout(function() { //若是是數組,注意保證它們的元素的類型是一致的 //只能全是字符串,或是全是布爾,不能有一些是這種類型,另外一些是其餘類型 //這時咱們可使用set方法來更新(它有兩個參數,第一個是index,第2個是新值) model.simpleArray.set(0, 1000) model.simpleArray.set(2, 3000) model.objectArray.set(0, {name: "xxxxxxxxxxxxxxxx", value: "xxx"}) }, 2500) setTimeout(function() { model.objectArray[1].name = "5555" }, 3000) setTimeout(function() { //若是要更新對象,直接賦給它一個對象,注意不能將一個VM賦給它,能夠到VM的$model賦給它(要不會在IE6-8中報錯) model.object = { aaaa: "aaaa", bbbb: "bbbb", cccc: "cccc", dddd: "dddd" } }, 3000) </script> <div ms-controller="update"> <div>{{aaa}}</div> <div>{{bbb}}</div> <div>{{ccc}}</div> <div>{{time | date("yyyy - MM - dd hh:mm:ss")}}</div> <ul ms-each="simpleArray"> <li>{{el}}</li> </ul> <div> <select ms-each="objectArray"> <option ms-value="el.value">{{el.name}}</option> </select> </div> <ol ms-with="object"> <li>{{$key}} {{$val}}</li> </ol> </div> </body> </html>
avalon的綁定(或指令),擁有如下三種類型:
ms-skip //這個綁定屬性沒有值 ms-controller="expr" //這個綁定屬性沒有參數 ms-if="expr" //這個綁定屬性沒有參數 ms-if-loop="expr" //這個綁定屬性有一個參數 ms-repeat-el="array" //這個綁定屬性有一個參數 ms-attr-href="xxxx" //這個綁定屬性有一個參數 ms-attr-src="xxx/{{a}}/yyy/{{b}}" //這個綁定屬性的值包含插值表達式,注意只有少部分表示字符串類型的屬性可使用插值表達式 ms-click-1="fn" //這個綁定屬性的名字最後有數字,這是方便咱們綁定更多點擊事件 ms-click-2="fn" ms-click-3="fn" ms-on-click="fn" //只有表示事件與類名的綁定屬性的能夠加數字,如這個也能夠寫成 ms-on-click-0="fn" ms-class-1="xxx" ms-class-2="yyy" ms-class-3="xxx" //數字還表示綁定的次序 ms-css-background-color="xxx" //這個綁定屬性有兩個參數,但在css綁定裏,至關於一個,會內部轉換爲backgroundColor ms-duplex-aaa-bbb-string="xxx"//這個綁定屬性有三個參數,表示三種不一樣的攔截操做
s s