JS界面框架的思考——控件的继承、组合和可见性

  JS框架的另一个重要任务,就是控件的访问控制,即通过怎样的方式访问一个控件,如何保证不丢失引用,以及如何确保控件可见性的合理和安全。

  控件的继承是一个额外的问题,因为控件公共的属性和方法太多是需要使用继承特性的一个重要原因,而另一个重要原因则是为了使用多态性带来的便利。JavaScript的语法决定了,在JavaScript中实现多重继承是可行的,多重继承写法可以有意识地写成类似接口继承的形式,避免混淆的对象引用。

  例如,一个Form控件,要如何控制允许出现在其中的控件类型呢?比较合理的做法就是,将文本框、单选框、提交按钮等抽象FormItem的基类即可。某种控件里,应该限定允许存在的控件类型。

  一般情况下理想情况是,组合关系不可以形成环,但是有时形成环是无法避免的,这时候框架必须小心避免实际控件的循环包含关系,否则将导致无穷递归包含的现象发生。因此,要由框架来限制环的形成(毕竟无穷递归的广义表只是理论上研究的数据结构)。

  站在安全可控的角度,我们不应该让使用者用new关键字来建立一个控件,使用new建立的控件意味着框架无法得知控件的引用情况,这对于面向对象的界面控制是一种灾难,因为既无法保证安全的可见性,也无法保证不发生引用丢失。不安全的可见性就意味着,框架将无法感知控件的更改,而丢失引用意味着一些DOM没有逻辑控件对象可以操控。

  因此,我们往往通过父对象的方法来创建一个子对象,这个子对象的可见性自然处于父对象之中,由框架感知、掌控其所有操作和引用计数。这时候程序员的设计方法就变成,先创建父对象,然后向其中添加子对象,与使用new关键字方式创建控件的常见实现正好相反。

  不过这自然会导致代码复用性的问题,因为基于这种方式就意味着无法直接使用已经创建好的控件。对于这个问题的解决方案是,重用用于创建这个控件的选项参数,而不是重用这个控件本身。

  站在消息传递的角度,控件产生的消息只能由其创建者捕获,父控件捕获消息后,自行处理或将其转发到合适的子控件的处理队列中。这种消息传递机制非常严格,类似于树形网络的消息传递,是非常严谨可靠的,但是不可避免对于开发效率有一定的影响。这种写法是有长远的好处的,因为如果程序员无法明确控件的父子关系,那么很有可能无法预料异常结果的发生。

JS界面框架的思考——控件逻辑结构与DOM的控制

  纯JS实现的Web界面框架,被认为是一种基于组件的管理方式,可以认为是在浏览器上近似实现桌面应用GUI的方式。Web组件的封装与桌面控件的实现不同,JS框架需要通过封装来做的,就是管理与其逻辑结构相关联的DOM元素;而这两者在思想上却具有相似的地方(这是一定的,因为本质上这是一种主动的模仿)。

  因此有以下几个问题需要关心:

  1. 封装必然导致的问题就是灵活性受限,提供的API不可能是完备的。用户如果跨越框架直接操纵DOM,将是非常不可靠的,框架的更新或者仅仅简单的界面风格切换,都可能导致用户的越级操作失效。因此,接口提供用户需要的尽可能多的功能,是非常必要的。
  2. 对于控件的操作,应该总是从逻辑对象入手,由框架操作DOM对象,就像Windows编程时程序员会得到控件句柄,而控件句柄永远是指向了该控件的逻辑结构所在的空间,而不是二维层缓冲区。因此,一个逻辑控件的构造与析构,与实际控件是否在缓冲区或被释放,是没有直接的联系的(对JS来说,就是与DOM对象的存在与否没有关系)。
  3. 理论上良好的实现是需要时才产生DOM,而不是在构造时就产生DOM,尽管由于浏览器的优化行为,构造一个不用显示的DOM,计算开销是比较小的,然而还是由于浏览器的优化行为(空间换时间),DOM往往必须占据不少内存空间,远大于控件逻辑对象。实际上程序员是比较习惯手动控制DOM的创建和移除,因此许多框架中程序员都需要明确向框架发出构造DOM的指示,这就要求程序员对与何时构造DOM有一个基本的考虑。
  4. 意图不明确的封装是糟糕的。如果在控件的逻辑结构上需要再进行一次封装,那么最好有明确的理由和完整的架构考虑,就像在Windows的C库上封装的MFC,甚至虚拟机。当然,使用MFC的程序员,往往不再同时直接接触Windows API,因为那样是被视为混乱的程序结构。然而在架构不明确的框架中,使用不同层次的框架往往是难免的,这样将同样产生不可靠的操作。另外,除了造成内聚性差以外,层次不明确导致开发人员也无法知道,使用哪一层的操作才是最优的,往往可能导致不同的实现,代码可读性也变差。
  5. 框架要提供必要的文档,帮助开发人员了解,对逻辑控件的操作,会如何影响用户交互。
  6. 不能够直接操纵逻辑对象的数据成员,因为直接操作数据成员一般都是不好的行为,而应该总是使用方法来进行调用,尽管JS语法上并没有可见性修饰符,但是从封装性的角度而言是不希望用户这样操作的。因此,对于可以修改的数据成员,总是应该提供修改/获取它们的值的方法。