雖然是基於 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 的一些數據狀態管理的東西,若是你對這些問題也有本身的想法,不妨在下方給我留言一塊兒討論。