React學習 —— redux和react

Redux 是 JavaScript 狀態(tài)容器,提供可預測化的狀態(tài)管理。
Redux 的創(chuàng)作理念:

  • Web 應用是一個狀態(tài)機,視圖與狀態(tài)是一一對應的。
  • 所有的狀態(tài),保存在一個對象里面。

視圖的每一種變化,都應該對應著一組數(shù)據(jù),這組數(shù)據(jù)就存放在state里。
而我們每次修改數(shù)據(jù)應該是有記錄的,并且是可維護的,redux就給我們提供了這樣的一個機制。

Redux 提供createStore這個函數(shù),用來生成 Store。

import { createStore } from 'redux';
const store = createStore(fn);

什么是Store?從開發(fā)角度來說就是封裝了數(shù)據(jù)的存儲狀態(tài)及暴露出修改查尋數(shù)據(jù)的API。

action和dispatch

我們想要獲得某時刻的數(shù)據(jù)就可以調(diào)用getState()。

import { createStore } from 'redux';
const store = createStore(fn);

const state = store.getState();

前面提到了,我們希望修改數(shù)據(jù)是可維護可記錄的,那么我們就不應該直接對數(shù)據(jù)進行操作。而在redux里,修改數(shù)據(jù)就叫action。
Action 是一個對象。其中的type屬性是必須的,表示 Action 的名稱。其他屬性可以自由設置,社區(qū)有一個規(guī)范可以參考。

const action = {
  type: 'ADD_TODO',
  payload: 'eat dinner'
};

如果我們的應用會很龐大,修改的種類也很多,每次都手寫一個Object雖然沒有什么不行,但是如果要項目升級,統(tǒng)一修改的時候維護都會非常困難,那么我們可以寫一個函數(shù)來創(chuàng)建action,這就是action creator:

const ADD_TODO = '添加 TODO';

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

const action = addTodo('Learn Redux');

說了半天action,最終我們還是要落實到修改數(shù)據(jù),那么有了action如何修改,這個時候就需要使用dispatch(store.dispatch()是 View 發(fā)出 Action 的唯一方法。):

import { createStore } from 'redux';
const store = createStore(fn);

store.dispatch({
  type: 'ADD_TODO',
  payload: 'eat dinner'
});

Reducer

仔細看的到這里都會有疑惑,dispatch了之后store怎么拿到action的type?并且怎么更具payload修改數(shù)據(jù)呢?
這就是reducer在起作用。什么是reducer:Reducer 是一個函數(shù),它接受 Action 和當前 State 作為參數(shù),返回一個新的 State。就是我們:

const store = createStore(fn);

里的fn。
fn通常長這樣:

const defaultState = 0;
const reducer = (state = defaultState, action) => {
  switch (action.type) {
    case 'ADD':
      return state + action.payload;
    default: 
      return state;
  }
};

const state = reducer(1, {
  type: 'ADD',
  payload: 2
});

store.dispatch方法會觸發(fā) Reducer 的自動執(zhí)行。為此,Store 需要知道 Reducer 函數(shù),做法就是在生成 Store 的時候,將 Reducer 傳入createStore方法。

為什么這個函數(shù)叫做 Reducer 呢?因為它可以作為數(shù)組的reduce方法的參數(shù)。請看下面的例子,一系列 Action 對象按照順序作為一個數(shù)組。

const actions = [
  { type: 'ADD', payload: 0 },
  { type: 'ADD', payload: 1 },
  { type: 'ADD', payload: 2 }
];

const total = actions.reduce(reducer, 0); // 3

這里不理解的可以google下Array.reduce。

我們這里只是處理了ADD,但是實際業(yè)務我們會處理更多action來更改更改數(shù)據(jù),那么這個reducer就會變得非常龐大。例如:

const chatReducer = (state = defaultState, action = {}) => {
  const { type, payload } = action;
  switch (type) {
    case ADD_CHAT:
      return Object.assign({}, state, {
        chatLog: state.chatLog.concat(payload)
      });
    case CHANGE_STATUS:
      return Object.assign({}, state, {
        statusMessage: payload
      });
    case CHANGE_USERNAME:
      return Object.assign({}, state, {
        userName: payload
      });
    default: return state;
  }
};

