iOS混合开发库(GICXMLLayout)六、数据绑定原理

栏目: 数据库 · Oracle · 发布时间: 5年前

内容简介:各位对于MVVM这种架构应该多多少少有一定的了解了,而提到MVVM那么数据绑定应该是绕不过去的一个话题。数据绑定是MVVM架构中的一个重要组成部分,可以做到View跟ViewModel之间的解耦,真正的做到UI、逻辑的分离。在iOS上要是实现MVVM,那么一般使用在

各位对于MVVM这种架构应该多多少少有一定的了解了,而提到MVVM那么数据绑定应该是绕不过去的一个话题。数据绑定是MVVM架构中的一个重要组成部分,可以做到View跟ViewModel之间的解耦,真正的做到UI、逻辑的分离。

在iOS上要是实现MVVM,那么一般使用 RAC 或者 RXSwift 来实现数据绑定的功能。而 GIC单向双向 的数据绑定的实现是基于 RAC 来实现的,只是 GIC 在实现的过程中进一步的简化了数据绑定的方式,可以让开发者仅仅使用一个绑定表达式就能实现数据绑定。

GIC 中, 数据绑定三种模式 ,分别是:

  1. once:

    一次性的绑定,绑定后不管数据源的有没有更新都不会再次触发绑定。默认就是这种模式。原因后面详细分析

  2. one way:

    单向绑定。在once的基础上,增加了当数据源有更新后自动重新进行绑定的功能。

  3. two way:

    双向绑定。在one way的基础上,增加了当目标value改变后反向更新数据源的功能。比如:input元素的text属性支持双向绑定,当输入内容有改变的话,会反向将输入内容更新到数据源。

原理剖析

GIC 的数据绑定在实际的实现过程中参考了 WPF前端VUE 等。要实现数据绑定,那么必须要有数据源,在 GIC 中叫做 dataContext

这里 数据源 指的是任意NSObject,并不是特指 ViewModelViewModel 算是一种特殊的数据源,不仅提供view所需的数据,还提供view所需的方法、业务逻辑等等,通常将 ViewModel 作为根元素的数据源。

当为某个元素设置数据源后, GIC 会根据先执行该元素上所有的数据绑定,然后遍历该元素的所有子孙元素,按照顺序依次执行子孙元素上的数据绑定。

相当于当为某个树的节点设置了数据源后,那么该节点的所有子孙节点都自动继承了这个数据源。

GIC 中,为了能够在绑定的时候支持JS脚本计算,比如:一个lable的text属性需要绑定到数据源上的 name 属性,并且在前面添加 姓名: 的前缀,这时候你就可以直接以 {{'姓名:'+name}} 这样的绑定表达式来表示,表达式可以是任意的一段JS代码, GIC 会自动将表达式的结果赋值给元素的对应属性上。

另外,在绑定的表达式中你可以对数据源的任意属性做计算,这也就是说需要一种方式,能够访问数据源的任意属性,而且确保表达式不会过于复杂,比如在一个表达式中访问多个属性, {{'姓名:'+name+',性别:'+(sex==1?'男':'女')}} ,对于这样的表达式计算,如果是直接在native中计算好那自然是没问题的,但是 GIC 作为一个库来说,这样的计算只能由库来计算,而能够直接完成如此复杂的表达式的,只能是使用脚本类语言去动态计算,比如:JS。因此, GIC 在整个的执行数据绑定的流程中都是围绕 JSValue 来实现的。(注: JSValueJavaScriptCore 提供的一种数据类型,用来作为native跟JS之间互相调用的中间人) ,如果您对什么是 JSValue 不熟悉的话,可以google下。这样一来,由JS提供的动态特性就能实现对任意native的数据源做动态计算的能力。

once 绑定模式

这里先上一张执行数据绑定的流程图。

iOS混合开发库(GICXMLLayout)六、数据绑定原理

这张流程图显示的是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;
            }];
        }];
    }
}
复制代码

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

离心力:互联网历史与数字化未来

离心力:互联网历史与数字化未来

[英] 乔尼·赖安(Johnny Ryan) / 段铁铮 / 译言·东西文库/电子工业出版社 / 2018-2-1 / 68.00元

★一部详实、严谨的互联网史著作; ★哈佛、斯坦福等高校学生必读书目; ★《互联网的未来》作者乔纳森·L. 齐特雷恩,《独立报》《爱尔兰时报》等知名作者和国外媒体联合推荐。 【内容简介】 虽然互联网从诞生至今,不过是五六十年,但我们已然有必要整理其丰富的历史。未来的数字世界不仅取决于我 们的设想,也取决于它的发展历程,以及互联网伟大先驱们的理想和信念。 本书作者乔尼· ......一起来看看 《离心力:互联网历史与数字化未来》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

MD5 加密
MD5 加密

MD5 加密工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具