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;
            }];
        }];
    }
}
复制代码
相关文章
相关标签/搜索