redux提供了Redux 提供了一個combineReducers方法,用于 Reducer 的拆分。你只要定義各個子 Reducer 函數(shù),然后用這個方法,將它們合成一個大的 Reducer。

const reducer = combineReducers({
  a: doSomethingWithA,
  b: processB,
  c: c
})

// 等同于
function reducer(state = {}, action) {
  return {
    a: doSomethingWithA(state.a, action),
    b: processB(state.b, action),
    c: c(state.c, action)
  }
}

純函數(shù)

Reducer 函數(shù)最重要的特征是,它是一個純函數(shù)。也就是說,只要是同樣的輸入,必定得到同樣的輸出。
純函數(shù)是函數(shù)式編程的概念,必須遵守以下一些約束。

  • 不得改寫參數(shù)
  • 不能調(diào)用系統(tǒng) I/O 的API
  • 不能調(diào)用Date.now()或者Math.random()等不純的方法,因為每次會得到不一樣的結(jié)果
    由于 Reducer 是純函數(shù),就可以保證同樣的State,必定得到同樣的 View。
    但也正因為這一點,Reducer 函數(shù)里面不能改變 State,必須返回一個全新的對象,請參考下面的寫法。
// State 是一個對象
function reducer(state, action) {
  return Object.assign({}, state, { thingToChange });
  // 或者
  return { ...state, ...newState };
}

// State 是一個數(shù)組
function reducer(state, action) {
  return [...state, newItem];
}

store.subscribe()

查和改算是說完了,實際業(yè)務中通常會有這樣的場景,當改變了這個值,我需要把界面變成xxx的樣子,這個時候就是用store.subscribe()設置監(jiān)聽函數(shù):

import { createStore } from 'redux';
const store = createStore(reducer);

store.subscribe(listener);

store.subscribe方法返回一個函數(shù),調(diào)用這個函數(shù)就可以解除監(jiān)聽。

let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
);

unsubscribe();

React-Redux

那么redux和react有啥關系? 答案是他倆沒啥關系。
Redux 支持 React、Angular、Ember、jQuery 甚至純 JavaScript。
我們之前說過redux,就是提供了數(shù)據(jù)的管理,并且數(shù)據(jù)和視圖一一對應,那么這一理念就可以使用在react上,用redux來管理復雜的react數(shù)據(jù),所以redux是可有可無,也沒有必要一上來就使用redux,如果要使用,最好判別下你的應用是否真的需要,以下是一些真正需要的場景:

  • 某個組件的狀態(tài),需要共享
  • 某個狀態(tài)需要在任何地方都可以拿到
  • 一個組件需要改變?nèi)譅顟B(tài)
  • 一個組件需要改變另一個組件的狀態(tài)

例如:

  • 用戶的使用方式復雜
  • 不同身份的用戶有不同的使用方式(比如普通用戶和管理員)
  • 多個用戶之間可以協(xié)作
  • 與服務器大量交互,或者使用了WebSocket
  • View要從多個來源獲取數(shù)據(jù)

直接使用redux在react里比較麻煩,于是就出現(xiàn)了react-redux。可以讓redux和react迅速結(jié)合。
使用數(shù)據(jù),無非還是查改,那么我們究竟怎么來操作?

import { connect } from 'react-redux'
const VisibleTodoList = connect()(TodoList);

(1)查-輸入邏輯:外部的數(shù)據(jù)(即state對象)如何轉(zhuǎn)換為 UI 組件的參數(shù)
(2)改-輸出邏輯:用戶發(fā)出的動作如何變?yōu)?Action 對象,從 UI 組件傳出去。

import { connect } from 'react-redux'

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

mapStateToProps就是將store里的state轉(zhuǎn)化為react組件的props:

