考不上三本也会实现数据绑定(二)

考不上三本也会实现数据绑定(二)

你现在所阅读的并不是第一篇文章,你可能想看目录和前言
我想了想还是拆开写。今天只讲一种简单的表达式的形状,也就是XAML用的属性链。属性链指的就是a.b.c这样的表达式,同时你也可以带有下标,但是下标也只能是属性链。不过在大多数的实现里面,像a[b.c]这样的表达式,只会监视b.c有没有变化,而不会在b.c固定的情况下监视a[x]有没有变化,所以实际上跟只没有下标处理起来是差不多的。

例子

让我们来看这样的一个例子,我们要把语句:

attach(bind(a.b.c.d).ValueChanged, callback);

变成一个触发callback的事件,从而你可以监听他的变化,那到底要处理多少东西呢?如果我们假设每一个属性都有对应的变化事件,那么其实就等于监听

  • a.bChanged
  • a.b.cChanged
  • a.b.c.dChanged

这几个事件。同时我们也做出下面的假设:

  • a不会变
  • 多次读取一个属性只会返回相同的值,也就是这个表达式的副作用将被忽略

而且a.b变化了,那么原先我们监听的a.b.cChanged就没有用了,我们要解绑它,重新监听一个新的。通常来讲,解绑一个事件,要么是通过直接操作监听事件返回的cookie(如.net的IObservable),要么是直接把cookie交给被监听对象的属性来完成。如果我们取的是后者,那么我们还得把旧的a.b留下来,然后才能正确的解绑事件。你要是不这么做的话,那么我们就会一直在试图监听旧的a.b的cChanged事件,那么新的a.b.c再怎么变化你也不会接到通知。

这就意味着,如果我们监听一个对象o的属性p,那么我们就得把o给cache下来,当o变了的时候,我们才能正确的解绑事件pChanged,然后挂一个新的callback到新的o.pChanged上面去。因此对于表达式a.b.c.d,首先假设a不会变,那么我们要cache下来的就是a.b和a.b.c了。

代码生成

所以我们大概会生成这样的代码:

class MyDataBinding : Subscription
{
    var cache_a : A^ = null;
    var cache_a_b : B^ = null;
    var cache_a_b_c : C^ = null;
    var handler_a_b : EventHandler^;
    var handler_a_b_c : EventHandler^;
    var handler_a_b_c_d : EventHandler^;

    ctor(a : A)
    {
        // 给a.bChanged上绑
        cache_a = a;
        handler_a_b = attach(cache_a.bChanged, OnChanged_a_b);
        // 绑上剩下的所有事件
        Update_a_b(cache_a.b);
    }

    func Update_a_b(b : B) : void
    {
        // 如果a.b变了,那么要正确处理a.b.cChanged
        if (cache_a_b == b) return;
        // 把旧的a.b.cChanged解绑
        if (handler_a_b_c is not null)
        {
            detach(cache_a_b.cChanged, handler_a_b_c);
        }
        // 把新的a.b.cChanged上绑
        cache_a_b = b;
        handler_a_b_c = attach(cache_a_b.cChanged, OnChanged_a_b_c);
        // 既然a.b变了,那么a.b.c多半也变了
        OnChanged_a_b_c();
    }

    func Update_a_b_c(c : C) : void
    {
        // 如果a.b.c变了,那么要正确处理a.b.c.dChanged
        if (cache_a_b_c == c) return;
        // 把旧的a.b.c.dChanged解绑
        if (handler_a_b_c_d is not null)
        {
            detach(cache_a_b_c.dChanged, handler_a_b_c_d);
        }
        // 把新的a.b.c.dChanged上绑
        cache_a_b_c = c;
        handler_a_b_c_d = attach(cache_a_b_c.dChanged, OnChanged_a_b_c_d);
        // 既然a.b.c变了,那么a.b.c.d多半也变了
        OnChanged_a_b_c_d();
    }

