虽然是基于 react 的框架,可是在 nautil 中可使用双向数据绑定,这得益于基于观察者模式的开发思路。在 react 中使用双向绑定并不是没有需求,react 严格的单向数据流,严重影响了开发者的发挥空间,特别是在表单组件的使用中,很容易陷入回调地狱,即便 redux 也没法避免。vue
咱们都知道,react 是单向数据流的,数据只能从外部经过 props 传入,再经过 props 上面传入的回调函数再传出去,直接修改 props 或者上面的对象,不会带来界面的更新,并且会致使数据不可预期。react
基于这种单向数据流的 flux 思想,redux 还遵循了函数式编程的规范,保证了数据的干净。同时,它提供了自顶向下的分发机制,修改 redux store 中的数据,会触发全部connected 的组件。而触发过程是,调用 connected 组件 props.dispatch 方法。git
虽然单向数据流的方式保证了数据流干净,但 redux 的编程方式太复杂了。它不只增长了数据构造自己的逻辑代码,并且 action 代码也是分散的,当你须要进行修改时,有的时候会在好几个文件之间转晕。虽然有不少优化 redux 样板代码的库,但受限于它的编程思想,仍然很差在项目中节省更多时间。github
出于节省更多时间成本的目的,我在开发 nautil 中没有使用 flux 那一套,而是另辟蹊径,作了很像 mobx 但又更简单的事。vuex
咱们来看一下如何在 nautil 中建立一个 store:编程
import { Store } from 'nautil' const store = new Store({ some: 123, })
这样咱们就建立了一个 store,很是简单,只传入了默认值。而没有各类 reducer 的样板代码。redux
Store 实例是一个可观察的对象,经过 watch 方法,能够监听 store 中数据的变化。但凡能监听到数据变化,咱们就能够在数据变化时,更新界面渲染。因此,在 nautil 中,观察者模式是核心思想,是实现 nautil 中各类响应式效果的前提条件。框架
若是你用过 vue 的话,你必定喜欢 vue 中操做数据的方式。在 vue 中要将输出框组件和数据绑定很是容易:dom
<input type="text" v-model="name" />
当用户在输入框中输入内容时,this.name 也会随之变化。而因为 vue 的响应式是自主绑定的,this.name 发生变化的同时,也会触发 vue 内部对整个组件的从新渲染机制。这种将数据映射到视图,再由视图从新映射会数据的编程方式,在 angular 1.x 中随处可见。函数式编程
在 angular 中,经过 ng-click 等事件绑定,或者控制器中调用 $http
实现数据请求,在响应结束的时候,都会自动触发 angular 内部的 digest,并经过脏检查机制,从顶至底的去完成界面从新渲染,因为脏检查的特质,根本不须要 react 那种要求数据是 immutable 的,即便原始数据被修改,新的界面也会被按照新的数据进行渲染。
我并非说 angular 这种直接修改数据的方式更好,但起码,在面对开发者时,它更直接,更容易理解,更符合编程习惯。
从某些角度讲,vue 是很容易让人费解的。在 vue 的组件里,须要在组件内内置不少状态来控制,这里的状态指经过 data() 绑定到 this 上的各类响应式属性。在组件内部,修改 this.name 能够触发组件的从新渲染。可是,奇怪的是,vue 不能经过这种方式修改 props 中传入的数据。
这一点很让人费解,对比 react,react 虽然支持组件内 state,可是比较强调组件的可控性,经过 props 来彻底掌控 UI 界面的展现,也就是一个状态对应一个 UI 界面。所以,react 提供了函数式组件,这种组件没有本身的 state,这种组件最符合 react 主流思想的口味,并且,整个 react 编程也一以贯之,遵循这种 props 控制一切的理念。
可是,vue 明显更强调 this 上面属性的响应式特性。却又不提供 props 反写的能力,让人百思不解。另外一个让人百思不解的是,既然 vue 推崇它的属性响应式特色,为什么 vuex 却要像 redux 那样编程?甚至还要分 state, mutaion, action 三种东西,却不继续发挥属性更新形式的响应式编程特色。
Nautil 在这条路上一走到底,将响应式编程发挥到极致。
简单的讲,“双向绑定”是要作到组件内和组件外数据的双向修改,外部修改数据时,组件内部即时响应变化,组件内部修改数据时,外部整个应用的对应部分也随即发生更新。这一点在 angular 1.x 中已经实现了,为什么新的框架反而不实现呢?
所以,我要在 nautil 实现的双向绑定方案,更加完全,更符合开发者想要的方式。
可是,如何在 react 里面实现双向绑定呢?
vue 的 v-model 给了我启示。咱们去看 v-model 指令,实质上,它是一个将 v-bind 和 v-on 动做简化的语法糖。
<input type="text" :value="name" @input="name = $event.target.value" />
一个双向绑定的语法,其实是一个数据绑定和一个事件响应的结合体。不过 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 值发生变化。
createTwoWayBinding
该函数用于基于传入的对象,建立一个用于双向绑定的对象。它的传入参数是任意的,可是我推荐使用 store 或 model 的 state,这样就不用本身构造第二个参数。
可是,若是你想让一个普通对象也能够实现响应式,你能够利用第二个参数:
const { state } = this // react 的 state 本质上是一个普通对象 const $state = createTwoWayBinding(state, ([state, keyPath, value], [target, key]) => { this.setState({ [key]: value }) }) <Input $value={$state.name} />
目的上,createTwoWayBinding 最终是为双向绑定服务的,因此不该该用它所建立的对象去读取值。
本文主要介绍了为何要在 Nautil 中实现双向绑定,怎么实现,以及如何使用的问题。虽然本文主要是介绍 Nautil 中的双向数据绑定,可是也讨论了 react, vue, angular 的一些数据状态管理的东西,若是你对这些问题也有本身的想法,不妨在下方给我留言一块儿讨论。