Reactive
无论构建什么类型的表单引擎,我们需要的基础能力往往类似, 如数据合规性的校验能力,条件分支的解析能力等。
Reactive 对构建表单引擎所需的各种能力进行了聚合,提供了多样的扩展方式, 来帮助开发者高效率地构建引擎。简单的说,Reactive 是一个可被扩展的引擎核心。
本章着重介绍 Reactive 的重要概念、原理,以及核心实现, 建议想要深入理解 Reacitve 或有基于 Reactive 深度定制需求的读者阅读。
实例化过程
Reactive 对各种引擎能力的组合发生在实例化过程中, Reactive 实例本质上是组合了其它模型的 Context 实例。 为了让读者能清楚地了解这个过程,我们在下面的流程图中,以伪代码展示了各个节点的核心实现。

关于成员
Reactive 在实例化后会形成一个上下文环境,在这个环境中可以存在不同类型的成员。 所谓“不同类型的成员”指的是上下文环境中属性和行为存在差异的基本单元。 Reactive 为每个成员开辟独立的存储空间以存储其状态,成员状态包括属性设定值、属性当前值,以及错误信息。
属性的设定值和当前值在无成员间关联时是相同的,但在有成员间关联时是不同的。 比如,memberB
的 age
由 memberA
的 age * 3
动态计算得到, 这种情况下,memberB
的 age
设定值,存储的是描述依赖关系和计算逻辑的表达式 {"multiple": ["<< memberA.age >>", 3]}
, 而当前值存储的则是,根据 memberA
的 age
变化而动态计算得到的结果。
为了处理成员间的依赖追踪和属性联动,以及其它必要的干预,Reactive 在实例化时“注入了必备钩子函数”, 这些钩子函数起到的作用包括:
预设并冻结成员 type
当成员被创建后,优先设定其 type
属性,如果创建时,未定义成员 type
,默认为 default
。 成员的 type
是区分成员类型的标识,被设定后,不接受更新,且不接受被设定为 DSL。
预设并校验成员 name
当成员被创建后,优先设定其 name
属性, 如果创建时,未定义成员 name
,默认为其 id
, 当 name
被更新时,执行同辈成员间 name
唯一性校验。 成员的 name
在 DSL 插值符引用路径中是同辈成员间的唯一性标识,不接受被设定为 DSL。
解析成员属性 DSL
根据成员属性的设定值是否包含 DSL,自动建立/解除成员间的关联关系,动态解析 DSL 并更新该属性的当前值。
阻止创建未知 type 成员
当创建成员时,如果成员的类型未注册,抛出异常。
阻止删除被依赖的成员
当删除成员时候,如果该成员正在被引用,抛出异常。
以上这些由“必备钩子函数”产生的作用是 Reactive 正确处理成员状态变更的前提, 除此之外,我们还可以通过注入自定义的钩子函数,干预状态变更,以实现定制。 接下来,我们将详细介绍成员状态变更过程以及如何注入钩子函数。
成员状态变更过程
由于关联关系的存在,当有成员被创建、销毁或更新时, 势必引起其它成员的状态变更,而这些状态的变更又可能引发新的变更。 为了高效处理这种连锁反应,Reactive 对成员状态的变更是批量的、异步的。 当产生状态变更时,所有关联成员的状态会按最优顺序在同一轮中变更,本轮引发的新变更将排队, 在下一轮中变更。具体过程如下图。

