先弄個什麼例子呢?若是是現代的MVVM框架,可能會用雙向綁定來吸引你。那react有雙向綁定嗎?javascript
沒有。html
也算是有吧,有插件。不過雙向綁定跟react不是一個路子的。react強調的是單向數據流。 固然,即使是單向數據流也總要有個數據的來源,若是數據來源於頁面自身上的用戶輸入,那效果也就等同於雙向綁定了。前端
下面就展現一下如何達到這個效果。咱們來設計一個登陸的場景,用戶輸入用戶名後,會在問候語的位置展現用戶名,像下圖這樣:java
預警一下先,我要用這個小東西展現react+redux的數據流工做方式,因此代碼看起來比較多, 確定比一些MVVM框架雙向綁定一對雙大括號代碼要多得多。但正如我前面說的,它倆不是一個路子, react這種模式的好處後面你必定會看出來,這裏先耐着性子把這幾段貌似很羅嗦的代碼看完。 react和redux不少重要的思想在這就開始體現出來了。react
先把組件寫出來。爲了簡便,咱們把整個登陸頁面做爲一個組件,放在containers目錄下。 還記得前面說過containers和components目錄嗎?把組件放在containers目錄下,意味着這個組件要跟外界打交道。 不過一開始,咱們先別管打交道的事兒,就寫一個簡單的,普通的組件:webpack
import React from 'react' class Login extends React.Component{ render(){ return ( <div> <div>早上好,{this.props.username}</div> <div>用戶名:<input/></div> <div>密 碼:<input type="papssword"/></div> <button>登陸</button> </div> ) } } export default Login
爲了能讓咱們寫的東西顯示出來,得改點模板代碼,如今來修改一下src/index.js,裏面原來的代碼都不須要了,改爲:es6
import React from 'react'; import { render } from 'react-dom'; import { Provider } from 'react-redux'; import configureStore from './stores'; import Login from './containers/Login'; const store = configureStore(); render( <Provider store={store}> <Login /> </Provider>, document.getElementById('app') );
搭建環境時自動打開的瀏覽器頁面還沒關吧?保存代碼後少等片刻就能夠看到咱們作的登錄頁面了。web
目前這個登陸組件裏問候語裏顯示的用戶名和用戶輸入的用戶名毫無關係,如何將它們聯繫起來呢? 既然看到了{this.props.username}你確定會想到有一個數據模型。的確是有這麼個東西,不過在redux裏, 這個數據模型很壯觀,整個應用只有一個數據模型,因此更應該管它叫數據倉庫。這個倉庫的代碼在stores/index.js裏面。 代碼很簡單,就是用reducers和initialState兩個參數來建立一個倉庫。看剛纔run.js裏面的代碼, 有個叫Provider的組件使用了倉庫,意思很明顯:在provider這個組件內部,已經給咱們提供好了倉庫的訪問條件, 也就是說咱們的Login組件已經能夠訪問倉庫了。怎麼訪問呢?須要把咱們的組件跟倉庫鏈接起來。 登陸組件代碼最後一行「export default Login」要改爲這樣:編程
function mapStateToProps(state) { return {} } export default connect(mapStateToProps)(Login);
connect是react-redux這個庫提供的函數,功能就是把組件鏈接到rudux的倉庫。注意在文件頂部加上一句「import { connect } from 'react-redux'」。 這裏有個函數mapStateToProps,它返回的對象就是從倉庫取出的數據,具體的數據等咱們寫完reducer再補充。redux
那麼reducer是什麼呢?
咱們考慮一下倉庫的數據是要變化的,怎麼讓它變化呢?咱們得給個規則,這個規則描述起來就是: 「在發生某一動做(action)時,倉庫中的一部分數據要進行相應的變化」。咱們管會因動做而變化的這一部分數據叫作狀態, 許許多多瑣碎的狀態組成了倉庫數據,因此整個倉庫其實就是一個大的狀態。在程序運行過程當中,咱們主要關心的就是這個倉庫的狀態如何變化。 如何變化?那就要靠reducer。針對一個動做,倉庫裏會有一個或多個狀態發生變化,reducer就是要指導狀態如何變化。
等等,那動做是哪來的?從具體上說,動做通常是來源於用戶的操做或者網絡請求的迴應。在代碼裏須要對動做規範一下, 其實也就是跟reducer進行一個約定,讓它知道有動做來了。其實怎樣表示動做均可以,只要具備惟一性就行。 通常咱們就用字符串就好了,即容易製造惟一,又可以表義,在使用中當心點別重了就行。下面就來定義一個用戶輸入用戶名的動做:
const INPUT_USERNAME = 'INPUT_USERNAME'
咋不直接用字符串呢?爲了不低級錯誤,定義了這個常量之後,發起動做時用這個常量,reducer也根據這個常量辨別動做類型。
咱們光告訴reducer發生了「用戶輸入」這個動做還不夠,還要告訴reducer用戶輸入了什麼內容。因此完整的動做得是一個具備豐富信息的對象。 爲了方便,咱們寫一個動做生成器,也就是個函數:
function inputUsername (value) { return { type: INPUT_USERNAME, value: value } }
如今reducer就能獲得足夠的信息來指導狀態的變化了。reducer要作的就是把倉庫裏一個叫作「username」的狀態的值修改一下。 因爲狀態能夠是一層套一層的,因此reducer也被設計成能夠一層套一層。單個reducer就是它上級reducer的一分子。 其實reducer自己也就是個函數:
function username (state='', action) { switch(action.type){ case INPUT_USERNAME: return action.value defalut: return state } }
reducer的函數名對應着狀態名稱,函數接受兩個參數:第一個是當前狀態,若是是程序開始運行的時候, 極可能沒有當前狀態,就給個默認值,這裏是空字符串;第二個是前面動做生成器生成的action對象。 一個reducer能夠處理多種動做,目前咱們只有一個,之後有別的就直接加case分支。對於每種動做, reducer都要返回一個新的狀態值,這個值就能夠根據action傳來的信息按照業務要求生成了。 最後必定要加一個默認狀況返回當前狀態。在redux裏,任何一個action都會在全部的reducer裏過一遍, 因此對於一個reducer來講實際上絕大多數狀況action都不是它能處理的,最後仍是返回當前狀態值。 以爲很低效嗎?😉別怕,只是空走了一遍分支,這對諸如修改DOM這樣的重頭戲來講根本不算什麼。
reducer是一層又一層的樹狀結構,怎麼把它們組合到一塊兒呢?redux提供了一個組合工具combineReducers。 加入咱們已經寫好了另外一個名爲password的reducer,組合它們就是這個樣子:
combineReducers({username, password})
注意,combineReducers接收的參數是一個對象,而不是多個函數,上面的代碼用的是es6的簡寫方式。
很容易發現,上面的reducer和action生成器都是很是死板的代碼,從此咱們會寫大量的這樣的代碼, 那會出現滿篇樣板代碼的情形,那可有點蠢笨了。因此咱們把重複的東西儘量的抽取出來,寫個reucer生成器以及action生成器的生成器, 把他們放到src/utils裏面:
// reducer生成器,爲了之後使用方便,起名爲create reducer的簡寫 export function cr (initialState, handlers) { return function reducer(state = initialState, action) { if (handlers.hasOwnProperty(action.type)) { return handlers[action.type](state, action); } else { return state; } } } // action生成器的生成器,一樣緣由,起名爲create action creator的簡寫 export function cac (type, ...argNames){ return function(...args) { let action = { type } argNames.forEach((arg, index) => { action[argNames[index]] = args[index] }) return action } }
這倆函數完成的事情跟咱們寫樣板代碼作的事情徹底相同。具體說明一下:
cr的兩個參數:initialState是初始狀態;handlers是由一堆函數組成的對象,每一個函數的名稱對應着一個action的類型, 每一個函數接受的參數與reducer同樣,是action和當前狀態,返回值會被當作新狀態。默認狀況就不用咱們處理了。
cac接受的第一個參數是action的類型名稱,後面參數是全部附帶數據的屬性名稱。
好了,把代碼規整一下。對如今小小的模擬雙向綁定的功能來講,咱們還不須要記錄密碼的狀態,不過咱們也先寫上,後面會用到。
最好先寫action。由於通常來講,只要你想好了你得應用有什麼功能,action就能夠寫了,並且action不依賴其它東西。
src/actions/login.js:
import {cac} from '../utils' export const INPUT_USERNAME = 'INPUT_USERNAME' export const INPUT_PASSWORD = 'INPUT_PASSWORD' export const inputUsername = cac(INPUT_USERNAME, 'value') export const inputPassword = cac(INPUT_PASSWORD, 'value')
這裏咱們把全部的東西都導出了,action類型名稱reducer會用到,action生成器組件會用到。
而後寫reducer。當你想好應用的功能後,接下來就是要考慮背後的數據結構了。而reducer一寫出來,數據結構就肯定了。
src/reucers/login.js:
import {combineReducers} from 'redux'; import {cr} from '../utils' import {INPUT_USERNAME, INPUT_PASSWORD} from 'actions/login' export default combineReducers({ username: cr('', { [INPUT_USERNAME](state, {value}){return value} }), password: cr('', { [INPUT_PASSWORD](state, {value}){return value} }) })
rducer最終是要註冊到store那裏的,這個過程在src/storces/index.js裏面已經寫了, 能夠看到裏面的代碼用的是../reducers這個文件(這是個目錄,實際的文件是裏面index.js), 因此咱們也須要把新寫的reducer註冊到這裏面去。修改src/reducers/index.js:
import { combineReducers } from 'redux'; import login from './login' const reducers = { login }; module.exports = combineReducers(reducers);
在reducers/index裏,全部的reducer也是經過combineReducers組合到一塊兒的,只不過如今咱們只有一個孤零零的子reducer:login。
終於,是時候回到組件上來了。src/containers/Login.js如今要修改爲這樣:
import React from 'react' import { connect } from 'react-redux' import {inputUsername, inputPassword} from 'actions/login' class Login extends React.Component{ inputUsernameHandler(evt){ this.props.dispatch(inputUsername(evt.target.value)) } inputPasswordHandler(evt){ this.props.dispatch(inputPassword(evt.target.value)) } render(){ return ( <div> <div>早上好,{this.props.username}</div> <div>用戶名:<input onChange={this.inputUsernameHandler.bind(this)}/></div> <div>密 碼:<input type="papssword" onChange={this.inputPasswordHandler.bind(this)}/></div> <button>登陸</button> </div> ) } } function mapStateToProps(state) { return { username: state.login.username, password: state.login.password } } export default connect(mapStateToProps)(Login);
有幾處變化:
首先,前面已經說過,要把組件鏈接到倉庫,就要用connect。而且如今咱們已經肯定了倉庫裏login對應狀態的數據接口, 那麼mapStateToProps返回的內容也就肯定了。login狀態裏的兩個屬性映射成了組件的屬性, 因此用this.props.username就能夠訪問到倉庫裏的login.username。
而後兩個input上都加上了change事件處理。當change事件被觸發時,經過this.props.dispatch函數就能夠通知倉庫有動做發生了, 倉庫此時就會調用全部的reducer來應對這個事件。
好了,到這裏小小的雙向綁定功能實現了😓試試吧。
在MVVM框架裏只須要創建一個視圖模型,用一對雙大括號就能完成的事情,到react加redux裏面爲什麼如此大費周折?
其實我是專門在展現完整的redux+react開發流程。若是隻是要單個頁面上的這點功能,用事件處理來改變組件的state就好了。 那麼redux爲何要引入這麼個流程?我在開發中以爲有這麼幾個特色:從直觀上看在視野不同。仍是跟MVVM比吧, MVVM框架的視野在於局部,而redux的視野在於全局。MVVM對一個controller對應一個模型,模型裏的數據只能本身用, 模型之間通訊須要其它的數據傳遞方式。redux(或者說是flux的模式)管理着一個大數據倉庫, 任什麼時候候均可以從這個倉庫中取到一切細節的狀態(有沒有云的感受?),當開發單頁應用的時候,這一優點會特別明顯。 從編程語言角度上看,redux+react方式充分利用了函數式編程的優點。redux(flux)強調單向數據流, 單向數據流就像生產流水線,原料被各個工序依次加工,最終成爲產品,而在這個過程當中要避免外界因素對各個階段的原料產生影響, 不然就會出現非預期的產品(次品)。純函數就像這個流水線中的工序,讓數據處理的過程簡單明瞭。 發現了嗎?前面的代碼中純函數是主力。reducer很明顯是純函數。組件也是純函數,注意,咱們的組件並無直接被狀態控制, 而是有個connect的過程,狀態是被映射成組件的屬性的,對於組件來講,根本不知道狀態爲什麼物。 這樣咱們的組件、reducer都很是獨立,很是容易測試,意義也很是直白。
吹噓了這麼多,靠目前這點簡單的代碼也不容易看出來。畢竟這些代碼還沒啥實際意義,做爲一個現代的前端應用,連異步都沒有。。。
那麼下一節,咱們就加點異步進來。