廢話說了大幾篇,我們開始來點干貨了~
ViewModel的內部機制
- 在MVVM中,數據是核心。而jQuery則以DOM為核心。
- 而DOM只是HTML在JS的世界的抽象,是一個很易變的東西。因此如果業務代碼遍歷選擇器表達式會非常難維護。但不可否認,jQuery是操作DOM的王者,讓我們操作DOM順手拈來。但如果不讓你操作DOM,不是更好嗎?就像jQuery不讓你用getElementById,getElementsByTagName, querySelecterAll,大家都不知道里面有多少坑,短短幾個字母$(expr)是背后sizzle選擇器引擎1700行的實現!!!!jQuery其實是在用戶代碼與原生API中提供一層厚厚的粘合層,因此摸起來光溜溜。在MVVM中,DOM操作基本是水下運作了。由於VM與V之間的雙向綁定,操作了VM中的數據(當然只能是監控屬性),就會同步到DOM,我們透過DOM事件監控用戶對DOM的改動,也會同步到VM。DOM隱形了,就像軟件公司,到處跑出來活動的是業務員與不寫代碼的經理老總,程序員全部關起來加班!雖然這比喻有點殘酷,但這正體現了各司其職的威力。能說會道去拉風投接單子沒什么不妥,喜歡呆在電腦前的就讓他呆吧。jQuery的世界就是一個混亂的公司,全能的程序員什么都做。
定義一個ViemMode
<fieldset ms-controller="simple"> <legend>例子</legend> <p>First name: <input ms-model="firstName" /></p> <p>Last name: <input ms-model="lastName" /></p> <p>Hello, <input ms-model="fullName"></p> <div>{{firstName +" | "+ lastName }}</div> <p>nick name: <input ms-model="nick.name" /></p> <p>{{nick.name}}</p> </fieldset> avalon.define("simple", function(vm) { vm.firstName = "司徒" vm.lastName = "正美" vm.fullName = {//一個包含set或get的對象會被當成PropertyDescriptor, set: function(val) {//set, get里面的this不能改成vm var array = (val || "").split(" "); this.firstName = array[0] || ""; this.lastName = array[1] || ""; }, get: function() { returnthis.firstName + " " + this.lastName; } }, vm.nick = { name: "暗黑之民" } });
這是官方給出的DEMO,我們看看對應的操作定義
HTML中:
- ms-controller是用於指定ViewModel的作用范圍, ms-controller的值等於avalon.define的第一個參數,並且這個值必須是一個命法的變量名, 如aaa, $aaa, aaaSSS, aaa_bbb,不能寫成23432, sdfs-A
- ms-model="firstName" 此類綁定只能用於表單中,框架會在上面綁定一些事件,如input, change, click以進行同步
- {{firstName +" | "+ lastName }} 模版機制,插值表達式,用於替換值
Javascript中:
- ViewModel的定義,它是通過avalon.define來創建,在函數內我們定義它的屬性與方法
- vm.firstName 監控屬性:定義時為一個簡單的數據類型,如undefined, string, number, boolean。
- vm.fullName 計算屬性:定義時為一個最多擁有get,set方法的對象(get方法是必需的),注意,get, set里面的this不能改為vm,框架內部會幫你調整好指向
- 監控數組:定義時為一個數組
- 普通屬性或方法:我們可以在vm里面設置一個$skipArray數組,里面裝着你不想處理的方法與屬性名
因為在ViewModel的轉化中會用到defineProperty的定義,有必要先預先提出來
要了解詳細,見我的一篇譯文 (譯)ECMAScript 5 Objects and Properties
JavaScript中有三種不同類型的屬性:
命名數據屬性(named data properties
命名數據屬性,就是我們在IE8碰到的絕對大多數屬性,可以隨意刪除添加,設置什么返回什么,不會在內部做多余的事。
var obj = { prop: 123 }; console.log(obj.prop); // 123 console.log(obj["prop"]); // 123 obj.prop = "abc"; obj["prop"] = "abc";
命名訪問器屬性(named accessor properties)
- 命名訪問器屬性,就是設置或讀取時內部調用一些函數做事情的函數,著名的代表是元素的innerHTML,給它一個字符串會創建一大堆節點,讀它時返回的值與我們給它的值可能不一樣。 又如數組的length,可能通過它來添加或刪除元素。IE8添加了set get關鍵字,不過沒什么人用。不過它又添加了著名的Object.defineProperty方法, 里面可指定讀取時或寫入時的處理函數。標准瀏覽器老早就支持__defineGetter__,__defineSetter__。
var obj = {} var _a = 1; Object.defineProperty(obj, "a", { get: function() { return _a }, set: function(a) { _a = a + 10 } }); console.log(obj.a) //1; obj.a = 20; console.log(obj.a) //30;
- 計算屬性的set, get函數其實就是對應它們倆。
- avalon, emberjs的ViewModel就是基於訪問器實現的,不過emberjs只兼容到IE8。
內部屬性就是無法通過JavaScript直接訪問的屬性
走進vm的幕后:
源碼:
1 avalon.define = function(name, deps, factory) { 2 var args = [].slice.call(arguments); 3 if (typeof name !== "string") { 4 name = generateID(); 5 args.unshift(name); 6 } 7 if (!Array.isArray(args[1])) { 8 args.splice(1, 0, []); 9 } 10 deps = args[1]; 11 if (typeof args[2] !== "function") { 12 avalon.error("factory必須是函數"); 13 } 14 factory = args[2]; 15 var scope = { 16 $watch: noop 17 }; 18 deps.unshift(scope); 19 factory(scope); //得到所有定義 20 var model = modelFactory(scope); //轉為一個ViewModel 21 stopRepeatAssign = true; 22 deps[0] = model; 23 factory.apply(0, deps); //重置它的上下文 24 deps.shift(); 25 stopRepeatAssign = false; 26 model.$id = name; 27 return avalon.models[name] = model; 28 };
我們一行行分析:
- avalon.define 的定義能接受3個實參
- var args = [].slice.call(arguments); 轉換數組,arguments是偽數組
- 保證傳參數滿足3個定義 如果第二個參數不是數組,轉換 avalon.define("on",fn); -> avalon.define("on",[],fn);
var scope = { $watch: noop };
定義一個作用域,是一個對象,這個東東其實就是暴露給用戶的一個接口,也就是vm了,其實VM是后台先創建的
factory(scope); //得到所有定義
對象嘛是引用,執行后就會把用戶定義的方法給掛到scope上了,這樣就達到收集用戶在外面的處理方法了
var model = modelFactory(scope); //
這個就是核心的東東了,把scpoe轉為一個ViewModel,只有轉化之后,才能讓我們的東東具有實際的處理能力了
factory.apply(0, deps);
這是個非常巧妙的設計,用戶定義的函數內部的作用域其實還是在普通的對象,我們可以強制轉化vm
return avalon.models[name] = model;
很明顯轉化后的模型對象掛在到了全局中,方便在掃描節點綁定中獲取
所以整個VM的創建過程,
核心點就是
modelFactory方法了
下篇繼續着中分析~