You should know more or less about the ARCHITECTURE of MVVM, and when it comes to MVVM, data binding should be an unavoidable topic. Data binding is an important part of MVVM architecture, which can achieve decoupling between View and ViewModel, and truly achieve the separation of UI and logic.

If MVVM is implemented on iOS, it is common to use RAC or RXSwift for data binding. GIC’s implementation of one-way and two-way data binding is based on RAC, but GIC further simplifies the way of data binding in the process of implementation, allowing developers to achieve data binding only by using a binding expression.

In GIC, data binding is divided into three modes:

  1. Once:

    A one-time binding that does not trigger the binding again regardless of whether the data source is updated. The default is this mode. The reasons will be analyzed in detail later

  2. One way:

    One-way binding. On the basis of once, the function of automatic rebinding is added when the data source is updated.

  3. Two way:

    Bidirectional binding. On the basis of one way, the function of reverse updating data source is added when the target value changes. For example, the text attribute of the input element supports bidirectional binding, which updates the input back to the data source when it changes.

The principle of analyzing

GIC data binding refers to WPF and front-end VUE in the actual implementation process. To implement data binding, you must have a data source, called dataContext in GIC.

A ViewModel is a special kind of data source that provides not only the data needed by the view, but also the methods, business logic, and so on that the view needs. The ViewModel is usually used as the data source for the root element.

After setting a data source for an element, GIC will perform all data binding on the element first and then iterate over all the descendants of the element, performing the data binding on the descendants in sequence.

When a data source is set for a node in a tree, all descendants of the node automatically inherit the data source.