为了加深对状态变更异步性的理解,我们来看下面的示例。
钩子函数与状态变更
在每一轮状态变更中,Job Runner 依 mutation 类型不同, 按照 del
、add
、set
、calc
的顺序执行变更任务。
del
: 删除成员,产生该类型 mutation 的方法包括reactive#delete()
等。add
: 创建成员, 产生该类型 mutation 的方法包括reactive#add()
、reactive#seed()
等。set
: 更新属性设定值,产生该类型 mutation 的方法包括reactive#set()
、reactive#seed()
等。calc
: 更新属性当前值,该类型 mutation 在每次完成set
mutation 后产生, 如果对该类型 mutation 的处理过程不做任何干预,默认会将属性的当前值及当前值错误信息指向其设定值及设定值错误信息, 也就是说默认情况下属性当前值及当前值错误信息与设定值及设定值错误信息相同。
钩子函数可以从不同的节点注入,从而干预 mutation 的处理过程。 Job Runner 提供的钩子节点有四组: before/after-del
、before/after-add
、before/after-set
,以及 before/after-calc
。 下面我们来详细介绍。
before/after-del
当成员被删除前,注入到 before-del
节点的钩子函数,按照注入顺序被调用, 传入一个 mutation object, mutation.id
为目标成员唯一标识。 如任一钩子函数返回值为 false
,则针对目标成员的删除被终止。
当成员被删除后,注入到 after-del
节点的钩子函数,按照注入顺序被调用, 传入一个 mutation object, mutation.id
为已删除成员唯一标识,mutation.parentId
为其父成员唯一标识。
before/after-add
当成员被创建前,注入到 before-add
节点的钩子函数,按照注入顺序被调用, 传入一个 mutation object,mutation.id
为待创建成员唯一标识, mutation.parentId
为待创建成员父级成员唯一标识, mutation.props
为待创建成员属性设定值构成的 object,调用期间此 object 可被修改。 如任一钩子函数返回值为 false
,则成员的创建被终止。
当成员被创建后且属性设定值被赋值前,注入到 after-add
节点的钩子函数,按照注入顺序被调用, 传入一个 mutation object,mutation.id
为该成员唯一标识, mutation.parentId
为该成员父级成员唯一标识, mutation.props
为该成员属性设定值构成的 object。
before/after-set
当成员属性设定值被赋值前,注入到 before-set
节点的钩子函数,按照注入顺序被调用, 传入一个 mutation object,mutation.id
为目标成员唯一标识, mutation.prop
为目标成员待设定属性名,mutation.value
为目标成员待设定属性值, 调用期间 mutation.value
可被修改。 如任一钩子函数返回值为 false
,则成员属性设定值的赋值被终止。
当成员属性设定值被赋值后且当前值被赋值前,注入到 after-set
节点的钩子函数,按照注入顺序被调用, 传入一个 mutation object,mutation.id
为成员唯一标识, mutation.prop
为该成员被赋值的属性名,mutation.value
为该成员被赋值属性的设定值。
before/after-calc
当成员属性当前值被赋值前,注入到 before-calc
节点的钩子函数,按照注入顺序被调用, 传入一个 mutation object,mutation.id
为目标成员唯一标识, mutation.prop
为目标成员待更新属性名。 如任一钩子函数返回值为 false
,则成员属性当前值的赋值被终止。
当成员属性当前值被赋值后,注入到 after-calc
节点的钩子函数,按照注入顺序被调用, 传入一个 mutation object,mutation.id
为成员唯一标识, mutation.prop
为该成员基于默认逻辑赋值的属性名。 成员属性当前值及当前值错误的默认赋值逻辑为传址引用对应属性的设定值及设定值错误,调用期间可覆盖默认赋值逻辑。
上面的几个示例比较简单地展示了各个节点钩子函数的作用和用法, 当与其它模型一起使用时,钩子函数可以完成复杂的功能, 下面我们来注册一个自定义类型的成员,并利用钩子函数来控制该类型成员的属性和行为。
注册成员类型
在前文中我们提到过 Reactive 只接受创建已注册过类型的成员, 要注册一个新类型,可以使用 reactive#install()
方法, 该方法接收钩子函数的定义,且这些钩子函数只作用于匹配该类型的成员。
在上面的示例中,我们为新类型添加了 label
和 value
必须为字符串的校验逻辑, 但是这两个校验的大部分代码都是相同的,这样重复冗余的代码显然是不可接受的。在下面的示例中,我们进行了重构。
可以看到,在注册新类型时除了定义了 hooks
,我们还定义了 schema
, 并且在钩子函数中调用 reactive#schema.for()
查询到了 schema
中 label
和 value
的 dataType
, 这样一来,就可以动态地匹配到不同属性的校验逻辑,而不必编写重复的代码了。
通过上例我们不难发现,钩子函数是控制成员属性和行为的具体实现, 而 schema
是对实现逻辑的一种高度抽象。没有具体实现做支撑的 schema
是起不到任何作用的。
事实上,Reactive 提供了一些可复用的钩子函数, 上例中的校验逻辑我们并不需要自行编写,可直接使用 ENABLE_SCHEMA_VALIDATION
钩子函数配合 schema
定义实现, 我们在下面的示例中将逐一展示这些内置钩子函数的作用和用法。
校验属性值
Reactive 提供的 ENABLE_SCHEMA_VALIDATION
钩子函数提供了校验成员属性值的能力。
如果在成员属性 schema
中定义了 validation
,每当属性的当前值变更后,会按照 validation
规则对该值进行校验,成员的错误信息将根据校验结果被赋值为 null
或一个包含 VALIDATOR_VALIDATION_FAILED
类型错误的数组。
如果在成员属性 schema
中定义了 rawValidation
,每当属性的设定值变更后,会按照 rawValidation
规则对该值进行校验,成员的错误信息将根据校验结果被赋值为 null
或一个包含 VALIDATOR_VALIDATION_FAILED_RAW
类型错误的数组。
校验规则可以被定义为 null
,或一个数组,数组中可包含受支持的 DSL 校验表达式,或返回错误信息的函数。 校验规则的执行顺序遵从 validator#sort( ) 方法的排序结果。
冻结设定值
Reactive 提供的 ENABLE_SCHEMA_ALWAYS
钩子函数提供了冻结成员属性设定值的能力。 如果在成员属性 schema
中定义了 always
,成员属性的设定值将始终等于 always
的值 。
使用默认设定值
Reactive 提供的 ENABLE_SCHEMA_DEFAULT
钩子函数提供了使用默认属性设定值的能力。 如果在成员属性 schema
中定义了 default
,那么在创建成员时,如果成员属性设定值未定义, 则使用 default
相同的值来作为其设定值。
阻止添加到不合规父级
Reactive 提供的 BLOCK_INVALID_PARENT
钩子函数提供了限制父级成员类型的能力, 子成员只能添加到特定类型范围内的父级成员。
到目前为止,我们已经接触到两种注入钩子函数的方式,在调用 reactive#install()
时注入, 和直接使用 reactive#hooks.mount()
注入。通常情况下,建议使用后者注入全局钩子函数, 使用前者注入针对特定成员类型的钩子函数。 当然,reactive#hooks#mount()
本身也支持向特定类型注入钩子函数, 只是过多的使用可能导致钩子函数的管理混乱,不建议过度使用。
DSL
在前文中,我们提过 Reactive 的 DSL 语句, 简单地说,DSL 是针对特定领域、高度抽象的专用语言,比如 SQL 就是针对数据库设计的语言, 专用于高效地编写数据库读写逻辑,但不能像通用型编程语言那样构建种类繁多的应用。 Reacitve DSL 是专为表单领域设计的,用来描述表单模型的语言。 基于标准的 js plain object,语句构成和使用方式简单易懂,我们来看下面的例子。
通过上面的例子可以很直观地看到,Reactive DSL 语言构件由四部分构成:条件语句、校验表达式、计算表达式以及引用插值符。 我们在介绍“必备钩子函数”时提到过,DSL 的解析过程借助了钩子函数来实现, 但本质上, Reactive DSL 语言构件的能力是由不同的模型做支撑的,我们可以通过扩展这些模型,实现 DSL 能力和语意范围的扩充。 下面我们将一起实践,扩展 Reactive DSL。
解析插值符
Reactive DSL 引用插值符 << >>
的解析过程是,将插值符内的路径以 .
号分割, 最后一位是被引用的属性名,前几位是被引用成员的 name
, 按照 name
查找成员并使用 Dependency 模型维护引用者和被引用者的依赖关系。 如果被引用的成员在查找过程中不存在,则会停止查找,并向引用者的存储空间写入错误信息, 如果引用者和被引用者产生循环引用,则不会添加依赖关系,并向引用者的存储空间写入错误信息。
举例来说,<< parentName.childName.childProp >>
的解析过程是, 从根成员的子级开始,依次向下查找 name
为 parentName
的成员, 和 name
为 childName
的成员,并添加两个成员的 name
属性和引用者间的依赖关系, 以及 name
为 childName
的成员的 childProp
属性和引用者间的依赖关系。
在解析过程中,成员的查找方式可以针对成员类型的不同进行修改, 基本原理是当查找到某个成员时,对其后代成员的查找过程进行拦截并返回修改后的查找结果。 下例中,我们注册了一个自定义成员类型,并将查找其子成员的方式由默认的按 name
查找, 修改为按照数组下标进行查找。
需要注意的是, 如果需要修改成员的查找方式,那么 schema.reference.parse
和 schema.reference.stringify
大多需要同步定义。 因为与 JSON.parse
和 JSON.stringify
类似,这两个方法互为镜像,一个方法的输入是另一个方法的输出, 如果不同步定义,可能导致成员属性引用路径的拆分逻辑和拼接逻辑不匹配。
注册校验方法
Reactive DSL 中的校验表达式是基于 Validator 模型的能力实现解析的, 扩展 reactive#validator
中的校验方法,将同步扩展 DSL 校验表达式的语义范围。 我们来看下面的示例。
在 Reactive DSL 解析过程中,当校验表达式合规且定义的校验方法没有返回错误消息时, 表达式求值为 true
,反之为 false
。所以,任何被注册的校验方法都可以在 DSL 中充当布尔运算的条件, 从而实现了 DSL 语意范围的扩充。
另外需要提及的是,校验方法注册后,前文提到的 ENABLE_SCHEMA_VALIDATION
钩子函数支持的 validation
及 rawValidation
可应用的表达式范围也会被同步扩充。
为了方便使用,Reactive 在实例化过程中内置了一部分校验方法,可以直接使用。
dataType
,校验数据类型是否合规,如{dataType: ['String', 'Number']}
。equalTo
,校验数据是否等于某值,支持对象和数组,如{equalTo: [1, {val: 2}]}
。isInteger
,校验数据是否是整数,如{isInteger: true}
。maxLength
,校验字符串和数组最大长度,如{maxLength: 10}
。presence
,校验数据是否为空,如{presence: true}
。withFormat
,校验字符串是否满足格式,如{withFormat: '/^abc$/'}
。
注册计算方法
Reactive DSL 中的计算表达式是基于 Calculator 模型的能力实现解析的, 扩展 reactive#calculator
中的计算方法,将同步扩展 DSL 计算表达式的语义范围。请看下面的示例。
在 Reactive DSL 解析过程中,当计算表达式合规时,求值结果为定义的计算方法的返回值,否则为 null
。 成员的属性值可以直接被设定为计算表达式,也可以嵌套进条件语句进行设定,所以,注册计算表达式也可以实现 DSL 语意范围的扩充。
为了方便使用,Reactive 在实例化过程中内置了一部分计算方法,可以直接使用。
add
,数字加法,如{add: [1, 2, 3]}
。devide
,数字除法,如{devide: [8, 2, 2]}
。map
,映射任意类型数据,如{map: {val: true}}
。minus
,数字减法,如{minus: [10, 1]}
。multiple
,数字乘法,如{multiple: [4, 5, 6]}
。
小结
- Reactive 是一个可被扩展的引擎核心。
- Reactive 实例是组合了其它模型的 Context 实例。
- 成员状态包括属性设定值、属性当前值,以及错误信息。
- 属性的设定值和当前值在无成员间关联时是相同的,但在有成员间关联时是不同的。
- 成员的
type
是区分成员类型的标识,被设定后,不接受更新,且不接受被设定为 DSL。 - 成员的
name
在 DSL 插值符引用路径中是同辈成员间的唯一性标识,不接受被设定为 DSL。 - Reactive 对成员状态的变更是批量的、异步的。
- Job Runner 依 mutation 类型不同,按照
del
、add
、set
、calc
的顺序执行变更任务。 - Job Runner 提供的钩子节点有四组:
before/after-del
、before/after-add
、before/after-set
,以及before/after-calc
。 - 钩子函数是控制成员属性和行为的具体实现,而
schema
是对实现逻辑的一种高度抽象。 - Reacitve DSL 是专为表单领域设计的,用来描述表单模型的语言。
- Reactive DSL 语言构件由四部分构成:条件语句、校验表达式、计算表达式以及引用插值符。
- 如果需要修改成员的查找方式,那么
schema.reference.parse
和schema.reference.stringify
大多需要同步定义。 - 在 Reactive DSL 解析过程中,当校验表达式合规且定义的校验方法没有返回错误消息时,表达式求值为
true
,反之为false
- 在 Reactive DSL 解析过程中,当计算表达式合规时,求值结果为定义的计算方法的返回值,否则为
null
。
通过阅读本章内容,我们较深入地了解了 Reactive 的核心实现原理, 关于 Reactive 的更多内容,请阅读 Reactive 接口文档。 在下一章,我们将一起使用 Reactive 从头 构建一个 Form 引擎, 将本章的原理应用到实践中。