先弄個什么例子呢?如果是現代的MVVM框架,可能會用雙向綁定來吸引你。那react有雙向綁定嗎?
沒有。
也算是有吧,有插件。不過雙向綁定跟react不是一個路子的。react強調的是單向數據流。 當然,即便是單向數據流也總要有個數據的來源,如果數據來源於頁面自身上的用戶輸入,那效果也就等同於雙向綁定了。
下面就展示一下如何達到這個效果。我們來設計一個登錄的場景,用戶輸入用戶名后,會在問候語的位置展示用戶名,像下圖這樣:
預警一下先,我要用這個小東西展示react+redux的數據流工作方式,所以代碼看起來比較多, 肯定比一些MVVM框架雙向綁定一對雙大括號代碼要多得多。但正如我前面說的,它倆不是一個路子, react這種模式的好處后面你一定會看出來,這里先耐着性子把這幾段貌似很羅嗦的代碼看完。 react和redux很多重要的思想在這就開始體現出來了。
先把組件寫出來。為了簡便,我們把整個登錄頁面作為一個組件,放在containers目錄下。 還記得前面說過containers和components目錄嗎?把組件放在containers目錄下,意味着這個組件要跟外界打交道。 不過一開始,我們先別管打交道的事兒,就寫一個簡單的,普通的組件:
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,里面原來的代碼都不需要了,改成:
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')
);
搭建環境時自動打開的瀏覽器頁面還沒關吧?保存代碼后少等片刻就可以看到我們做的登陸頁面了。
目前這個登錄組件里問候語里顯示的用戶名和用戶輸入的用戶名毫無關系,如何將它們聯系起來呢? 既然看到了{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再補充。
那么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都非常獨立,非常容易測試,意義也非常直白。
吹噓了這么多,靠目前這點簡單的代碼也不容易看出來。畢竟這些代碼還沒啥實際意義,作為一個現代的前端應用,連異步都沒有。。。
那么下一節,我們就加點異步進來。