const mapStateToProps = (state) => {
  return {
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
  }
}
const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case 'SHOW_ALL':
      return todos
    case 'SHOW_COMPLETED':
      return todos.filter(t => t.completed)
    case 'SHOW_ACTIVE':
      return todos.filter(t => !t.completed)
    default:
      throw new Error('Unknown filter: ' + filter)
  }
}

我們在react組件里就可以直接通過this.props.todos來訪問到redux里store的數(shù)據(jù)了。

我們說過了,改數(shù)據(jù)只能dispatch(action)
mapDispatchToProps()用來建立 react組件的參數(shù)到store.dispatch方法的映射。也就是說,它定義了哪些用戶的操作應該當作 Action,傳給 Store。它可以是一個函數(shù),也可以是一個對象。

const mapDispatchToProps = (
  dispatch,
  ownProps
) => {
  return {
    onClick: () => {
      dispatch({
        type: 'SET_VISIBILITY_FILTER',
        filter: ownProps.filter
      });
    }
  };
}

我們調(diào)用this.props.onClick即可改變store的數(shù)據(jù)了。
如果mapDispatchToProps是一個對象,它的每個鍵名也是對應 UI 組件的同名參數(shù),鍵值應該是一個函數(shù),會被當作 Action creator ,返回的 Action 會由 Redux 自動發(fā)出。舉例來說,上面的mapDispatchToProps寫成對象就是下面這樣。

const mapDispatchToProps = {
  onClick: (filter) => {
    type: 'SET_VISIBILITY_FILTER',
    filter: filter
  };
}

<Provider> 組件

connect方法生成容器組件以后,需要讓容器組件拿到state對象,才能生成 真正的react組件的參數(shù)。
React-Redux 提供Provider組件,可以讓容器組件拿到state。

import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'

let store = createStore(todoApp);

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

上面代碼中,Provider在根組件外面包了一層,這樣一來,App的所有子組件就默認都可以拿到state了。

它的原理是React組件的context屬性,請看源碼。

class Provider extends Component {
  getChildContext() {
    return {
      store: this.props.store
    };
  }
  render() {
    return this.props.children;
  }
}

Provider.childContextTypes = {
  store: React.PropTypes.object
}

這樣redux就真正的和react關聯(lián)上了。

異步操作

Action 發(fā)出以后,Reducer 立即算出 State,這叫做同步;Action 發(fā)出以后,過一段時間再執(zhí)行 Reducer,這就是異步。

中間件的概念

let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
  console.log('dispatching', action);
  next(action);
  console.log('next state', store.getState());
}

上面代碼中,對store.dispatch進行了重定義,在發(fā)送 Action 前后添加了打印功能。這就是中間件的雛形。
中間件就是一個函數(shù),對store.dispatch方法進行了改造,在發(fā)出 Action 和執(zhí)行 Reducer 這兩步之間,添加了其他功能。
這個而外的功能就叫做中間件,但是我們有更官方的方式支持。

import { applyMiddleware, createStore } from 'redux';
import createLogger from 'redux-logger';
const logger = createLogger();

const store = createStore(
  reducer,
  applyMiddleware(logger)
);
  • createStore方法可以接受整個應用的初始狀態(tài)作為參數(shù),那樣的話,applyMiddleware就是第三個參數(shù)了。
  • 中間件的次序有講究。

異步操作的基本思路

同步操作只要發(fā)出一種 Action 即可,異步操作的差別是它要發(fā)出三種 Action。
操作發(fā)起時的 Action
操作成功時的 Action
操作失敗時的 Action

// 寫法一:名稱相同,參數(shù)不同
{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }

// 寫法二:名稱不同
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }

除了 Action 種類不同,異步操作的 State 也要進行改造,反映不同的操作狀態(tài)。下面是 State 的一個例子。

let state = {
  // ... 
  isFetching: true,
  didInvalidate: true,
  lastUpdated: 'xxxxxxx'
};

