Nautil 中使用雙向數據綁定的實現

這篇文章主要介紹了Nautil 中使用雙向數據綁定的實現,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨着小編來一起學習學習吧

雖然是基於 react 的框架,但是在 nautil 中可以使用雙向數據綁定,這得益於基於觀察者模式的開發思路。在 react 中使用雙向綁定並非沒有需求,react 嚴格的單向數據流,嚴重影響了開發者的發揮空間,特別是在表單組件的使用中,很容易陷入回調地獄,即使 redux 也無法避免。

現有狀態管理的問題

我們都知道,react 是單向數據流的,數據只能從外部通過 props 傳入,再通過 props 上面傳入的回調函數再傳出去,直接修改 props 或者上面的對象,不會帶來界面的更新,而且會導致數據不可預期。

基於這種單向數據流的 flux 思想,redux 還遵循了函數式編程的規範,保證了數據的乾淨。同時,它提供了自頂向下的分發機制,修改 redux store 中的數據,會觸發所有connected 的組件。而觸發過程是,調用 connected 組件 props.dispatch 方法。

雖然單向數據流的方式保證了數據流乾淨,但 redux 的編程方式太複雜了。它不僅增加了數據構造本身的邏輯代碼,而且 action 代碼也是分散的,當你需要進行修改時,有的時候會在好幾個文件之間轉暈。雖然有很多優化 redux 樣板代碼的庫,但受限於它的編程思想,仍然不好在項目中節省更多時間。

新的思維方式

出於節省更多時間成本的目的,我在開發 nautil 中沒有使用 flux 那一套,而是另闢蹊徑,做了很像 mobx 但又更簡單的事。

我們來看一下如何在 nautil 中創建一個 store:

import { Store } from 'nautil'
const store = new Store({
 some: 123,
})

這樣我們就創建了一個 store,非常簡單,只傳入了默認值。而沒有各種 reducer 的樣板代碼。

Store 實例是一個可觀察的對象,通過 watch 方法,可以監聽 store 中數據的變化。但凡能監聽到數據變化,我們就可以在數據變化時,更新界面渲染。所以,在 nautil 中,觀察者模式是核心思想,是實現 nautil 中各種響應式效果的前提條件。

如果你用過 vue 的話,你一定喜歡 vue 中操作數據的方式。在 vue 中要將輸出框組件和數據綁定非常容易:

<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 值發生變化。

  1. 組件內直接使用 this.attrs.age ++ 修改外部傳來的屬性,目標是反寫外部數據
  2. 外部組件在往子組件傳遞雙向綁定屬性時,需要傳入一個特定結構

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 的一些數據狀態管理的東西,如果你對這些問題也有自己的想法,不妨在下方給我留言一起討論。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持神馬文庫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章