Redux基礎

前端開發React用的很多,但它只是一個view,涉及到復雜的功能時就必須要用到Redux/Mobx等狀態管理器,React 只是 DOM 的一個抽象層,并不是 Web 應用的完整解決方案。
參考http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_one_basic_usages.html

是否需要Redux?

首先明確一點,Redux 是一個有用的架構,但不是非用不可。事實上,大多數情況,你可以不用它,只用 React 就夠了。

曾經有人說過這樣一句話
"如果你不知道是否需要 Redux,那就是不需要它。"

Redux 的創造者 Dan Abramov 又補充了一句。
"只有遇到 React 實在解決不了的問題,你才需要 Redux 。"

簡單說,如果你的UI層非常簡單,沒有很多互動,Redux 就是不必要的,用了反而增加復雜性。

  • 用戶的使用方式非常簡單
  • 用戶之間沒有協作
  • 不需要與服務器大量交互,也沒有使用 WebSocket
  • 視圖層(View)只從單一來源獲取數據

上面這些情況,都不需要使用 Redux。

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

上面這些情況才是 Redux 的適用場景:多交互、多數據源

從組件角度看,如果你的應用有以下場景,可以考慮使用 Redux。

  • 某個組件的狀態,需要共享
  • 某個狀態需要在任何地方都可以拿到
  • 一個組件需要改變全局狀態
  • 一個組件需要改變另一個組件的狀態

發生上面情況時,如果不使用 Redux 或者其他狀態管理工具,不按照一定規律處理狀態的讀寫,代碼很快就會變成一團亂麻。你需要一種機制,可以在同一個地方查詢狀態、改變狀態、傳播狀態的變化。

總之,不要把 Redux 當作萬靈丹,如果你的應用沒那么復雜,就沒必要用它。另一方面,Redux 只是 Web 架構的一種解決方案,也可以選擇其他方案。

Redux設計思想

Redux 的設計思想很簡單,就兩句話:

  1. Web 應用是一個狀態機,視圖與狀態是一一對應的。
  2. 所有的狀態,保存在一個對象里面。

基本概念和 API

Store

Store 就是保存數據的地方,你可以把它看成一個容器。整個應用只能有一個 Store。

Redux 提供createStore這個函數,用來生成 Store。

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

上面代碼中,createStore函數接受另一個函數作為參數,返回新生成的 Store 對象。

State

Store對象包含所有數據。如果想得到某個時點的數據,就要對 Store 生成快照。這種時點的數據集合,就叫做 State。

當前時刻的 State,可以通過store.getState()拿到。

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

const state = store.getState();

Redux 規定, 一個 State 對應一個 View。只要 State 相同,View 就相同。你知道 State,就知道 View 是什么樣,反之亦然。

Action

State 的變化,會導致 View 的變化。但是,用戶接觸不到 State,只能接觸到 View。所以,State 的變化必須是 View 導致的。Action 就是 View 發出的通知,表示 State 應該要發生變化了。

Action 是一個對象。其中的type屬性是必須的,表示 Action 的名稱。其他屬性可以自由設置,社區有一個規范可以參考。

const action = {
  type: 'ADD_TODO',
  payload: 'Learn Redux'
};

上面代碼中,Action 的名稱是ADD_TODO,它攜帶的信息是字符串Learn Redux。

可以這樣理解,Action 描述當前發生的事情。改變 State 的唯一辦法,就是使用 Action。它會運送數據到 Store。

Action Creator

View 要發送多少種消息,就會有多少種 Action。如果都手寫,會很麻煩。可以定義一個函數來生成 Action,這個函數就叫 Action Creator。

const ADD_TODO = '添加 TODO';

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

const action = addTodo('Learn Redux');

上面代碼中,addTodo函數就是一個 Action Creator。

store.dispatch()

store.dispatch()是 View 發出 Action 的唯一方法。

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

store.dispatch({
  type: 'ADD_TODO',
  payload: 'Learn Redux'
});

上面代碼中,store.dispatch接受一個 Action 對象作為參數,將它發送出去。

結合 Action Creator,這段代碼可以改寫如下。

store.dispatch(addTodo('Learn Redux'));

Reducer

Store 收到 Action 以后,必須給出一個新的 State,這樣 View 才會發生變化。這種 State 的計算過程就叫做 Reducer。