上面代碼中,State 的屬性isFetching表示是否在抓取數(shù)據(jù)。didInvalidate表示數(shù)據(jù)是否過時,lastUpdated表示上一次更新時間。
現(xiàn)在,整個異步操作的思路就很清楚了。

  • 操作開始時,送出一個 Action,觸發(fā) State 更新為"正在操作"狀態(tài),View 重新渲染
  • 操作結(jié)束后,再送出一個 Action,觸發(fā) State 更新為"操作結(jié)束"狀態(tài),View 再一次重新渲染
    這就實現(xiàn)了異步操作。

redux-thunk 中間件

異步操作至少要送出兩個 Action:用戶觸發(fā)第一個 Action,這個跟同步操作一樣,沒有問題;如何才能在操作結(jié)束時,系統(tǒng)自動送出第二個 Action 呢?

奧妙就在 Action Creator 之中。

const fetchPosts = postTitle => (dispatch, getState) => {
  dispatch(requestPosts(postTitle));
  return fetch(`/some/API/${postTitle}.json`)
    .then(response => response.json())
    .then(json => dispatch(receivePosts(postTitle, json)));
  };
};

// 使用方法一
store.dispatch(fetchPosts('reactjs'));
// 使用方法二
store.dispatch(fetchPosts('reactjs')).then(() =>
  console.log(store.getState())
);

上面代碼中,fetchPosts是一個Action Creator(動作生成器),返回一個函數(shù)。這個函數(shù)執(zhí)行后,先發(fā)出一個Action(requestPosts(postTitle)),然后進行異步操作。拿到結(jié)果后,先將結(jié)果轉(zhuǎn)成 JSON 格式,然后再發(fā)出一個 Action( receivePosts(postTitle, json))。
這里有些問題:

  • fetchPosts返回了一個函數(shù),而普通的 Action Creator 默認返回一個對象。
  • 返回的函數(shù)的參數(shù)是dispatch和getState這兩個 Redux 方法,普通的 Action Creator 的參數(shù)是 Action 的內(nèi)容。
  • 在返回的函數(shù)之中,先發(fā)出一個 Action(requestPosts(postTitle)),表示操作開始。
  • 異步操作結(jié)束之后,再發(fā)出一個 Action(receivePosts(postTitle, json)),表示操作結(jié)束。

這樣的處理,就解決了自動發(fā)送第二個 Action 的問題。但是,又帶來了一個新的問題,Action 是由store.dispatch方法發(fā)送的。而store.dispatch方法正常情況下,參數(shù)只能是對象,不能是函數(shù)。
這時,就要使用中間件redux-thunk

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducers';

// Note: this API requires redux@>=3.1.0
const store = createStore(
  reducer,
  applyMiddleware(thunk)
);

因此,異步操作的第一種解決方案就是,寫出一個返回函數(shù)的 Action Creator,然后使用redux-thunk中間件改造store.dispatch。

redux-promise 中間件

既然 Action Creator 可以返回函數(shù),當然也可以返回其他值。另一種異步操作的解決方案,就是讓 Action Creator 返回一個 Promise 對象。

import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise';
import reducer from './reducers';

const store = createStore(
  reducer,
  applyMiddleware(promiseMiddleware)
); 

這個中間件使得store.dispatch方法可以接受 Promise 對象作為參數(shù)。這時,Action Creator 有兩種寫法。寫法一,返回值是一個 Promise 對象。

const fetchPosts = 
  (dispatch, postTitle) => new Promise(function (resolve, reject) {
     dispatch(requestPosts(postTitle));
     return fetch(`/some/API/${postTitle}.json`)
       .then(response => {
         type: 'FETCH_POSTS',
         payload: response.json()
       });
});

import { createAction } from 'redux-actions';

class AsyncApp extends Component {
  componentDidMount() {
    const { dispatch, selectedPost } = this.props
    // 發(fā)出同步 Action
    dispatch(requestPosts(selectedPost));
    // 發(fā)出異步 Action
    dispatch(createAction(
      'FETCH_POSTS', 
      fetch(`/some/API/${postTitle}.json`)
        .then(response => response.json())
    ));
  }

參考文獻

react.docschina
redux中文官網(wǎng)
阮一峰redux 1-3

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372