在MVVM框架中,你都會看到頁面定了許多奇怪的屬性,好比knockout的data-☆,angular的ng-☆,avalon的ms-☆,此外還有一些只寫文本節點上的雙花括號,它們統稱爲指令。ms-☆因爲定義在元素節點上,是一個特性節點(Attribute),所以稱爲綁定屬性。 雙花括號稱之爲插值表達式,意即這裏在插入ViewModel對應的屬性,或經過加減乘除後獲得的結果。javascript
綁定屬性與插值表達式對於MVVM是很是重要的東西,它們是實現雙向綁定的重要一環,咱們經過它來操做DOM。在angular裏面,還能夠自定義標籤,不過自定義標籤在IE6-8下存在兼容性問題,所以被avalon拋棄了。knockout還有一些特殊的註釋節點作指令,這在UI,OL,LI標籤間會引起各類渲染BUG,這得須要大量代碼來修復,所以avalon也不歡迎它。avalon使用最可靠的特性節點與文本節點的內容作指令,使其兼容性最好。html
avalon的許多指令從是knockout抄過來的,如ms-visible, ms-click, ms-if, ms-with, ms-each……angular興起後,也抄來了它的ms-repeat, ms-include,ms-include-src,{{}}。但ms-on-*這種接第三個參數的設計是從rivetsjs抄來的。java
先說綁定屬性,它的名字是分爲三部分。因爲它是特性節點,根據HTML規範,它只能是全小寫,就算有大寫,瀏覽器也會將它轉換成小寫,這個你們要注意了。每一部分是由「-」隔開,最開始是前綴,avalon的前綴是ms,意即mass, 彌撒,紀念我以前的框架mass Framework。正由於搞了mass Framework,我得到了像jQuery那樣強大的DOM處理能力,掌握大量瀏覽器私有方法與屬性,成噸的黑魔法與飛線方案。換言之,技術是須要積累,每一個框架都存在傳承關係。git
<body AAA="aaa"> test </body>
第二個是指令的名字,如if、visible、on、text、html、include……從它們的名字,你們也能一窺端睨。如if表示是否它輸出到頁面,visible表示是否可見,on是綁定事件,text是原樣輸出,html是轉換爲HTML標籤再輸出,include表示加載子模板到當前位置。avalon就是根據它轉換爲不一樣的視圖刷新函數。angularjs
第三個是指令的參數,好比 咱們能夠ms-on-click,ms-on-keyup,ms-duplex-radio,ms-duplex-bool,ms-duplex-text。此外,還有一個特殊的東西,如ms-click-1,ms-click-2,ms-click-3表示能夠爲某一個元素綁定N個點擊事件。這些咱們之後會慢慢詳述。而綁定屬性的值,則複雜多了,不過具體來講,表示事件的要求對應一個回調的名字,後面可帶小括號可不帶;表示字符串屬性(ms-title, ms-value,ms-src, ms-href, ms-alt)的,裏面能夠添加插值表達式;與類名相關的ms-class, ms-hover, ms-active能夠跟一個冒號,經過它以後表達式計算是否添加移除對應的類名。下面的avalon的綁定屬性的族譜—— github
從這圖,咱們也能夠看出,綁定屬性間也是存在繼承關係的,或者準確地說,一些綁定屬性是從某個綁定屬性衍生出來的。最明顯的是ms-click、ms-keyup、ms-mousedown都是從ms-on-☆衍生出來。它們在框架內部是調用同一方法。數組
var events = oneObject("animationend,blur,change,input,click,dblclick,focus,keydown,keypress,keyup,mousedown,mouseenter,mouseleave,mousemove,mouseout,mouseover,mouseup,scan,scroll,submit") if (events[type]) { param = type type = "on" } else if (type === "enabled") {//吃掉ms-enabled綁定,用ms-disabled代替 type = "disabled" value = "!(" + value + ")" } //吃掉如下幾個綁定,用ms-attr-*綁定代替 if (type === "checked" || type === "selected" || type === "disabled" || type === "readonly") { param = type type = "attr" elem.removeAttribute(name) name = "ms-attr-" + param elem.setAttribute(name, value) match = [name] msData[name] = value } if (typeof bindingHandlers[type] === "function") { var binding = { type: type, param: param, element: elem, name: match[0], value: value, priority: type in priorityMap ? priorityMap[type] : type.charCodeAt(0) * 10 + (Number(param) || 0) }
咱們細看源碼,就會發現,一個綁定屬性會在內部轉換爲一個叫binding的對象,其屬性名的第二個部分,如if, visible變成它的type,第三部分變成param,綁定屬性所在的元素節點就是它的element,原屬性值就是value,原屬性名則是name,此外還有優先級priority。這就涉及avalon另外一個重要的機制了——掃描機制。瀏覽器
絕對大多數MVVM框架是沒有本身的選擇器引擎的,那麼它怎麼找到要處理的元素呢?辦法就在於框架在DOMReady之時,對DOM樹進行全盤掃描,把特殊的標籤,如它有特殊的tagName,有綁定屬性,有插值表達式的元素,所有保存起來。avalon裏就有一個scan方法,它有兩個可選參數,一個是元素節點,另外一個是ViewModel,也能夠是ViewModel數組。順序是從上到下掃描,頁面必需要有ms-controller, ms-important綁定屬性。若是沒有這兩個綁定屬性,就算你寫了ms-visible,ms-text,它們也不會生效,由於它們找不到本身的做用域對象(ViewModel)。ruby
掃描機制通常是從body元素開始,逐等下級,它會跳過script, style, noscript, textarea這幾個元素的內部。而script、noscript、textarea我一般稱之爲模板元素,由於它們在avalon都用於存放子模板。框架
一個元素能夠綁定多個指令,這些指令存在優先級,其中ms-skip的優移動級最高(0),其次是ms-important(1),再次是ms-controller(2),它們決定掃描引擎是否繼續往下掃描,所以排最前面。
其餘元素能夠從內部一個哈希裏查到,越小的優先級越高。
var priorityMap = { "if": 10, "repeat": 90, "widget": 110, "each": 1400, "with": 1500, "duplex": 2000, "on": 3000 }
能夠看到排第4的是if,排第5的是repeat,排第5的是widget,那麼它們與each之間的其餘屬性呢?ms-attr,ms-visible, ms-data的優先級是怎麼計算出來的呢?就要用到下面這條公式了:
type.charCodeAt(0) * 10 + (Number(param) || 0)
好比ms-data-index,它的type爲data,取其第一個字母d,再取其UniCode值,計算獲得100, 再乘以10, 得1000,而後取其第三個參數index強制轉數字,獲得NaN,後跟短路或因而變成0,最後相加,等到1000。這就是它的優先級了。
因爲屬性名只能是小寫,所以就是z開頭,最大也只能是1220上下。最後計算獲得它們的移動後順序爲:
ms-skip(0) --> ms-important(1) --> ms-controller(2) --> ms-if(10) --> ms-repeat(90) -->ms-if-loop(110) --> ms-attr(970) ...--> ms-each(1400)-->ms-with(1500)-->ms-duplex(2000)-->ms-duplex(3000)墊後
有關掃描順序更詳細的介紹可見這裏,不過用戶沒必要在乎它們,框架這樣巧妙的設計已經保證你爲同一個元素綁定N個指令,它們都能相安無事地正常執行。
最後咱們看一下綁定屬性能爲咱們作什麼吧,這裏有一個圖,展現了它們的全部功能。下一章節起來咱們就根據它逐個擊破了。
<!DOCTYPE html> <html> <head> <title>ms-duplex</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <script src="avalon.js" ></script> <script> var model = avalon.define("model", function(vm) { vm.textModel = "text"; vm.passwordModel = "password" vm.radioModel = true; vm.checkRadio = true; vm.selectModel = "bbb"; vm.checkboxModel = ["aaa", "bbb"]; vm.checkboxText = vm.checkboxModel.join(",") vm.sex = "1" }); var qinerg = avalon.define("qinerg", function(vm) { vm.sex = ""; vm.lang = [] vm.langtext = "" }); qinerg.lang.$watch("length", function() { qinerg.langtext = qinerg.lang.join(",") }) model.checkboxModel.$watch("length", function() { model.checkboxText = model.checkboxModel.join(",") }) var dynamic = avalon.define("dynamic", function(vm) { vm.langtext = "" vm.array = ["aaa", "bbb", "ccc"] vm.lang = [] }) dynamic.lang.$watch("length", function() { dynamic.langtext = dynamic.lang.join(",") }) </script> <style> .parent{ height:50px; width:100%; overflow:hidden; } .visible{ width:400px; height: 50px; background: red; float:left; } .if{ width:400px; height: 50px; background: blueviolet; float:left; } fieldset{ background:#d2d2d2; } </style> </head> <body > <div ms-controller="model"> <h3 style="text-align: center">ms-duplex</h3> <input ms-duplex="textModel" ms-data-duplex-observe="radioModel"/> <input ms-duplex="passwordModel" type="password"/> <input type="radio" ms-duplex="radioModel"> <input type="checkbox" ms-duplex-radio="checkRadio"> <select ms-duplex="selectModel"> <option value="aaa" selected>aaa</option> <option value="bbb">bbb</option> <option value="ccc">ccc</option> </select> <input ms-duplex="checkboxModel" type="checkbox" value="aaa" /> <input ms-duplex="checkboxModel" type="checkbox" value="bbb" /> <input ms-duplex="checkboxModel" type="checkbox" value="ccc" /> <input ms-duplex-text="sex" type="radio" value="1"/> <input ms-duplex-text="sex" type="radio" value="2"/> <input ms-duplex-text="sex" type="radio" value="3"/> {{sex}} <p>text, password, textarea要求對應的VM屬性爲字符串, select、checkbox爲字符串數組,radio爲布爾, 咱們能夠經過ms-duplex-radio讓checkbox對應一個布爾, ms-duplex-text讓radio對應一個字符串 </p> <div class="parent"> <div ms-visible="radioModel" class="visible"> <div>ms-visible這個區域是受到radioModel控制</div> <div>data-duplex-observe爲{{radioModel}}</div> </div> <div ms-if="checkRadio" class="if"> <div> ms-if這個區域是受到checkRadio控制</div> </div> </div> <fieldset> <legend>textModel</legend> <p>{{textModel}}</p> </fieldset> <fieldset> <legend>passwordModel</legend> <p>{{passwordModel}}</p> </fieldset> <fieldset> <legend>radioModel</legend> <p>{{radioModel}}</p> </fieldset> <fieldset> <legend>selectModel</legend> <p>{{selectModel}}</p> </fieldset> <fieldset> <legend>checkboxModel</legend> <p>{{checkboxText}}</p> </fieldset> </div> <div ms-controller='qinerg' > <p><input type="radio" ms-duplex-text="sex" value="man">男 <input type="radio" ms-duplex-text="sex" value="woman">女</p> <p> <input type="checkbox" ms-duplex="lang" value="#C">#C <input type="checkbox" ms-duplex="lang" value="java">java <input type="checkbox" ms-duplex="lang" value="ruby">ruby </p> <P>{{sex}} {{langtext}}</P> </div> <fieldset ms-controller="dynamic"> <div ms-each="array"> <div><input type="checkbox" ms-duplex="lang" ms-value="{{el}}">{{el}}</div> </div> <div>{{langtext}}</div> </fieldset> </body> </html>