Reducer 是一個函數,它接受 Action 和當前 State 作為參數,返回一個新的 State。

const reducer = function (state, action) {
  // ...
  return new_state;
};

整個應用的初始狀態,可以作為 State 的默認值。下面是一個實際的例子。

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
});

上面代碼中,reducer函數收到名為ADD的 Action 以后,就返回一個新的 State,作為加法的計算結果。其他運算的邏輯(比如減法),也可以根據 Action 的不同來實現。

實際應用中,Reducer 函數不用像上面這樣手動調用,store.dispatch方法會觸發 Reducer 的自動執行。為此,Store 需要知道 Reducer 函數,做法就是在生成 Store 的時候,將 Reducer 傳入createStore方法。

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

上面代碼中,createStore接受 Reducer 作為參數,生成一個新的 Store。以后每當store.dispatch發送過來一個新的 Action,就會自動調用 Reducer,得到新的 State。

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

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

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

上面代碼中,數組actions表示依次有三個 Action,分別是加0、加1和加2。數組的reduce方法接受 Reducer 函數作為參數,就可以直接得到最終的狀態3。

純函數

Reducer 函數最重要的特征是,它是一個純函數。也就是說,只要是同樣的輸入,必定得到同樣的輸出。

純函數是函數式編程的概念,必須遵守以下一些約束。

  • 不得改寫參數
  • 不能調用系統 I/O 的API
  • 不能調用Date.now()或者Math.random()等不純的方法,因為每次會得到不一樣的結果

由于 Reducer 是純函數,就可以保證同樣的State,必定得到同樣的 View。但也正因為這一點,Reducer 函數里面不能改變 State,必須返回一個全新的對象,請參考下面的寫法。

// State 是一個對象
function reducer(state, action) {
  return Object.assign({}, state, { thingToChange });
  // 或者
  return { ...state, ...newState };
}

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

最好把 State 對象設成只讀。你沒法改變它,要得到新的 State,唯一辦法就是生成一個新對象。這樣的好處是,任何時候,與某個 View 對應的 State 總是一個不變的對象。

store.subscribe()

Store 允許使用store.subscribe方法設置監聽函數,一旦 State 發生變化,就自動執行這個函數。

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

store.subscribe(listener);

顯然,只要把 View 的更新函數(對于 React 項目,就是組件的render方法或setState方法)放入listen,就會實現 View 的自動渲染。

store.subscribe方法返回一個函數,調用這個函數就可以解除監聽。

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

unsubscribe();

Store 的實現

上面介紹了 Redux 涉及的基本概念,可以發現 Store 提供了三個方法。

  • store.getState()
  • store.dispatch()
  • store.subscribe()
import { createStore } from 'redux';
let { subscribe, dispatch, getState } = createStore(reducer);

createStore方法還可以接受第二個參數,表示 State 的最初狀態。這通常是服務器給出的。

let store = createStore(todoApp, window.STATE_FROM_SERVER)

上面代碼中,window.STATE_FROM_SERVER就是整個應用的狀態初始值。注意,如果提供了這個參數,它會覆蓋 Reducer 函數的默認初始值。

下面是createStore方法的一個簡單實現,可以了解一下 Store 是怎么生成的。

const createStore = (reducer) => {
  let state;
  let listeners = [];

  const getState = () => state;

  const dispatch = (action) => {
    state = reducer(state, action);
    listeners.forEach(listener => listener());
  };

  const subscribe = (listener) => {
    listeners.push(listener);
    return () => {
      listeners = listeners.filter(l => l !== listener);
    }
  };

  dispatch({});

  return { getState, dispatch, subscribe };
};

Reducer 的拆分

Reducer 函數負責生成 State。由于整個應用只有一個 State 對象,包含所有數據,對于大型應用來說,這個 State 必然十分龐大,導致 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;
  }
};

上面代碼中,三種 Action 分別改變 State 的三個屬性。

  • ADD_CHAT:chatLog屬性
  • CHANGE_STATUS:statusMessage屬性
  • CHANGE_USERNAME:userName屬性

這三個屬性之間沒有聯系,這提示我們可以把 Reducer 函數拆分。不同的函數負責處理不同屬性,最終把它們合并成一個大的 Reducer 即可。