    func OnChanged_a_b() : void
    {
        Update_a_b(cache_a.b);
    }

    func OnChanged_a_b_c() : void
    {
        Update_a_b_c(cache_a_b.c);
    }

    func OnChanged_a_b_c_d() : void
    {
        // 触发ValueChanged事件,调用callback
        ValueChanged(cache_a_b_c.d);
    }
}

然后原来的那段代码就变成了:

var myDataBinding = new MyDataBinding^(a);
attach(myDataBinding.ValueChanged, callback);

计算过程

如果bind(a.b.c.d)这样实现的话,那么其实可以看到,如果a.bChanged触发了,那么旧的a.b.cChanged和a.b.c.dChanged都会被解开,然后OnChanged系列函数就挂到了新的a.b.cChanged和a.b.c.dChanged上面去。所以不管是b、c、d这三个属性哪一个变了,ValueChanged事件最终都会触发。

上面这个代码有一个瑕疵,就是构造MyDataBinding的时候,ValueChanged会被触发一次。不过这种小事只要不影响profiling,都不用管。生成代码的算法尽量以简单为好。

生成MyDataBinding的策略很简单,因为我们涉及的属性有

  • a.b
  • a.b.c
  • a.b.c.d

这三个,因此就有以下三个步骤:

  • 对于a.b
    • cache一下a,然后挂上a.bChanged
      • 生成ctor
  • 对于a.b.c和a.b.c.d
    • cache一下a.b,然后挂上a.b.cChanged
      • 生成Update_a_b和OnChanged_a_b
    • cache一下a.b.c,然后挂上a.b.c.dChanged
      • 生成Update_a_b_c和OnChanged_a_b_c
  • 触发ValueChanged事件
    • 生成OnChanged_a_b_c_d

对于a[b.c]这样的表达式,道理也是一样的,就是分别给下标表达式生成MyDataBinding这样的类,最后把它们组合起来,这是一个递归地步骤,留给大家课后练习一下。今天就说到这里了,下一篇就会讲到处理复杂表达式的方法。

其实在这里,a.b的变化会导致a.b.cChanged重挂的事情,就是一个a.b.c对a.b的依赖关系。在复杂的表达式里面,就会有各种各样依赖关系,它们共同组成了一个偏序的关系。举个简单的例子:

let a = x.y in (a.b + a.c)

a.b和a.c就共同依赖于a,也就是x.y,因此x.yChanged的事件处理函数里面,就要把a.bChanged和a.cChanged都进行重挂。这样的依赖关系当然是不会有环的。

通常来讲,做出这样的一个关系图倒是不难,难的是当你的表达式类型特别多的时候,你做的这些类似把a.b.c替换成cache_a_b_c和cache_a_b.c这样的工作,就容易出bug。这些还得靠你多写几个测试用例来保证。

后注

UWP的{x:Bind}比起{Binding}先进的地方在于,Binding需要运行时的反射,而x:Bind是不需要的。但是XAML的编译器在处理x:Bind的时候,怎么知道a、a.b、a.b.c和a.b.c.d的类型,从而生成出正确的代码呢?这通常有两个办法:

  • 你可以想办法让XAML编译器知道他们的类型。对于x:Bind,这是通过cl、midl、mdmerge和xamlcompiler这几个程序共同实现的。其中的辛酸,只有手写idl文件的人才能明白(逃。这种方法不仅可以支持C++,也可以支持WinRT上面的任何编程语言。
  • 如果是想支持C++的话,其实你完全可以通过模板的技巧来计算出所有的这些东西,只要你使用同样的语法来在所有可以绑定的对象身上读取属性和挂事件就可以。x:Bind的灵感其实是从Office借去的(包括以前的XAML),而Office又怎么有办法修改cl呢?所以内部的工具就通过生成模板类和函数的方法来解决。
编辑于 2017-05-25

文章被以下专栏收录