一个双向绑定的语法,实际上是一个数据绑定和一个事件响应的结合体。不过 vue 有一个优势,它是基于模板解析的,所以写法上非常有优势。而 react 如果要依靠编译的话,非常不稳定,因为不知道其他人打算怎么用。最后,我找到一种特别的语法,用来表达双向绑定这种数据传递方式。
我们先来看下一个实现的效果:
import { Component, Store } from 'nautil' import { createTwoWayBinding } from 'nautil/utils' import { initialize, pipe, observe } from 'nautil/operators' import { Section, Text, Input } from 'nautil/components' export class OneComponet extends Component { static props = { store: Store, } render() { const { store } = this.attrs const { state } = store const $state = createTwoWayBinding(state) // 创建一个可用于双向绑定的宿主对象 return ( <Section> <Text>name: {state.name}</Text> <Input $value={$state.name} /> </Section> ) } } export default pipe([ initialize('store', Store, { name: 'tomy' }), observe('store'), ])(OneComponent)
上面的代码利用了比较多的东西,例如 nautil 中的 Store 和指令。但单纯双向绑定这个点,你只需要注意 Input 组件的 $value 属性。在 nauti 中,$ 开头的属性表示双向绑定属性,它的值必须是一个特定结构,而非普通值。
从原理上将,nautil 中的双向绑定基于一个特定结构。在这个特定结构中,包含了值本身,和一个值改变时的回调函数,当组件内部的该值发生变化时,这个回调函数会被执行,更新界面的动作,在回调函数中被执行。而这个特定结构,被 createTwoWayBinding 抹平了结构在视觉上的差异。它的原始结构实际上是:
$value={[state.value, value => state.value = value]}
之所以 state.value = value 可以更新界面的渲染,是因为我们通过 observe 指令观察了 store 的变化,从而在外层就让界面可以根据 store 的变化而更新。
利用双向绑定
对于组件本身而言,如何利用双向绑定完成一些事情呢?我们来看Input 组件的源码:
export class Input extends Component { render() { const { type, placeholder, value, ...rest } = this.attrs const onChange = (e) => { const value = e.target.value this.attrs.value = value // 主要是这一句 this.onChange$.next(e) } return <input {...rest} type={type} placeholder={placeholder} value={value} onChange={onChange} onFocus={e => this.onFocus$.next(e)} onBlur={e => this.onBlur$.next(e)} onSelect={e => this.onSelect$.next(e)} className={this.className} style={this.style} /> } }
对于 Input 组件而言,中间比普通 react 组件多了一句 this.attrs.value = value,这句话利用了双向绑定特殊结构的第二个值,进行值的回传和反写。也就是说,在 nautil 中,双向绑定具有兼容性,你可以这样写:
<Input value={state.value} onChange={e => state.value = e.target.value} />
也可以这样写(标准写法):
<Input $value={[state.value, value => state.value = value]}
当然,如果你知道 nautil 里面的内置规则,甚至还可以这样写:
<Input $value={state} />
或者也可以利用前面提到的 createTwoWayBinding 函数(推荐用法):
const $state = createTwoWayBinding(state) <Input $value={$state.value} />
这样写可能更容易理解一些。
Input, Textarea 等表单组件都有双向绑定功能。但是,假如现在你自己想写一个组件,使用双向绑定功能,你需要怎么写?其实很简单,只需要直接操作 this.attrs 上的属性即可:
import { Component } from 'nautil' import { Button } from 'nautil/components' export class Some extends Component { static props = { $age: Number, } render() { return ( <Button onHint={() => this.attrs.age ++}>grow</Button> ) } }
这样的写法比较严格,要求外部传入的时候,必须传入 $age 这个属性,而不允许传入 age 属性。为了兼容,你可以学习 Input 组件的做法,在 onHint 的回调函数中,增加一个回调函数的调用。
需要注意,this.attrs.age ++ 这个语句,不会真的修改 this.attrs.age 的值,这个修改动作会被拦截,它只是在编程上顺延了 js 语法,但实际上,它的效果是调用双向绑定特定结构的第二个参数,至于 this.attrs.age 的值是否真的变化,取决于双向绑定特定结构第二个参数是否修改外部传入的 age 值发生变化。
组件内直接使用 this.attrs.age ++ 修改外部传来的属性,目标是反写外部数据
外部组件在往子组件传递双向绑定属性时,需要传入一个特定结构
createTwoWayBinding