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()等不純的方法,因為每次會得到不一樣的結果
    由于 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迅速結合。
使用數(shù)據(jù)锻霎,無非還是查改著角,那么我們究竟怎么來操作?

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

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

import { connect } from 'react-redux'

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

mapStateToProps就是將store里的state轉化為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 重新渲染
  • 操作結束后燃乍,再送出一個 Action唆樊,觸發(fā) State 更新為"操作結束"狀態(tài),View 再一次重新渲染
    這就實現(xiàn)了異步操作刻蟹。

redux-thunk 中間件

異步操作至少要送出兩個 Action:用戶觸發(fā)第一個 Action逗旁,這個跟同步操作一樣,沒有問題舆瘪;如何才能在操作結束時片效,系統(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)),然后進行異步操作唠叛。拿到結果后只嚣,先將結果轉成 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))剩拢,表示操作開始线得。
  • 異步操作結束之后饶唤,再發(fā)出一個 Action(receivePosts(postTitle, json))徐伐,表示操作結束。

這樣的處理募狂,就解決了自動發(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

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末废菱,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子抖誉,更是在濱河造成了極大的恐慌殊轴,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,807評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件袒炉,死亡現(xiàn)場離奇詭異旁理,居然都是意外死亡,警方通過查閱死者的電腦和手機我磁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評論 3 399
  • 文/潘曉璐 我一進店門孽文,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人夺艰,你說我怎么就攤上這事芋哭。” “怎么了郁副?”我有些...
    開封第一講書人閱讀 169,589評論 0 363
  • 文/不壞的土叔 我叫張陵减牺,是天一觀的道長。 經(jīng)常有香客問我,道長拔疚,這世上最難降的妖魔是什么肥隆? 我笑而不...
    開封第一講書人閱讀 60,188評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮稚失,結果婚禮上栋艳,老公的妹妹穿的比我還像新娘。我一直安慰自己句各,他們只是感情好吸占,可當我...
    茶點故事閱讀 69,185評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著凿宾,像睡著了一般旬昭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上菌湃,一...
    開封第一講書人閱讀 52,785評論 1 314
  • 那天问拘,我揣著相機與錄音,去河邊找鬼惧所。 笑死骤坐,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的下愈。 我是一名探鬼主播纽绍,決...
    沈念sama閱讀 41,220評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼势似!你這毒婦竟也來了拌夏?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 40,167評論 0 277
  • 序言:老撾萬榮一對情侶失蹤履因,失蹤者是張志新(化名)和其女友劉穎障簿,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體栅迄,經(jīng)...
    沈念sama閱讀 46,698評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡站故,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,767評論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了毅舆。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片西篓。...
    茶點故事閱讀 40,912評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖憋活,靈堂內(nèi)的尸體忽然破棺而出岂津,到底是詐尸還是另有隱情,我是刑警寧澤悦即,帶...
    沈念sama閱讀 36,572評論 5 351
  • 正文 年R本政府宣布吮成,位于F島的核電站橱乱,受9級特大地震影響,放射性物質發(fā)生泄漏赁豆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,254評論 3 336
  • 文/蒙蒙 一冗美、第九天 我趴在偏房一處隱蔽的房頂上張望魔种。 院中可真熱鬧,春花似錦粉洼、人聲如沸节预。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽安拟。三九已至,卻和暖如春宵喂,著一層夾襖步出監(jiān)牢的瞬間糠赦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評論 1 274
  • 我被黑心中介騙來泰國打工锅棕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留拙泽,地道東北人。 一個月前我還...
    沈念sama閱讀 49,359評論 3 379
  • 正文 我出身青樓裸燎,卻偏偏與公主長得像顾瞻,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子德绿,可洞房花燭夜當晚...
    茶點故事閱讀 45,922評論 2 361