由于小程序没有使用“组件类”的构造方式,无法使用继承,因此,小程序框架提供了“混入”(mixins)方式的复用机制,“混入”主要通过 Behavior 来实现。
根据官方的定义:
每个 behavior 可以包含一组属性、数据、生命周期函数和方法。组件引用它时,它的属性、数据和方法会被合并到组件中,生命周期函数也会在对应时机被调用。每个组件可以引用多个 behavior ,behavior 也可以引用其它 behavior 。
Behavior 的构成
Behavior 能够包含的属性是预定义的,是 Component 配置项的一个子集,且无法自行扩展,比如,无法在 Behavior 中指定 options 或者 externalClasses 属性。
而且,Behavior 无法自定义名称,只能由框架自主生成。不能像官方组件那样定义 wx://form-field、wx://form-field-button 之类的语义化名称。
Behavior 的有效属性根据用途可以分三类:
第一类;数据属性;
properties
data
relations
methods
第二类:生命周期属性
observers
created
attached
ready
moved
detached
lifetimes
pageLifetimes
第三类:扩展属性
behaviors
definitionFilter
不同的属性对应不同的混入规则。
其中只有 behaviors 是一个数组,可以把 behaviors 这个类比 Js 中的原型链(Prototype Chain),definitionFilter 类比 js 中的构造函数。
Behavior 的混入规则
由于 Behavior 支持嵌套,因此两种组合引用方式:
- 组件直接引用 behaviors;
- 被引用的 Behavior 自身也引用 behaviors。
这两类在其实没什么本质区别,因此,可以统一为 引用者 与 被引用者 之间的规则。
一个示例组件的配置如下:
1 | ComponentA |
在上面的结构中,存在两组关系:
- ComponentA 是引用者,[BehaviorA, BehaviorB, BehaviorC] 这个数组整体(序列)是被引用者;
- BehaviorB 是引用者,[BehaviorB1, BehaviorB2] 这个数组整体(序列)是被引用者。
由于被引用者 behaviors 是个序列,对于不同的属性,对应的操作顺序不一样,因此,定义了一个引用优先级。优先级的基本原则是:
- “引用者”的优先级高于“被引用者“
- “被引用者”中越靠后的引用,优先级越高。
我们根据这个原则,将上述示例组件按照优先级从低到高排个序:
1 | BehaviorA < BehaviorB < BehaviorC < ComponentA |
对象合并
对象的合并遵循基本的对象合并规则:
- 如果“合并对象”本身有这个属性,则忽略“被合并对象”的同名属性;
- 如果“合并对象”本身无这个属性,则将“被合并对象”的属性(包括属性值)拷贝至“合并对象”;
假如对象 A 要合并对象 B,合并结果等同于:
1 | objA = Object.assign({},objB, objA); |
补充说明 :
- 这里的“对象类型”不够清晰,毕竟在 js 中,Array 也是一个对象,准确一点的表述应该是“映射/散列类型”,文档中叫“ObjectMap”。
- 属性拷贝不是“深拷贝”。
合并数据属性
根据属性的不同,会有不同的合并方式,合并原则:优先级高的数据属性覆盖优先级低的数据属性。(相当于继承体系里面子类屏蔽了父类属性)
data 属性
官方框架对 data 进行了一个特别的处理,并不是简单合并 Behavior 的 data,而是合并每一个 “data 的属性”。
比如:
1 | const behavior2 = Behavior({ |
这两个合并之后,data 为:
1 | data = { |
即先检测 data 的每一个直接属性的值类型,根据值类型的不同,采取不同的合并方式。对于某个属性 key:
- 如果“高优先级引用”与“低优先级引用”的值类型的都是“对象类型”,则根据“对象合并”规则合并这个属性值。
- 其他情况下,直接用“高优先级引用”的值覆盖“低优先级引用”的值。
一个示例:
1 | [{key:{name1:'val1'}},{key:{name2:'val2'}}] // 合并结果是 {key:{name1:'val1', {name2:'val2'}} |
代码描述如下:
1 | const items = [BehaviorA , BehaviorB1 , BehaviorB2 , BehaviorB , BehaviorC , ComponentA]; |
properties 与 methods 属性
遵循“合并数据属性”的方式。
合并 properties 代码描述如下:
1 | const items = [BehaviorA , BehaviorB1 , BehaviorB2 , BehaviorB , BehaviorC , ComponentA]; |
同理,合并 methods 代码描述如下:
1 | const items = [BehaviorA , BehaviorB1 , BehaviorB2 , BehaviorB , BehaviorC , ComponentA]; |
如果我们把 properties 和 methods 看作是 data 的两个特殊属性,就更容易理解:
1 | { |
此时可以发现,properties 是 data 的一个“对象类型”属性,methods 也是 data 的一个“对象类型”属性,最终使用的都是 data 所描述的规则。
生命周期函数和 observers
与数据属性不同,生命周期方法不会相互覆盖,而是在对应触发时机被逐个调用(相当于继承体系里面先调用父类方法)。官方文档特别指出:如果同一个 behavior 被一个组件多次引用,它定义的生命周期函数和 observers 不会重复执行。
调用原则:先调用优先级低的生命周期方法,后调用优先级高的生命周期方法。
代码描述如下:
1 | const items = [BehaviorA , BehaviorB1 , BehaviorB2 , BehaviorB , BehaviorC , ComponentA]; |
可以把生命周期函数看作是对生命周期事件进行监听的 observers,因此可以与 observers 一起理解,这一段属于个人补充,不属于官方文档的内容。
relations 和 definitionFilter
官方文档中没有提到 relations 和 definitionFilter。
其中 definitionFilter 像是构造函数,因此单独定义了,并且,definitionFilter 的机制有些麻烦,可以单独再讨论。
至于 relations,根据验证,规则与 properties 一致。
另一视角
对于数据属性,可以理解为执行一个查找操作;对于生命周期方法,可以理解为执行一个遍历操作。
查找数据属性
查找“引用者”数据属性的规则是:
- 如果在 “引用者” 自身找到此属性,则结束查找,返回此属性值;
- 如果在 “引用者” 没有找到此属性,则在“被引用者”查找此属性,返回查找结果。
查找“被引用者”数据属性的规则是:
- 根据 behaviors 的数组顺序,从右往左 执行查找。
- 此时每个 Behavior 都是一个“引用者”,执行“引用者”数据属性的查找规则。
对于上面的示例组件结构,数据属性的查找顺序是:
ComponentA -> BehaviorC -> BehaviorB -> BehaviorB2 -> BehaviorB1 -> BehaviorA
遍历生命周期方法
遍历“引用者”生命周期方法的规则:
- 遍历“被引用者”自身的生命周期方法;
- 执行“引用者”的生命周期方法。
遍历“被引用者”生命周期方法的规则是:
- 根据 behaviors 的数组顺序,从左往右 遍历;
- 此时每个 Behavior 都是一个“引用者”,执行“引用者”生命周期方法的遍历规则。
对于上面的示例组件结构,生命周期方法的遍历顺序是:
BehaviorA -> BehaviorB1 -> BehaviorB2 -> BehaviorB -> BehaviorC -> ComponentA
总结
查找的时候先己后人,从右到左;遍历的时候先人后己,从左到右,其实这两条规则遵循的原则是一致的:越晚定义的数据,优先级越高。