本文是『horseshoe·Redux專題』系列文章之一,後續會有更多專題推出
來個人 GitHub repo 閱讀完整的專題文章
來個人 我的博客 得到無與倫比的閱讀體驗
Redux是一套精巧而實用的工具,這也是它在開發者中如此流行的緣由。javascript
因此對待Redux,最重要的就是熟練使用它的主要API,一旦將它瞭然於胸,就會對Redux的設計思想有一個全局的認識,也就能清楚的判斷本身的應用需不須要勞駕Redux出手。java
須要注意:我們默認將Redux和React搭配使用,不過Redux不是非得和它在一塊兒的。react
要達成某個目的,開發者首先要描述本身的意圖。Action就是用來描述開發者意圖的。它不是一個函數,而是一個普通的對象,經過聲明的類型來觸發相應的動做。git
咱們來看一個例子:github
{ type: 'ADD_TODO_ITEM', payload: { content: '每週看一本書', done: false, }, }
Redux官方定義了字段的一些規範:一個Action必須包含type
字段,同時一個Action包含的字段不該該超過type
、payload
、error
、meta
這四種。編程
有效載荷
。引伸到程序中就是有效字段的意思,也就是說真正用於構建應用的信息都應該放到payload字段裏。元
。在這裏表示除了payload以外的信息。由於意圖是經過類型來定義的,因此type字段必不可少,稱某個對象爲一個Action的標誌就是它有一個type字段。redux
除此以外,一個動做可能包含更爲豐富的信息。開發者能夠隨意添加字段,畢竟它就是個普通對象。不過遵照必定的規範便於其餘開發者閱讀你的代碼,能夠提高協做效率。dom
前面說了type字段通常用全大寫的字符串表示,多個字母用下劃線分隔。不只如此,你們還有一個約定俗成:用一個結構相同的變量保存該字符串,由於它會在多處用到。異步
const ADD_TODO_ITEM = 'ADD_TODO_ITEM';
集中保存這些變量的文件就叫Constants.js
。ide
在此,我提出一點異議。若是你以爲不麻煩,那遵循規範再好不過。但開發者向來以爲Redux過於繁瑣,若是你也這麼以爲,大可沒必要維護所謂的Constants。維護Constants的好處不過是一處更改到處生效,然而字符串和變量是結構相同的,若是字符串做了修改,語意上必然大打折扣,何況type字段一旦定義極少更改,因此視你的協做規模和我的喜愛而定,爲Redux的繁瑣減負不是麼?
咱們知道Action是一個對象,可是若是屢次用到這個對象,咱們能夠寫一個生成Action的函數。
function addTodoItem(content) { return { type: ADD_TODO_ITEM, payload: { content, done: false }, }; }
同理,若是你以爲繁瑣,這一步是能夠免去的。
異步場景下Action Creators會大有用處,後面會講到。
須要注意的是:所謂的Action更確切的說是一個執行動做的指令,而不是一個動做。或者咱們換一種說法,這裏的動做指的是動做描述,而不是動做派發。
Redux的本質不復雜,就是用一個全局的外部的對象來存儲狀態,而後經過觀察者模式來構建一套狀態更新觸發通知的機制。
這裏的Store就是存儲狀態的容器。
可是呢?它須要開發者動手寫一套邏輯來指導它怎麼處理狀態的更新,這就是後面要講的Reducer,暫且按下不表。
問題是Store怎麼接收這套邏輯呢?
import { createStore } from 'redux'; import reducer from './reducer'; const store = createStore(reducer);
看到沒有,Redux專門有一個API用來建立Store,它接受三個參數:reducer
、preloadedState
和enhancer
。
reducer就是處理狀態更新的邏輯。
preloadedState是初始狀態,若是你須要讓Store一開始不是空對象,那麼能夠從這裏傳進去。
enhancer翻譯成中文是加強器
,是用來裝載第三方插件以加強Redux的功能的。
咱們已經瞭解了Action的做用,可是Action只是對動做的描述,怎麼說它得有個發射器吧。這個發射器就隱藏在Store裏。
執行createStore
返回的對象包含了一個函數dispatch
,傳入Action執行就會發射一個動做。
import React, { Component } from 'react'; import store from './store'; import action from './action'; class App extends Component { render() { return ( <button onClick={() => store.dispatch(action)}>dispatch</button> ); } } export default App;
好了咱們已經發射了一個動做,假設如今Store中已經有狀態了,咱們怎麼把它取出來呢?
直接store.xxx
麼?
咱們先來打印Store這個對象看看:
{ dispatch: ƒ dispatch(action), getState: ƒ getState(), replaceReducer: ƒ replaceReducer(nextReducer), subscribe: ƒ subscribe(listener), Symbol(observable): ƒ observable(), }
打印出來一堆API,這可咋整?
彆着急,茫茫人海中看到一個叫getState
的東西,它就是咱們要找的高人吧。插一句,你們注意區分Store和State的區別,Store是存儲State的容器。
Redux隱藏了Store的內部細節,因此開發者只能用getState來獲取狀態。
Redux是基於觀察者模式的,因此它開放了一個訂閱的API給開發者,每次發射一個動做,傳入訂閱器的回調都會執行。經過它開發者就能監聽動做的派發以執行相應的邏輯。
import store from './store'; store.subscribe(() => console.log('有一個動做被髮射了'));
顧名思義,替換Reducer,這主要是方便開發者調試Redux用的。
Reducer是Redux的核心概念,由於Redux的做者Dan Abramov這樣解釋Redux
這個名字的由來:Reducer+Flux。
其實Reducer是一個計算機術語,包括JavaScript中也有用於迭代的reduce
函數。因此咱們先來聊聊應該怎樣理解Reducer這個概念。
reduce翻譯成中文是減小
,Reducer在計算機中的含義是歸併,也是化多爲少的意思。
咱們來看JavaScript中reduce的寫法:
const array = [1, 2, 3, 4, 5]; const sum = array.reduce((total, num) => total + num);
再來看Redux中Reducer的寫法:
function todoReducer(state = [], action) { switch (action.type) { case 'ADD_TODO_ITEM': const { content, done } = action.payload; return [...state, { content, done }]; case 'REMOVE_TODO_ITEM': const todos = state.filter(todo => todo.content !== action.content); return todos; default: return state; } }
state參數是一箇舊數據集合,action中包含的payload是一個新的數據項,Reducer要作的就是將新的數據項和舊數據集合歸併到一塊兒,返回給Store。這樣看起來Reducer這個名字起的也沒那麼晦澀了是否是?
一個Reducer接受兩個參數,第一個參數是舊的state,咱們返回的數據就是用來替換它的,而後風水輪流轉,此次返回的數據下次就變成舊的state了,如此往復;第二個參數是咱們派發的action。
由於Reducer的結構相似,都是根據Action的類型返回相應的數據,因此通常採用switch case
語句,若是沒有變更則返回舊的state,總之它必須有返回值。
Reducer的做用是歸併,也只能是歸併,因此Redux規定它必須是一個純函數。相同的輸入必須返回相同的輸出,並且不能對外產生反作用。
因此開發者在返回數據的時候不能直接修改原有的state,而是應該在拷貝的副本之上再作修改。
一個Reducer只應該處理一個動做,但是咱們的應用不可能只有一個動做,因此一個典型的Redux應用會有不少Reducer函數。那麼怎麼管理這些Reducer呢?
首先來看只有一個Reducer的狀況:
import { createStore } from 'redux'; import reducer from './reducer'; const store = createStore(reducer); export default store;
若是隻有一個Reducer,那咱們只須要將它傳入createStore
這個函數中,就這麼簡單。這時候Reducer返回的狀態就是Store中的所有狀態。
而若是有多個Reducer,咱們就要動用Redux的另外一個API了:combineReducers
。
const reducers = combineReducers({ userStore: userReducer, todoStore: todoReducer, });
當咱們有多個Reducer,就意味着有多個狀態須要交給Store管理,咱們就須要子容器來存儲它們,其實就是對象嵌套對象的意思。combineReducers就是用來幹這個的,它把每個Reducer分門別類的與不一樣的子容器對應起來,某個Reducer只處理對應的狀態。
{ userStore: {}, todoStore: {}, };
當咱們用getState獲取整個Store的狀態,返回的對象就是上面這樣的。
你猜對了,傳入combineReducers的對象的key就是子容器的名字。
當開發者調用createStore建立Store時,傳入的全部Reducer都會執行一遍。注意,這時開發者尚未發射任何動做呢,那爲何會執行一遍?
const randomString = () => Math.random().toString(36).substring(7).split('').join('.'); const ActionTypes = { INIT: `@@redux/INIT${randomString()}`, REPLACE: `@@redux/REPLACE${randomString()}`, PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}` }; dispatch({ type: ActionTypes.INIT });
由於Redux源碼中,在createStore函數裏面放了這樣一段邏輯,這初始化時的dispatch是Redux本身發射的。
爲何?
還記得Reducer接受兩個參數嗎?第一個是state,而咱們能夠給state設置默認值。
聰明的你必定想到了,初始化Store時Redux本身發射一個動做的目的是爲了收集這些默認值。Reducer會將這些默認值返回給Store,這樣默認值就保存到Store中了。
聰明的你大概還想到一個問題:createStore也有默認值,Reducer也有默認值,不會打架麼?
Redux的規矩:createStore的默認值優先級更高,因此不會打架。
在一個有若干Reducer的應用中,一個動做是怎麼找到對應的Reducer的?
這是一個好問題,答案是挨個找。
假如應用有1000個Reducer,與某個動做對應的Reducer又剛好在最後一個,那要把1000個Reducer都執行一遍,Redux不會這麼傻吧?
Redux還真就這麼傻。
由於當一個動做被派發時,Redux並不知道應該由哪一個Reducer來處理,因此只能讓每一個Reducer都處理一遍,看看究竟是誰的菜。可不能夠在設計上將動做與Reducer對應起來呢?固然是能夠的,可是Redux爲了保證API的簡潔和優美,決定犧牲這一部分性能。
只是一些純函數而已,莫慌。
當咱們使用Redux時,咱們但願每發射一個動做,應用的狀態自動發生改變,從而觸發頁面的從新渲染。
import React, { Component } from 'react'; import store from './store'; class App extends Component { state = { name: 'Redux' }; render() { const { name } = this.state; return ( <div>{name}</div> ); } componentDidMount() { this.unsubscribe = store.subscribe(() => { const { name } = store.getState(); this.setState({ name }); }); } componentWillUnmount() { this.unsubscribe(); } }
怎麼辦呢?開發者得手動維護一個訂閱器,才能監聽到狀態變化,從而觸發頁面從新渲染。
可是React最佳實踐告訴咱們,一個負責渲染UI的組件不該該有太多的邏輯,那麼有沒有更好的辦法使得開發者能夠少寫一點邏輯,同時讓組件更加優雅呢?
別擔憂,Redux早就幫開發者作好了,不過它是一個獨立的模塊:react-redux
。顧名思義,這個模塊的做用是鏈接React和Redux。
鏈接React和Redux的第一步是什麼呢?固然是將Store集成到React組件中,這樣咱們就不用每次在組件代碼中import store
了。多虧了React context的存在,Redux只須要將Store傳入根組件,全部子組件就能經過某種方式獲取傳入的Store。
import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import store from './store'; import App from './App'; ReactDOM.render( <Provider store={store}> <App /> </Provider> , document.getElementById('root') );
老式的context寫法,在子組件中定義contextTypes
就能夠接收到傳入的參數。固然,你確定也想到,Redux把這些細節都封裝好了,這就是connect
。
connect接口的意義主要有三點:
import React from 'react'; import Todo from './Todo'; const App = ({ todos, addTodoItem }) => { return ( <div> <button onClick={() => addTodoItem()}>add</button> {todos.map(todo => <Todo key={todo.id} {...todo} />)} </div> ); } const mapStateToProps = (state, ownProps) => { return { todos: state.todoStore, }; }; const mapDispatchToProps = (dispatch, ownProps) => { return { addTodoItem: (todoItem) => dispatch({ type: 'ADD_TODO_ITEM', payload: todoItem }), }; }; export default connect(mapStateToProps, mapDispatchToProps)(App);
咱們看上面例子,connect接受的兩個參數:mapStateToProps
和mapDispatchToProps
,所謂的map就是映射,意思就是將全部state和dispatch依次映射到props上。如此真正的組件須要的數據和功能都在props上,它就能夠安安心心的作一個傻瓜組件。
connect接受四個參數:
<App value={value} />
,那麼ownProps就是一個包含value的對象。Object.assign()
合併上述三種props。咱們注意到,connect要先執行一次,返回的結果再次執行才傳入開發者定義的組件。它返回一個新的組件,這個新的組件不會修改原組件(除非你操縱了ownProps的返回),而是爲組件增長一些新的props。
咱們也能夠用裝飾器寫法來重寫connect:
import React from 'react'; import Todo from './Todo'; const mapStateToProps = (state, ownProps) => { return { todos: state.todoStore, }; }; const mapDispatchToProps = (dispatch, ownProps) => { return { addTodoItem: (todoItem) => dispatch({ type: 'ADD_TODO_ITEM', payload: todoItem }), }; }; @connect(mapStateToProps, mapDispatchToProps) const App = ({ todos, addTodoItem }) => { return ( <div> <button onClick={() => addTodoItem()}>add</button> {todos.map(todo => <Todo key={todo.id} {...todo} />)} </div> ); } export default App;
Redux經過調用createStore返回Store,它是一個獨立於應用的全局對象,經過觀察者模式能讓應用監聽到Store中狀態的變化。最佳實踐是一個應用只有一個Store。
Redux必須經過一個明確的動做來修改Store中的狀態,描述動做的是一個純對象,必須有type字段,傳遞動做的是Store的屬性方法dispatch。
Store自己並無任何處理狀態更新的邏輯,全部邏輯都要經過Reducer傳遞進來,Reducer必須是一個純函數,沒有任何反作用。若是有多個Reducer,則須要利用combineReducers定義相應的子狀態容器。
基於容器組件和展現組件分離的設計原則,也爲了提升開發者的編程效率,Redux經過一個額外的模塊將React和Redux鏈接起來,使得全部的狀態管理接口都映射到組件的props上。其中,Provider將Store注入應用的根組件,解決的是鏈接的充分條件;connect將須要用到的state和dispatch都映射到組件的props上,解決的是鏈接的必要條件。只有被Provider包裹的組件,才能使用connect包裹。
Redux專題一覽