In GIC, in order to support JS script computation at binding time, for example, a text property of a lable needs to be bound to the name property of the data source, and prefixed with the name:, you can use {{‘ name: : The expression can be any section of JS code, and GIC will automatically assign the result of the expression to the corresponding attribute of the element.

In addition, you can evaluate any property of the data source in a bound expression, which means that you need a way to access any property of the data source without making the expression too complicated, such as accessing multiple properties in a single expression, {{‘ name :’+name+’, gender :’+(sex==1? For such expression calculation, there is no problem if it is directly calculated in native, but GIC as a library, such calculation can only be calculated by the library, and can directly complete such complex expression, can only use scripting language to dynamically calculate, such as: JS. Therefore,GIC is implemented around JSValue in the entire data binding process. JSValue is a data type provided by JavaScriptCore, which acts as an intermediary between native and JS calls. If you are not familiar with JSValue, Google it. In this way, dynamic features provided by JS can realize the ability of dynamic calculation to any native data source.

Once binding mode

Here is a flowchart for performing data binding.

This flowchart shows the binding process in once mode. In this mode, there is no need to listen for data source property changes, so RAC is not needed.

  1. The first step. Extract the parse expression and determine the binding pattern.
  2. The second step. Convert the data source to JSValue.

    This step is crucial. Only by converting the data source to JSValue can it be accessed in the JS environment, further enabling binding expressions to get the desired results.

  3. The third step. Add getter methods for all properties of JSValue.

    The reason for this step is so that JSValue can access non-NSDictionary data types, such as your custom classes. Because JSValue can only access data in the NSDictionary by default, for other data types, you need to manually add attributes or methods to the JSValue, so this step is to manually convert all the keys of the data source properties into getters in the JSValue. This allows you to access any property of any data type in JS.

  4. Step 4. Execute the binding expression.

    After executing the expression in this step, you get the final result. But GIC has also done something else with this step. If you have written front-end code, then you must understand the point syntax inside JS, in JS to access an object’s attributes that must be accessed through the point syntax, such as :obj.name. GIC, however, simplifies binding expressions by allowing you to access properties directly as if they were variables, rather than via dot syntax. Before the expression can be executed, a transformation must be done to change all the data source property keys to JS var.

Here is the fourth step to convert the data source property keys to var, and then execute the expression’s JS code.

/** * @param props properties keys * @param expStr binding expression * @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);
};
Copy the code

One way model

In the one-way binding mode, it is necessary to monitor the change of data source properties. GIC uses RAC to achieve this. But the question is, how do you decide which property to listen for? Or what properties? Because it is possible to access multiple properties in a binding expression.

GIC directly uses the collision method in this aspect, that is, traversing the property keys of the data source, and then checking whether the key is in the binding expression. If so, it means that it needs to monitor the property, that is, RAC is needed. When RAC listens for property changes, it reexecutes the binding process to get the new results.

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];
        }];
    }
}
Copy the code

You may also find that the collision method may lead to misjudgment, but until a better solution is thought of, this method is obviously simple and efficient.

Two way mode

The bidirectional binding mode adds the function of updating data source reversely on the basis of one-way. GIC’s bidirectional binding process is not perfect at present, which is also a helpless move.

Since the ability to reverse update the data source is required, a View -> data source mechanism is required. That is, to establish a mechanism that can reverse notify GIC when an attribute of an element changes. Considering that not all elements support bidirectional binding, for example, image element has no attribute that needs to provide bidirectional binding, while text attribute of input element needs to provide bidirectional binding capability, GIC assigns this reverse feedback mechanism to the element itself through protocol under comprehensive consideration. The RACSignal is returned by the element, and GIC’s data binding subscribes to this Signal. When the Signal is generated, GIC reverses the new value to the data source.

The implementation code is as follows:

// Handle bidirectional binding 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 [self rac_willDeallocSignal]] subscribeNext:^(id _Nullable newValue) {@strongify(self) Only when inconsistent will trigger updates the if (! [newValue isEqual: [self. The dataSource valueForKey: self. The expression]]) {/ / will update to the data source new value [self. The dataSource setValue:newValue forKey:self.expression]; } }]; }]; }}Copy the code

As can be seen from the code, RACSignal provided by this protocol is provided by a block. The reason for the use of block callback is that GIC supports asynchronous parsing + layout + rendering. In the process of creating bidirectional binding, it may need to access elements in the UI thread, so the use of block in this way. It is up to the element itself to determine how it is accessed. Of course, this can be implemented using threaded WAIT, but this can lead to inefficient parsing.

In addition, it can be seen that GIC directly uses the binding expression as the key to reverse the setting of data source attributes, which means that the bidirectional binding expression can only be the attribute name, not the script expression. GIC can know which attribute of the element generates the Signal, but cannot determine which attribute of the data source is retroactively updated, so a compromise scheme is used. Fortunately, in real development, binding expressions for bidirectional binding are relatively simple.

In actual development, most binding requirements are only requiredOnce patternsThat’s it. RecombineRACThere is an additional memory overhead in implementing KVO, so all things considered,GICThe default binding mode ofonce

How JavaScript objects are implemented as data sources.

The data sources in the binding process described above are implemented for Native NSObject, and the above process is partially inapplicable since GIC supports writing business logic directly in JavaScript. Because the data source might already be a JSValue.

In the case of the once mode, where the data source itself is a JSValue, executing the binding expression is a fairly straightforward process until you refer to step 4 above.

For the One Way model, it’s different. You can no longer listen on JSValue properties using RAC. JS itself can override setter methods for properties to get notification of property changes. GIC referred to the source code of VUE in the implementation process. In fact, strictly speaking, IT directly copied the relevant source code of VUE, because VUE has implemented a set of mechanism for monitoring the change of the related attribute value. Therefore, GIC is relatively easy to achieve in this respect. Here is the listening code for the property.

/** * Add element data binding * @param obj * @param bindExp binding expression * @param cbName * @returns {Watcher} */
Object.prototype.addElementBind = function (obj, bindExp, cbName) {
  observe(this);
  // It is used to determine which attributes need to be listened on
  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); }}}); };Copy the code

Finally, there is little difference between the two way implementation and the Native data source implementation. The only difference is that the reverse-updated data source object becomes a 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) {// check whether the original value is the same as the newValue. @stronGify (self) jsvalue.value [self.expression] = newValue;}];}]; }}Copy the code