各位對於MVVM這種架構應該多多少少有必定的瞭解了,而提到MVVM那麼數據綁定應該是繞不過去的一個話題。數據綁定是MVVM架構中的一個重要組成部分,能夠作到View跟ViewModel之間的解耦,真正的作到UI、邏輯的分離。javascript
在iOS上要是實現MVVM,那麼通常使用RAC
或者RXSwift
來實現數據綁定的功能。而GIC
在單向
、雙向
的數據綁定的實現是基於RAC
來實現的,只是GIC
在實現的過程當中進一步的簡化了數據綁定的方式,可讓開發者僅僅使用一個綁定表達式就能實現數據綁定。前端
在GIC
中,數據綁定
分三種模式,分別是:vue
一次性的綁定,綁定後無論數據源的有沒有更新都不會再次觸發綁定。默認就是這種模式。緣由後面詳細分析java
單向綁定。在once的基礎上,增長了當數據源有更新後自動從新進行綁定的功能。objective-c
雙向綁定。在one way的基礎上,增長了當目標value改變後反向更新數據源的功能。好比:input元素的text屬性支持雙向綁定,當輸入內容有改變的話,會反向將輸入內容更新到數據源。express
GIC
的數據綁定在實際的實現過程當中參考了WPF
、前端VUE
等。要實現數據綁定,那麼必需要有數據源,在GIC
中叫作dataContext
。架構
這裏
數據源
指的是任意NSObject,並非特指ViewModel
,ViewModel
算是一種特殊的數據源,不只提供view所需的數據,還提供view所需的方法、業務邏輯等等,一般將ViewModel
做爲根元素的數據源。異步
當爲某個元素設置數據源後,GIC
會根據先執行該元素上全部的數據綁定,而後遍歷該元素的全部子孫元素,按照順序依次執行子孫元素上的數據綁定。佈局
至關於當爲某個樹的節點設置了數據源後,那麼該節點的全部子孫節點都自動繼承了這個數據源。ui
在GIC
中,爲了可以在綁定的時候支持JS腳本計算,好比:一個lable的text屬性須要綁定到數據源上的name
屬性,而且在前面添加姓名:
的前綴,這時候你就能夠直接以{{'姓名:'+name}}
這樣的綁定表達式來表示,表達式能夠是任意的一段JS代碼,GIC
會自動將表達式的結果賦值給元素的對應屬性上。
另外,在綁定的表達式中你能夠對數據源的任意屬性作計算,這也就是說須要一種方式,可以訪問數據源的任意屬性,並且確保表達式不會過於複雜,好比在一個表達式中訪問多個屬性,{{'姓名:'+name+',性別:'+(sex==1?'男':'女')}}
,對於這樣的表達式計算,若是是直接在native中計算好那天然是沒問題的,可是GIC
做爲一個庫來講,這樣的計算只能由庫來計算,而可以直接完成如此複雜的表達式的,只能是使用腳本類語言去動態計算,好比:JS。所以,GIC
在整個的執行數據綁定的流程中都是圍繞JSValue
來實現的。(注:JSValue
是JavaScriptCore
提供的一種數據類型,用來做爲native跟JS之間互相調用的中間人) ,若是您對什麼是JSValue
不熟悉的話,能夠google下。這樣一來,由JS提供的動態特性就能實現對任意native的數據源作動態計算的能力。
這裏先上一張執行數據綁定的流程圖。
這張流程圖顯示的是once模式下的綁定流程。在這個模式下無需監聽數據源的屬性改變,所以也就無需RAC上場。
這一步相當重要。只有將數據源轉換成
JSValue
才能在JS環境下訪問該數據源,進一步可以執行綁定表達式獲得想要的結果。
之因此有這一步,是爲了JSValue可以訪問非
NSDictionary
的數據類型,好比你自定義的Class。由於JSValue默認只能訪問NSDictionary
中的數據,而對於其餘的數據類型,不論是訪問屬性或者方法都須要你手動加入到JSValue
中,所以這一步就是手動將數據源的全部屬性的keys,轉換成JSValue中的getter方法,這樣就能在JS中訪問任意數據類型的任意屬性了。
在這一步執行表達式後就能獲得最終的結果了。可是
GIC
在這一步上其實也作了其餘的處理。若是您寫過前端代碼,那麼必定對JS裏面的點語法
有了解,在JS中要想訪問某個對象的屬性的話那必需要經過點語法
來訪問的,好比:obj.name。然而GIC
爲了簡化綁定表達式,容許你不用經過點語法來訪問屬性,而是就像訪問變量同樣來直接訪問屬性。這樣一來在執行表達式以前就必須作一個轉換,將數據源的全部的屬性keys變成JS中的var
。
這裏貼一下第四步中將數據源的屬性keys轉換成var
,而後執行表達式的js代碼。
/** * @param props 數據源的屬性keys * @param expStr 綁定表達式 * @returns {*} */
Object.prototype.executeBindExpression2 = function (props, expStr) {
let jsStr = '';
props.forEach((key) => {
jsStr += `var ${key}=this.${key};`;
});
jsStr += expStr;
return (new Function(jsStr)).call(this);
};
複製代碼
在單向綁定的模式中,就須要監聽數據源的屬性改變了,GIC
在這一塊是使用RAC
來實現的。可是問題是,如何肯定到底要監聽哪一個屬性?或者哪些屬性?由於綁定表達式中有可能訪問了多個屬性。
GIC
的在這方面的處理直接採用撞
的方式,就是遍歷數據源的屬性keys,而後看看這個key是否在綁定表達式中,若是存在,那麼就說明須要對這個屬性作監聽,也就是須要使用RAC
。RAC監聽到屬性更改的時候,從新執行綁定流程從而獲得新的結果。
for(NSString *key in allKeys){
if([self.expression rangeOfString:key].location != NSNotFound){
@weakify(self)
[[self.dataSource rac_valuesAndChangesForKeyPath:key options:NSKeyValueObservingOptionNew observer:nil] subscribeNext:^(RACTwoTuple<id,NSDictionary *> * _Nullable x) {
@strongify(self)
[self refreshExpression];
}];
}
}
複製代碼
各位看官可能也發現了,採用
撞
的方式有可能會發生誤判,可是在沒有想到更好的解決方案以前,這樣的方式顯然簡單又高效的。
雙向綁定模式,就是在單向的基礎上增長了反向更新數據源的功能。GIC
實現的雙向綁定流程目前來講其實並不完美,這個也是無奈之舉。
既然是須要反向更新數據源的能力,那麼就得創建一套 View -> 數據源
的機制。也就是創建一套當元素的某個屬性改變的時候可以反向通知GIC
的機制。考慮到並非全部的元素都支持雙向綁定的,好比image元素沒什麼屬性須要提供雙向綁定,而input元素的text屬性卻有必要提供雙向綁定的能力,所以在綜合考慮下,GIC
將這個反向反饋的機制經過protocol
交由元素本身實現,由元素返回一個RACSignal
,而後GIC
的數據綁定訂閱這個Signal
,當這個Signal
產生信號的時候,GIC
就將新的value反向更新到數據源。
實現代碼以下:
// 處理雙向綁定
if(self.bingdingMode == GICBingdingMode_TowWay){
if([self.target respondsToSelector:@selector(gic_createTowWayBindingWithAttributeName:withSignalBlock:)]){
@weakify(self)
[self.target gic_createTowWayBindingWithAttributeName:self.attributeName withSignalBlock:^(RACSignal *signal) {
[[signal takeUntil:[self rac_willDeallocSignal]] subscribeNext:^(id _Nullable newValue) {
@strongify(self)
// 判斷原值和新值是否一致,只有在不一致的時候纔會觸發更新
if(![newValue isEqual:[self.dataSource valueForKey:self.expression]]){
// 將新值更新到數據源
[self.dataSource setValue:newValue forKey:self.expression];
}
}];
}];
}
}
複製代碼
從代碼中能夠看到,這個協議提供的RACSignal
是由一個block
提供的,之因此採用block
的回調方式,那是由於GIC
支持異步解析+佈局+渲染,而在建立雙向綁定的過程當中有可能須要在UI線程訪問元素,所以這裏面使用block的方式,由元素自己決定到底怎麼如何訪問。固然這裏面也可使用線程wait
方式來實現,可是這樣一來就有可能致使解析效率低下。
另外也能夠看到,GIC
是直接使用綁定表達式做爲key來反向設置數據源的屬性的,這也就意味着對於雙向綁定的表達式只能是屬性名,不能是腳本表達式。這個方案也是無奈的方案,由於GIC
能夠知道具體是元素的哪一個屬性產生了Signal
,可是沒法肯定究竟是反向更新到數據源的哪一個屬性,所以這裏面就使用了一個妥協的方案。好在,在實際的開發過程當中,對於雙向綁定的綁定表達式都是比較簡單的。
在實際的開發過程當中,大多數的綁定需求只須要once模式
就好了,再結合RAC
在實現KVO的過程當中會形成額外的內存開銷,所以綜合考慮下來,GIC
的默認綁定模式爲once
上面介紹的綁定流程中的數據源都是針對Native的NSObject來實現的,而自從GIC
支持直接使用JavaScript
來寫業務邏輯後,上面的那套流程就部分不適用了。由於數據源有可能已經直接是JSValue
了。
其實對於once模式
來講,在數據源自己就是JSValue
的狀況下,執行綁定表達式是已經很是簡單的過程,直至參考上面的第四步就好了。
對於one way模式
來講,就不同了。你已經不能經過RAC
來實現對JSValue
屬性的監聽了。JS自己就能夠經過對屬性的setter方法進行重寫從而得到屬性改變的通知。而GIC
在實現的過程當中參考了VUE
的源碼,其實嚴格來講是直接照搬了VUE
的相關源碼,由於vue
已經實現了相關的屬性value變動監控的一套機制了。所以GIC
在這方面的實現上相對來講是比較輕鬆的。下面貼一下對於屬性的監聽代碼。
/** * 添加元素數據綁定 * @param obj * @param bindExp 綁定表達式 * @param cbName * @returns {Watcher} */
Object.prototype.addElementBind = function (obj, bindExp, cbName) {
observe(this);
// 主要是用來判斷哪些屬性須要作監聽
Object.keys(this).forEach((key) => {
if (bindExp.indexOf(key) >= 0) {
let watchers = obj.__watchers__;
if (!watchers) {
watchers = [];
obj.__watchers__ = watchers;
}
let hasW = false;
watchers.forEach((w) => {
if (w.expOrFn === key) {
hasW = true;
}
});
if (!hasW) {
const watcher = new Watcher(this, key, () => {
obj[cbName](this);
});
watchers.push(watcher);
}
// check path
const value = this[key];
if (isObject(value)) {
value.addElementBind(obj, bindExp, cbName);
}
}
});
};
複製代碼
最後對於two way
的實現上,相對於Native的數據源實現來講區別不大。惟一的區別就是反向更新的數據源對象變成了JSValue
// 實現雙向綁定
if(self.bingdingMode == GICBingdingMode_TowWay){
if([self.target respondsToSelector:@selector(gic_createTowWayBindingWithAttributeName:withSignalBlock:)]){
@weakify(self)
[self.target gic_createTowWayBindingWithAttributeName:self.attributeName withSignalBlock:^(RACSignal *signal) {
[[signal takeUntil:[self rac_willDeallocSignal]] subscribeNext:^(id _Nullable newValue) {
// 判斷原值和新值是否一致,只有在不一致的時候纔會觸發更新
@strongify(self)
jsValue.value[self.expression] = newValue;
}];
}];
}
}
複製代碼