iOS混合開發庫(GICXMLLayout)6、數據綁定原理

各位對於MVVM這種架構應該多多少少有必定的瞭解了,而提到MVVM那麼數據綁定應該是繞不過去的一個話題。數據綁定是MVVM架構中的一個重要組成部分,能夠作到View跟ViewModel之間的解耦,真正的作到UI、邏輯的分離。javascript

在iOS上要是實現MVVM,那麼通常使用RAC或者RXSwift來實現數據綁定的功能。而GIC單向雙向的數據綁定的實現是基於RAC來實現的,只是GIC在實現的過程當中進一步的簡化了數據綁定的方式,可讓開發者僅僅使用一個綁定表達式就能實現數據綁定。前端

GIC中,數據綁定三種模式,分別是:vue

  1. once:

    一次性的綁定,綁定後無論數據源的有沒有更新都不會再次觸發綁定。默認就是這種模式。緣由後面詳細分析java

  2. one way:

    單向綁定。在once的基礎上,增長了當數據源有更新後自動從新進行綁定的功能。objective-c

  3. two way:

    雙向綁定。在one way的基礎上,增長了當目標value改變後反向更新數據源的功能。好比:input元素的text屬性支持雙向綁定,當輸入內容有改變的話,會反向將輸入內容更新到數據源。express

原理剖析

GIC的數據綁定在實際的實現過程當中參考了WPF前端VUE等。要實現數據綁定,那麼必需要有數據源,在GIC中叫作dataContext架構

這裏數據源指的是任意NSObject,並非特指ViewModelViewModel算是一種特殊的數據源,不只提供view所需的數據,還提供view所需的方法、業務邏輯等等,一般將ViewModel做爲根元素的數據源。異步

當爲某個元素設置數據源後,GIC會根據先執行該元素上全部的數據綁定,而後遍歷該元素的全部子孫元素,按照順序依次執行子孫元素上的數據綁定。佈局

至關於當爲某個樹的節點設置了數據源後,那麼該節點的全部子孫節點都自動繼承了這個數據源。ui

GIC中,爲了可以在綁定的時候支持JS腳本計算,好比:一個lable的text屬性須要綁定到數據源上的name屬性,而且在前面添加姓名:的前綴,這時候你就能夠直接以{{'姓名:'+name}}這樣的綁定表達式來表示,表達式能夠是任意的一段JS代碼,GIC會自動將表達式的結果賦值給元素的對應屬性上。

另外,在綁定的表達式中你能夠對數據源的任意屬性作計算,這也就是說須要一種方式,可以訪問數據源的任意屬性,並且確保表達式不會過於複雜,好比在一個表達式中訪問多個屬性,{{'姓名:'+name+',性別:'+(sex==1?'男':'女')}},對於這樣的表達式計算,若是是直接在native中計算好那天然是沒問題的,可是GIC做爲一個庫來講,這樣的計算只能由庫來計算,而可以直接完成如此複雜的表達式的,只能是使用腳本類語言去動態計算,好比:JS。所以,GIC在整個的執行數據綁定的流程中都是圍繞JSValue來實現的。(注:JSValueJavaScriptCore提供的一種數據類型,用來做爲native跟JS之間互相調用的中間人) ,若是您對什麼是JSValue不熟悉的話,能夠google下。這樣一來,由JS提供的動態特性就能實現對任意native的數據源作動態計算的能力。

once 綁定模式

這裏先上一張執行數據綁定的流程圖。

數據綁定流程

這張流程圖顯示的是once模式下的綁定流程。在這個模式下無需監聽數據源的屬性改變,所以也就無需RAC上場。

  1. 第一步。提取解析表達式,而且判斷綁定模式。
  2. 第二步。將數據源轉換成JSValue。

    這一步相當重要。只有將數據源轉換成JSValue才能在JS環境下訪問該數據源,進一步可以執行綁定表達式獲得想要的結果。

  3. 第三步。爲JSValue的全部屬性添加getter方法。

    之因此有這一步,是爲了JSValue可以訪問非NSDictionary的數據類型,好比你自定義的Class。由於JSValue默認只能訪問NSDictionary中的數據,而對於其餘的數據類型,不論是訪問屬性或者方法都須要你手動加入到JSValue中,所以這一步就是手動將數據源的全部屬性的keys,轉換成JSValue中的getter方法,這樣就能在JS中訪問任意數據類型的任意屬性了。

  4. 第四步。執行綁定表達式。

    在這一步執行表達式後就能獲得最終的結果了。可是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);
};
複製代碼

one way 模式

在單向綁定的模式中,就須要監聽數據源的屬性改變了,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];
        }];
    }
}
複製代碼

各位看官可能也發現了,採用的方式有可能會發生誤判,可是在沒有想到更好的解決方案以前,這樣的方式顯然簡單又高效的。

two way 模式

雙向綁定模式,就是在單向的基礎上增長了反向更新數據源的功能。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

JavaScript對象做爲數據源的綁定實現原理。

上面介紹的綁定流程中的數據源都是針對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;
            }];
        }];
    }
}
複製代碼
相關文章
相關標籤/搜索