TS/JS中实现对象属性的双向绑定Two-way Binding in Javascript
- 属性绑定
- Object.obseve 与Proxy
- 实现Bind,BindTwoWay
属性绑定
在web应用中,binding是一件常见的功能。使用MVC模式的web应用中,View层的DOM Object的属性通常需要和Model中的进行同步。在Angular/Vue/React中都有这种机制,如在Angular中使用[ngModel]
。
Property绑定有两种,One-Way Binding与Two-Way Binding,通常在实现Binding的时候都是用Event机制,由于一个Object的Property可能与其他的对象进行任意多次单向绑定或者双向绑定。使用Event机制将所有的绑定callback储存在一个ListenList中,当属性变化时再遍历调用Emitb便于扩展。如果不适用Event机制进行构建时,就需要使用闭包将原先Property的setter包裹起来,每绑定一次就需要将原先的setter方法包裹一次,会造成较大的overhead。
在编写UI库RUI.js时,UI布局LayoutEngine在布局流更新时需要处理一些特殊的布局规则,例如width=50%
。在FlexBox布局容器中我们可以使用Flex进行替代,但是在非Flex容器中,就需要属性绑定来完成。
Object.obseve() 与 Proxy
绑定的方法的定义如下
1 | Bind(tar:Object,property:String,callback:(value:any)=>void); |
其中
- tar为被监听的对象
- property为被监听的属性名称
- callback为对象属性变化后的回调
Object.obseve()
JS提供过一个Object.observe(),用于异步监听一个对象的更改,可以实现我们的需求,对应使用observe的方法如下:
1 | Object.observe(tar,function(changes){ |
但是由于这个方法已经被标记为废弃,所以不推荐使用。
Proxy
JS标准库中还提供了一个代理对象Proxy,用于处理对象的基本行为的代理。
使用Proxy处理Binding的如下:
1 | tar = new Proxy(tar,{ |
但是由于Proxy是在原有对象上创建出一个代理对象,如果我们需要对一个已有对象进行绑定的操作,我们就需要对所有该对象的引用处替换为当前的Proxy对象。这样对于一个对象有多次属性绑定或有多次引用是就不适用。
实现自定义的Bind,BindTwoWay
对于绑定,我们需要监听对象的setter方法,当对象值改变时,调用所有的callback方法。对于一个javascript object的property P。如果该对象的property没有声明该属性P的getter和setter,在Bind的方法中定义P的setter和getter,在setter中调用所有注册的binding callback。如果该property已经声明了属性P的getter和setter,需要覆盖原始P属性的getter和setter,在新的setter中调用原先的setter,同时调用所有注册的binding callback。
首先我们定义用于储存binding callback的Property
对于Object.p
,我们储存callback function在Object._bind_p
属性中。_bind_p
的类型为((v:any)=>void)[]
;
1 | function BIND_EMITTER(p:string){ |
BindSetup
方法初始化上文所说的setter和getter,每一个需要被绑定的对象,只需要初始化一次,对于该对象属性上的多次绑定,只需要在_bind_p
中添加多个callback方法。
使用Object.definePropertyWrap属性的setter。
使用Object.getOwnPropertyDescriptor获取当前Property的getter与setter。
1 | function BindSetup(tar: object, property: string) { |
对于一个Object,如果其property没有定义getter与setter,需要将原先的property访问值储存在其他property中,上面的实现储存在新的属性_bind_[property]_p
中。
Two-Way Binding
使用BindFunc
实现单向绑定后,可以很容易实现双向绑定。
1 | function BindTwoWay(property:string,tar1:object,tar2:object){ |
Full Code Github Gist