const chatReducer = (state = defaultState, action = {}) => {
  return {
    chatLog: chatLog(state.chatLog, action),
    statusMessage: statusMessage(state.statusMessage, action),
    userName: userName(state.userName, action)
  }
};

上面代碼中,Reducer 函數被拆成了三個小函數,每一個負責生成對應的屬性。

這樣一拆,Reducer 就易讀易寫多了。而且,這種拆分與 React 應用的結構相吻合:一個 React 根組件由很多子組件構成。這就是說,子組件與子 Reducer 完全可以對應。

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

import { combineReducers } from 'redux';

const chatReducer = combineReducers({
  chatLog,
  statusMessage,
  userName
})

export default todoApp;

上面的代碼通過combineReducers方法將三個子 Reducer 合并成一個大的函數。

這種寫法有一個前提,就是 State 的屬性名必須與子 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)
  }
}

總之,combineReducers()做的就是產生一個整體的 Reducer 函數。該函數根據 State 的 key 去執行相應的子 Reducer,并將返回結果合并成一個大的 State 對象。

下面是combineReducer的簡單實現。

const combineReducers = reducers => {
  return (state = {}, action) => {
    return Object.keys(reducers).reduce(
      (nextState, key) => {
        nextState[key] = reducers[key](state[key], action);
        return nextState;
      },
      {} 
    );
  };
};

你可以把所有子 Reducer 放在一個文件里面,然后統一引入。

import { combineReducers } from 'redux'
import * as reducers from './reducers'

const reducer = combineReducers(reducers)

工作流程

下面對 Redux 的工作流程,做一個梳理。


image.png
  1. 用戶發出 Action。
store.dispatch(action);
  1. Store 自動調用 Reducer,并且傳入兩個參數:當前 State 和收到的 Action。 Reducer 會返回新的 State 。
let nextState = todoApp(previousState, action);
  1. State 一旦有變化,Store 就會調用監聽函數。
// 設置監聽函數
store.subscribe(listener);
  1. listener可以通過store.getState()得到當前狀態。如果使用的是 React,這時可以觸發重新渲染 View。
function listerner() {
  let newState = store.getState();
  component.setState(newState);   
}

實例:計數器

下面我們來看一個最簡單的實例。

const Counter = ({ value }) => (
  <h1>{value}</h1>
);

const render = () => {
  ReactDOM.render(
    <Counter value={store.getState()}/>,
    document.getElementById('root')
  );
};

store.subscribe(render);
render();

上面是一個簡單的計數器,唯一的作用就是把參數value的值,顯示在網頁上。Store 的監聽函數設置為render,每次 State 的變化都會導致網頁重新渲染。

下面加入一點變化,為Counter添加遞增和遞減的 Action。

const Counter = ({ value, onIncrement, onDecrement }) => (
  <div>
  <h1>{value}</h1>
  <button onClick={onIncrement}>+</button>
  <button onClick={onDecrement}>-</button>
  </div>
);

const reducer = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT': return state + 1;
    case 'DECREMENT': return state - 1;
    default: return state;
  }
};

const store = createStore(reducer);

const render = () => {
  ReactDOM.render(
    <Counter
      value={store.getState()}
      onIncrement={() => store.dispatch({type: 'INCREMENT'})}
      onDecrement={() => store.dispatch({type: 'DECREMENT'})}
    />,
    document.getElementById('root')
  );
};

render();
store.subscribe(render);

完整的代碼請看這里

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

推薦閱讀更多精彩內容

  • 技術棧: react + redux + webpack + react-router + ES6/7/8 + i...
    黃昏少年閱讀 3,087評論 0 19
  • Actions Actions是用于存放數據的載體,通過store.dispatch()函數來將數據從app發送到...
    放風箏的小小馬閱讀 797評論 0 1
  • Redux的三大原則 Redux 可以用這三個基本原則來描述: 單一數據源 整個應用的 state被儲存在一棵 o...
    Dabao123閱讀 684評論 0 2
  • 什么是redux? Redux官方文檔對Redux的定義:Redux是針對JavaScript應用的可預測狀態容器...
    orange_9706閱讀 505評論 0 1
  • 學習必備要點: 首先弄明白,Redux在使用React開發應用時,起到什么作用——狀態集中管理 弄清楚Redux是...
    賀賀v5閱讀 8,934評論 10 58