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

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

你现在所阅读的并不是第一篇文章,你可能想看目录和前言

前言

经历了GacUI的一次超级大重构之后,终于又有空写文章了。这次重构在保留了所有功能的前提下,删掉了很多类跟接口,积累了很多魂来传火,过些日子再围绕相关的话题来谈一谈重构。今天先说一下数据绑定的事情。

MVVM

在我讲数据绑定的时候,背景是设定在 MVVM模式 上面的,在这里简单介绍一下MVVM。MVVM把我们的程序——其实大多数时候就是UI或者网页——分成了三大块,分别是Model、ViewModel和View。Model很好理解,指的是数据源,通常是数据库或者文件。View也很好理解,就是最终的UI,或者是测试用例,或者是一些别的东西。那ViewModel是什么呢?大多数时候我们也不用死抠概念。ViewModel出现的根本原因就是,Model和View的结构经常是不一样的。

举个简单的例子,Model是每门科目各一份的成绩单,View是教室门口贴的总分前10名的大红榜。显然Model和View的结构就是完全不同的。在这里Model按照科目来分,告诉你每个人的成绩是多少。View则告诉你总分前10名都是什么人。那ViewModel是啥呢?其实ViewModel在这里就是红榜的内容了。View——红,ViewModel——榜,没毛病(逃

因此ViewModel大多数时候就是用来负责实现具体的计算过程的,譬如从一堆成绩单里面算出总分前10名,这就是ViewModel要做的事情。那么针对这份ViewModel,我们就可以做一堆View,譬如说:

  • 让老师把前10名念出来
  • 写进大红榜贴在教室门口
  • 打印成文件给校长用来做发奖学金的参考

这三个View虽然长的完全不同,但是其内容都是一致的,而且的格式跟Model有巨大的区别。所以这就是ViewModel存在的原因,你需要一些复杂的计算,而且这些计算可以被重复使用,所以要独立出来,正确处理好依赖:让View去依赖ViewModel。

数据绑定

好了,那为什么会有数据绑定呢?因为到了这一步,ViewModel的格式跟View其实就差不多了,那么你把数据从ViewModel复制到View,或者从View反馈回ViewModel,理论上只需要一些简单的步骤就可以实现。UWP就假设这些步骤可以简单到你只需要给一串(通常只有一个)属性的名字就好了,复杂点的可以用Converter,再复杂就证明你的ViewModel做的不好。

GacUI的数据绑定并没有这个假设,我可以让你写无限复杂的表达式,不过同时也付出了没有双向绑定的代价——如果你需要双向绑定,那你就正反两边都绑定好了——因为GacUI的数据绑定除了联系View和ViewModel以外,还有别的事情要做。

UWP的x:bind提供了三种绑定的形式,分别是一次性绑定、单向绑定跟双向绑定。其中一次性绑定就跟写在构造函数里没有区别,而双向绑定可以理解为从一个单向绑定计算出相反的单向绑定要怎么写然后自动替你写好,因此我们就只需要考虑单向绑定要怎么做就可以了。

在这里不得不提到的是,我看到有些JavaScript的界面库在实现数据绑定的时候,采用了一些花式作死的方法,其中我印象最深刻的一种就是,他先跑一下你的表达式,看看一共用到了多少属性,然后挨个给他们挂上事件处理程序。居然存在使用采样的方式来实现的编译器,不得不佩服作者的想象力实在是太强大了,是谁这么做我已经不记得了。不过他根本不需要这么做,只要把JavaScript换成TypeScript,这个事情就可以完美的解决。

为什么呢?因为你既然要做数据绑定,那你总要知道你写的这个表达式到底需要响应多少事件——其实也就是说,到底要在ViewModel的什么部分被修改的情况下刷新View——这样才能做出完整的功能。而JavaScript这个语言在阅读的时候是没有上下文的,所以你根本没办法做这样的计算,因此才需要用采样的方法。而采样的一个缺点就是,万一你的表达式有分支怎么办?有些人可能会说,你不应该写分支。其实这个说法很对,毕竟这么复杂的逻辑应该放进ViewModel里,但是与此配套的,你应该在遇到分支的时候直接爆炸,而不是就这么默默的接受了。TypeScript就是有上下文的,所以可以清楚地把依赖关系都计算出来。

所以这个系列的前提就这么确定了,我们在MVVM的模式下,使用一个强类型的语言做数据绑定。那我们需要依靠什么三本的知识来实现数据绑定,就是接下来的文章要讲述的事情。当然说是说系列,多半(二)就会把所有的事情都说完了,毕竟数据绑定的内容很少(逃

一个例子

在文章结束之前,最后讲一下我们要如何对数据绑定这个功能进行建模。在GacUI使用的 Workflow脚本语言 里面,我给了一个bind表达式,具体的用法就是:

bind(obj.A + obj.B)

这个表达式会返回下面的接口的实例:

interface system::Subscription
{
    /* 一些无关紧要的其他函数 */

    event ValueChanged(object);
}

然后只要obj.A + obj.B这个表达式改变了——其实也就是A或者B属性的其中一个发生了变化,ValueChanged事件就会触发,参数就是这个表达式当前的值。你只要挂了这个事件,自然就可以把最新的数据显示到UI上面了。譬如说下面的这个测试用例

module test;
using test::*;
using system::*;

var s = "";

func Callback(value : object) : void
{
	s = $"$(s)[$(cast string value)]";
}

func main() : string
{
	var x = new ObservableValue^();
	var subscription = bind($"The value has been changed to $(x.Value)");
	subscription.Open();
	attach(subscription.ValueChanged, Callback);

	x.Value = 10;
	x.Value = 20;
	x.Value = 30;

	subscription.Close();
	return s;
}

最终就会返回下面这个字符串:

[The value has been changed to 10][The value has been changed to 20][The value has been changed to 30]

因为x.Value一共改变了3次。

待续

今天这篇文章就说到这里了,接下来我们会重点描述下面的三个问题

  • 如何跟踪属性变化
  • 如何分析属性之间的依赖关系
  • 如何把数据绑定重写为对回调函数的调用

这三个问题搞定了,数据绑定也就做出来了。敬请期待。

编辑于 2017-05-11

文章被以下专栏收录