接上一章 ViewModel
modelFactory工廠是如何加工用戶定義的VM?
附源碼
-
洋洋灑灑100多行內部是魔幻般的實現
1: function modelFactory(scope) {
2: var skipArray = scope.$skipArray, //要忽略監控的屬性名列表
3: model = {},
4: Descriptions = {}, //內部用於轉換的對象
5: json = {},
6: callSetters = [],
7: callGetters = [],
8: VBPublics = Object.keys(watchOne); //用於IE6-8
9: skipArray = Array.isArray(skipArray) ? skipArray.concat(VBPublics) : VBPublics;
10: forEach(scope, function(name, value) {
11: if (!watchOne[name]) {
12: json[name] = value;
13: }
14: var valueType = avalon.type(value);
15: if (valueType === "Function") {
16: VBPublics.push(name); //函數無需要轉換
17: } else {
18: if (skipArray.indexOf(name) !== -1) {
19: return VBPublics.push(name);
20: }
21: if (name.charAt(0) === "$" && !systemOne[name]) {
22: return VBPublics.push(name);
23: }
24: var accessor, oldArgs;
25: if (valueType === "Object" && typeof value.get === "function" && Object.keys(value).length <= 2) {
26: var setter = value.set,
27: getter = value.get;
28: accessor = function(neo) { //創建計算屬性
29: if (arguments.length) {
30: if (stopRepeatAssign) {
31: return; //阻止重復賦值
32: }
33: if (typeof setter === "function") {
34: setter.call(model, neo);
35: }
36: if (oldArgs !== neo) { //由於VBS對象不能用Object.prototype.toString來判定類型,我們就不做嚴密的檢測
37: oldArgs = neo;
38: notifySubscribers(accessor); //通知頂層改變
39: model.$events && model.$fire(name, neo, value);
40: }
41: } else {
42: if (openComputedCollect || !accessor.locked) {
43: collectSubscribers(accessor);
44: }
45: return value = json[name] = getter.call(model); //保存新值到json[name]
46: }
47: };
48: accessor.nick = name;
49: callGetters.push(accessor);
50: } else {
51: value = NaN;
52: callSetters.push(name);
53: accessor = function(neo) { //創建監控屬性或數組
54: if (arguments.length) {
55: if (stopRepeatAssign) {
56: return; //阻止重復賦值
57: }
58: if (value !== neo) {
59: var old = value;
60: if (valueType === "Array" || valueType === "Object") {
61: if (value && value.$id) {
62: updateViewModel(value, neo, Array.isArray(neo));
63: } else if (Array.isArray(neo)) {
64: value = Collection(neo, model, name);
65: } else {
66: value = modelFactory(neo);
67: }
68: } else {
69: value = neo;
70: }
71: json[name] = value && value.$id ? value.$json : value;
72: notifySubscribers(accessor); //通知頂層改變
73: model.$events && model.$fire(name, value, old);
74: }
75: } else {
76: collectSubscribers(accessor); //收集視圖函數
77: return value;
78: }
79: };
80: }
81: accessor[subscribers] = [];
82: Descriptions[name] = {
83: set: accessor,
84: get: accessor,
85: enumerable: true
86: };
87: }
88: });
89: if (defineProperties) {
90: defineProperties(model, Descriptions);
91: } else {
92: model = VBDefineProperties(Descriptions, VBPublics);
93: }
94: VBPublics.forEach(function(name) {
95: if (!watchOne[name]) {
96: model[name] = scope[name];
97: }
98: });
99: callSetters.forEach(function(prop) {
100: model[prop] = scope[prop]; //為空對象賦值
101: });
102: callGetters.forEach(function(fn) {
103: Publish[expose] = fn;
104: callSetters = model[fn.nick];
105: fn.locked = 1;
106: delete Publish[expose];
107: });
108: model.$json = json;
109: model.$events = {}; //VB對象的方法里的this並不指向自身,需要使用bind處理一下
110: model.$watch = Observable.$watch.bind(model);
111: model.$unwatch = Observable.$unwatch.bind(model);
112: model.$fire = Observable.$fire.bind(model);
113: model.$id = generateID();
114: model.hasOwnProperty = function(name) {
115: return name in model.$json;
116: };
117: return model;
118: }
- VM是用ecma262v5的新API, Object.defineProperties生成的一個充滿訪問器的對象,這樣的對象,能通過用戶對它的屬性的讀寫,觸發定義時的getter, setter函數。getter, setter對rubyer, pythoner, C#er應該很熟悉,我就不展開了。
-
舊式IE,avalon利用VBScript的類實例,它也存在其他語言的訪問器。不過,VBS對象不像JS對象那樣隨意添加新屬性,刪除已有屬性,因此我們就無法監后添加的新屬性。Object.defineProperties也一樣,它能處理的屬性也只是它定義時的屬性,想監控后來的,需要再調用一次Object.defineProperties。
整個工廠方法內部都是圍繞着scope處理
- 過濾監控的屬性
- 收集視圖函數
- 轉換用於定義
skipArray //要忽略監控的屬性名列表
0: "$json" 1: "$skipArray" 2: "$watch" 3: "$unwatch" 4: "$fire" 5: "$events"
我們還是已官網的demo為列
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() { return this.firstName + " " + this.lastName; } } }) avalon.scan(document.querySelector("fieldset"));
此時傳入的vm為
$watch: function noop() { firstName: "司徒" fullName: Object lastName: "正美"
意圖很明顯就是遍歷這些屬性,給出相對應的處理,具體我們接着往下看
純凈的js對象,所有訪問器與viewModel特有的方法屬性都去掉
1: if (!watchOne[name]) {
2: json[name] = value;
3: }
幾個簡單的條件過濾:
1: //判斷類型
2: var valueType = avalon.type(value);
3:
4: if (valueType === "Function") {
5: // 第一個就是$watch" 被重復假如到列表了
6: VBPublics.push(name); //函數無需要轉換
7: } else {
跳過過濾的條件后:
核心的轉換
- 轉換計算屬性
- 轉化監控屬性
轉換計算屬性:
- 定義時為一個最多擁有get,set方法的對象(get方法是必需的)
- 注意,get, set里面的this不能改為vm,框架內部會幫你調整好指向。
判斷的條件,值類型是對象,並且有get方法,並且方法要少於等於2個
if (valueType === "Object" && typeof value.get === "function" && Object.keys(value).length <= 2) {
滿足條件的
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() {
return this.firstName + " " + this.lastName;
}
}
具體有什么用我們接着往下看
轉化監控屬性
- 定義時為一個簡單的數據類型,如undefined, string, number, boolean。
- 監控數組:定義時為一個數組
firstName: "司徒"
accessor[subscribers] = [];
- 別看這個代碼是空的函數,不起眼,雙向綁定就是看他了,我們先Mark下
//生成defineProperties需要的配置屬性 Descriptions[name] = { set: accessor, get: accessor, enumerable: true };
- Descriptions臨時對象 //收集內部用於轉換的對象
- enumerable 很重要,為false的話 ,for in就找不到它了
這樣循環后就把該干嘛的不該干嘛的都給區分開了
最后都保存在Descriptions中
此時的Descriptions
1: Descriptions: Object
2:
3: firstName: Object
4: enumerable: true
5: get: function (neo) { //創建監控屬性或數組
6: set: function (neo) { //創建監控屬性或數組
7:
8: fullName: Object
9: enumerable: true
10: get: function (neo) { //創建計算屬性
11: set: function (neo) { //創建計算屬性
12:
13: lastName: Object
14: enumerable: true
15: get: function (neo) { //創建監控屬性或數組
16: set: function (neo) { //創建監控屬性或數組
看吧就是這樣給包裝了一下,只是定義了但是還沒生效
所以defineProperties(model, Descriptions); 給執行以下 (defineProperties的方法見前面)
model 就是工廠模式轉換后的新的vm模型對象了, 因為在開始遍歷scope的過濾了一些東東,原本也是用戶定義的,所以這時候我們還得加到新的vm-》model中去、
//添加用戶定義的未轉換的函數到模型 VBPublics.forEach(function(name) { if (!watchOne[name]) { model[name] = scope[name]; } });
