Redux開發(fā)實用教程

為了幫助大家快速上手什么是Redux開發(fā)忠蝗,在這本節(jié)中將向大家介紹什么是Redux開發(fā)所需要的一些什么是Redux必備基礎以及高級知識择膝。

什么是Redux翔冀?

Redux 是 JavaScript 狀態(tài)容器铐然,提供可預測化的狀態(tài)管理涝滴,可以讓你構建一致化的應用,運行于不同的環(huán)境(客戶端鲤拿、服務器假褪、原生應用),并且易于測試近顷。

我們過下整個工作流程:

  1. 用戶(操作View)發(fā)出Action生音,發(fā)出方式就用到了dispatch方法;
  2. 然后窒升,Store自動調用Reducer缀遍,并且傳入兩個參數(當前State和收到的Action),Reducer會返回新的State饱须,如果有Middleware域醇,Store會將當前State和收到的Action傳遞給Middleware,Middleware會調用Reducer 然后返回新的State蓉媳;
  3. State一旦有變化歹苦,Store就會調用監(jiān)聽函數,來更新View督怜;

到這兒為止殴瘦,一次用戶交互流程結束『鸥埽可以看到蚪腋,在整個流程中數據都是單向流動的。

Redux和Flux的對比

Redux是Flux思想的一種實現姨蟋,同時又在其基礎上做了改進屉凯。Redux秉承了Flux單向數據流、Store是唯一的數據源的思想眼溶。

  • Redux中沒有Dispatcher:它使用Store的Store.dispatch()方法來把action傳給Store悠砚,由于所有的action處理都會經過這個Store.dispatch()方法,所以在Redux中很容易實現Middleware機制堂飞。Middleware可以讓你在reducer執(zhí)行前與執(zhí)行后進行攔截并插入代碼灌旧,來達到操作action和Store的目的绑咱,這樣一來就很容易實現靈活的日志打印、錯誤收集枢泰、API請求描融、路由等操作。
  • Redux只有一個Store:Flux中允許有多個Store衡蚂,但是Redux中只允許有一個窿克,相較于多個Store的Flux,一個Store更加清晰毛甲,并易于管理年叮;

Redux和Flux的最大不同是Redux沒有 Dispatcher 且不支持多個 store。Redux只有一個單一的 store 和一個根級的 reduce 函數(reducer)玻募,隨著應用不斷變大谋右,我們需要將根級的 reducer 拆成多個小的 reducers,分別獨立地操作 state 樹的不同部分补箍,而不是添加新的 stores。

Redux優(yōu)點

  • 可預測:
    始終有一個唯一的準確的數據源(single source of truth)就是store啸蜜,通過actions和reducers來保證整個應用狀態(tài)同步坑雅,做到絕不混亂
  • 易維護:
    具備可預測的結果和嚴格的組織結構讓代碼更容易維護
  • 易測試:
    編寫可測試代碼的首要準則是編寫可以僅做一件事并且獨立的小函數(single responsibility principle),Redux的代碼幾乎全部都是這樣的函數:短小·純粹·分離

為什么要用Reudx?

隨著 JavaScript 應用越來越大衬横,越來越復雜裹粤,我們需要管理的state變得越來越多。 這些 state 可能包括服務器響應蜂林、緩存數據遥诉、本地生成尚未持久化到服務器的數據,也包括 UI 狀態(tài)噪叙,如激活的路由矮锈,被選中的標簽,是否顯示加載動效或者分頁器等等睁蕾。

管理不斷變化的 state 非常困難苞笨。如果一個 model 的變化會引起另一個 model 變化,那么當 view 變化時子眶,就可能引起對應 model 以及另一個 model 的變化瀑凝,依次地,可能會引起另一個 view 的變化臭杰。直至你搞不清楚到底發(fā)生了什么粤咪。state 在什么時候,由于什么原因渴杆,如何變化已然不受控制寥枝。 當系統變得錯綜復雜的時候宪塔,想重現問題或者添加新功能就會變得非常復雜。

雖然React 試圖在視圖層禁止異步和直接操作 DOM 來解決這個問題脉顿。美中不足的是蝌麸,React 依舊把處理 state 中數據的問題留給了你。Redux就是為了幫你解決這個問題艾疟。

Redux 的三個基本原則

  • 單一數據源:整個應用的 state 被儲存在一棵 object tree 中来吩,并且這個 object tree 只存在于唯一一個 store 中;
  • State 是只讀的:唯一改變 state 的方法就是觸發(fā) action蔽莱,action 是一個用于描述已發(fā)生事件的普通對象弟疆;
  • 使用純函數來執(zhí)行修改:為了描述 action 如何改變 state tree ,你需要編寫 reducers盗冷;

Redux有那幾部分構成怠苔?

  • action:action就是一個描述發(fā)生什么的對象;
  • reducer:形式為 (state, action) => state 的純函數仪糖,功能是根據action 修改state 將其轉變成下一個 state柑司;
  • store:用于存儲state,你可以把它看成一個容器锅劝,整個應用只能有一個store攒驰。

Redux應用中所有的 state 都以一個對象樹的形式儲存在一個單一的 store 中。 惟一改變 state 的辦法是觸發(fā) action故爵,action就是一個描述發(fā)生什么的對象玻粪。 為了描述 action 如何改變 state 樹,你需要編寫 reducers诬垂。

先看一個redux的簡單使用例子:

import { createStore } from 'redux';

// 創(chuàng)建Redux reducer
/**
 * 這是一個 reducer劲室,形式為 (state, action) => state 的純函數。
 * 描述了 action 如何把 state 轉變成下一個 state结窘。
 *
 * state 的形式取決于你很洋,可以是基本類型、數組隧枫、對象,
 * 當 state 變化時需要返回全新的對象蹲缠,而不是修改傳入的參數。
 *
 * 下面例子使用 `switch` 語句和字符串來做判斷悠垛,但你可以寫幫助類(helper)
 * 根據不同的約定(如方法映射)來判斷线定,只要適用你的項目即可。
 */
function counter(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1;
  case 'DECREMENT': 
    return state - 1;
  default:
    return state;
  }
}

// 創(chuàng)建 Redux store 來存放應用的狀態(tài)确买。
// API 是 { subscribe, dispatch, getState }斤讥。
let store = createStore(counter);

// 可以手動訂閱更新,也可以事件綁定到視圖層。
store.subscribe(() =>
  console.log(store.getState())
);

// 改變內部 state 惟一方法是 dispatch 一個 action芭商。
// action 可以被序列化派草,用日記記錄和儲存下來,后期還可以以回放的方式執(zhí)行
store.dispatch({ type: 'INCREMENT' });
// 1
store.dispatch({ type: 'INCREMENT' });
// 2
store.dispatch({ type: 'DECREMENT' });
// 1

以上代碼便是一個redux的最簡單的使用铛楣,接下來我們來分別介紹一下redux的三大組成部分:action近迁、reducer以及store。

action

Action 是把數據從應用傳到 store 的有效載荷簸州。它是 store 數據的唯一來源鉴竭,也就是說要改變store中的state就需要觸發(fā)一個action。

Action 本質上一個普通的JavaScript對象岸浑。action 內必須使用一個字符串類型的 type 字段來表示將要執(zhí)行的動作搏存,除了 type 字段外,action 對象的結構完全由你自己決定矢洲。多數情況下璧眠,type 會被定義成字符串常量。當應用規(guī)模越來越大時读虏,建議使用單獨的模塊或文件來存放 action责静。

import { ADD_TODO, REMOVE_TODO } from '../actionTypes'

//action
{
  type: ADD_TODO,
  text: 'Build my first Redux app'
} 

提示:使用單獨的模塊或文件來定義 action type 常量并不是必須的,甚至根本不需要定義盖桥。對于小應用來說灾螃,使用字符串做 action type 更方便些。不過葱轩,在大型應用中把它們顯式地定義成常量還是利大于弊的。

Action 創(chuàng)建函數

Action 創(chuàng)建函數 就是生成 action 的方法藐握。“action” 和 “action 創(chuàng)建函數” 這兩個概念很容易混在一起靴拱,使用時最好注意區(qū)分

在 Redux 中的 action 創(chuàng)建函數只是簡單的返回一個 action:

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

這樣做將使 action 創(chuàng)建函數更容易被移植和測試猾普。

reducer

reducer是根據action 修改state 將其轉變成下一個 state袜炕,記住 actions 只是描述了有事情發(fā)生了這一事實,并沒有描述應用如何更新 state初家。

(previousState, action) => newState

保持 reducer 純凈非常重要偎窘。永遠不要在 reducer 里做這些操作:

  • 修改傳入參數;
  • 執(zhí)行有副作用的操作溜在,如 API 請求和路由跳轉陌知;
  • 調用非純函數,如 Date.now() 或 Math.random()掖肋。

提示:reducer 是純函數仆葡。它僅僅用于計算下一個 state。它應該是完全可預測的:多次傳入相同的輸入必須產生相同的輸出志笼。它不應做有副作用的操作沿盅,如 API 調用或路由跳轉把篓。這些應該在 dispatch action 前發(fā)生。

//reducer
function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    default:
      return state
  }
}

提示:

  • 不要修改 state腰涧。 使用 Object.assign() 新建了一個副本韧掩。不能這樣使用 Object.assign(state, { visibilityFilter: action.filter }),因為它會改變第一個參數的值窖铡。你必須把第一個參數設置為空對象疗锐。你也可以開啟對ES7提案對象展開運算符的支持, 從而使用 { ...state,visibilityFilter: action.filter } 達到相同的目的。
  • 在 default 情況下返回舊的 state万伤。遇到未知的 action 時窒悔,一定要返回舊的 state。

拆分與合并Reducer

function onAction(state = defaultState, action) {
    switch (action.type) {
        case Types.THEME_CHANGE://主題
            return {
                ...state,
                theme: action.theme,
            };
        case Types.SHOW_THEME_VIEW://主題
            return {
                ...state,
                customThemeViewVisible: action.customThemeViewVisible,
            };
        case Types.SORT_LANGUAGE://排序
            return Object.assign({}, state, {
                checkedArray: action.checkedArray,
            });
        case Types.REFRESH_ABOUT://關于
            return Object.assign({}, state, {
                [action.flag]: {
                    ...state[action.flag],
                    projectModels: action.projectModels,
                }
            });
        case Types.ABOUT_SHOW_MORE://關于
            return Object.assign({}, state, {
                me: {
                    ...state.me,
                    [action.menuFlag]: action.menuShow
                }
            });
        default:
            return state;
    }
}

上述代碼看起來有些冗長敌买,并且主題简珠、排序、關于的更新看起來是相互獨立的虹钮,能不能將他們拆到單獨的函數或文件里呢聋庵,答案是可以的。

拆分

//主題 theme.js
export default function onTheme(state = defaultState, action) {
    switch (action.type) {
        case Types.THEME_CHANGE:
            return {
                ...state,
                theme: action.theme,
            };
        case Types.SHOW_THEME_VIEW:
            return {
                ...state,
                customThemeViewVisible: action.customThemeViewVisible,
            };
        default:
            return state;
    }
}

//排序 sort.js
export default function onSort(state = defaultState, action) {
    switch (action.type) {
        case Types.SORT_LANGUAGE:
            return Object.assign({}, state, {
                checkedArray: action.checkedArray,
            });
        default:
            return state;
    }
}

//關于 about.js
export default function onAbout(state = defaultState, action) {
    switch (action.type) {
        case Types.REFRESH_ABOUT:
            return Object.assign({}, state, {
                [action.flag]: {
                    ...state[action.flag],
                    projectModels: action.projectModels,
                }
            });
        case Types.ABOUT_SHOW_MORE:
            return Object.assign({}, state, {
                me: {
                    ...state.me,
                    [action.menuFlag]: action.menuShow
                }
            });
        default:
            return state;
    }
}

在上述代碼中芙粱,我們將對主題祭玉、排序、關于的操作拆到了單獨的函數中并放到了不同的文件里春畔,這樣以來各個模塊的操作就更加的聚合了,代碼看起來也就更加的簡潔明了律姨。

合并reducer

經過上述的步驟我們將一個大的reducer拆分成了不同的小的reducer振峻,但redux原則是只允許一個根reducer,接下來我們需要將這幾個小的reducer聚合到一個跟reducer中择份。

這里我們需要用到Redux 提供的combineReducers(reducers)扣孟。

import {combineReducers} from 'redux'
import theme from './theme'
import sort from './sort'
import about from './about'

const index = combineReducers({
    theme: theme,
    sort: sort,
    about: about,
})
export default index;

combineReducers() 所做的只是生成一個函數,這個函數來調用你的一系列 reducer荣赶,每個 reducer 根據它們的 key 來篩選出 state 中的一部分數據并處理扯饶,然后這個生成的函數再將所有 reducer 的結果合并成一個大的對象匹耕。沒有任何魔法。正如其他 reducers,如果 combineReducers() 中包含的所有 reducers 都沒有更改 state梦鉴,那么也就不會創(chuàng)建一個新的對象像街。

Store

是存儲state的容器澳骤,Store 會把兩個參數(當前的 state 樹和 action)傳入 reducer膛虫。

store 有以下職責:

  • 維持應用的 state;
  • 提供 getState() 方法獲取 state;
  • 提供 dispatch(action) 方法更新 state:我們可以在任何地方調用 store.dispatch(action)氛改,包括組件中帐萎、XMLHttpRequest 回調中、甚至定時器中胜卤;
  • 通過 subscribe(listener) 注冊監(jiān)聽器;
  • 通過 subscribe(listener) 返回的函數注銷監(jiān)聽器疆导。

在前一個章節(jié)中,我們使用 combineReducers() 將多個 reducer 合并成為一個「瘐铮現在我們通過Redux的 createStore()來創(chuàng)建一個Store澈段。

import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp)

高級

異步Action

我們上文中所講的Action都是基于同步實現的,那么對于網絡請求數據庫加載等應用場景同步Action顯然是不適用的舰攒,對此我們需要用到異步Action败富。

我們可將異步Action簡答理解為:在Action中進行異步操作等操作返回后再dispatch一個action。

為了使用異步action我們需要引入redux-thunk庫摩窃,redux-thunk是為Redux提供異步action支持的中間件兽叮。

使用redux-thunk

npm install --save redux-thunk

import thunk from 'redux-thunk'
let middlewares = [
    thunk
]
//添加異步中間件redux-thunk
let createAppStore = applyMiddleware(...middlewares)(createStore)

創(chuàng)建異步action

export function onSearch(inputKey, token, popularKeys) {
    return dispatch => {
        dispatch({type: Types.SEARCH_REFRESH});
        fetch(genFetchUrl(inputKey)).then(response => {//如果任務取消,則不做任何處理
            return checkCancel(token) ? response.json() : null;
        }).then(responseData => {
            if (!checkCancel(token, true)) {//如果任務取消猾愿,則不做任何處理
                return
            }
            if (!responseData || !responseData.items || responseData.items.length === 0) {
                dispatch({type: Types.SEARCH_FAIL, message: inputKey + '什么都沒找到'});
                return
            }
            let items = responseData.items;
            getFavoriteKeys(inputKey, dispatch, items, token, popularKeys);
        }).catch(e => {
            console.log(e);
            dispatch({type: Types.SEARCH_FAIL, error: e});
        })
    }
}

異步數據流

默認情況下鹦聪,createStore() 所創(chuàng)建的 Redux store 沒有使用 middleware,所以只支持 同步數據流蒂秘。

你可以使用 applyMiddleware() 來增強 createStore()泽本。它可以幫助你用簡便的方式來描述異步的 action。

像 redux-thunk 或 redux-promise 這樣支持異步的 middleware 都包裝了 store 的 dispatch() 方法姻僧,以此來讓你 dispatch 一些除了 action 以外的其他內容规丽,例如:函數或者 Promise。你所使用的任何 middleware 都可以以自己的方式解析你 dispatch 的任何內容撇贺,并繼續(xù)傳遞 actions 給下一個 middleware赌莺。比如,支持 Promise 的 middleware 能夠攔截 Promise显熏,然后為每個 Promise 異步地 dispatch 一對 begin/end actions雄嚣。

當 middleware 鏈中的最后一個 middleware 開始 dispatch action 時晒屎,這個 action 必須是一個普通對象喘蟆;

總結

  • Redux 應用只有一個單一的 store。當需要拆分數據處理邏輯時鼓鲁,你應該使用 reducer 組合 而不是創(chuàng)建多個 store蕴轨;
  • redux一個特點是:狀態(tài)共享,所有的狀態(tài)都放在一個store中骇吭,任何component都可以訂閱store中的數據橙弱;
  • 并不是所有的state都適合放在store中,這樣會讓store變得非常龐大,如某個狀態(tài)只被一個組件使用棘脐,不存在狀態(tài)共享斜筐,可以不放在store中;

未完待續(xù)

參考

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末蛀缝,一起剝皮案震驚了整個濱河市顷链,隨后出現的幾起案子,更是在濱河造成了極大的恐慌屈梁,老刑警劉巖嗤练,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異在讶,居然都是意外死亡煞抬,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門构哺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來革答,“玉大人,你說我怎么就攤上這事遮婶』人椋” “怎么了?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵旗扑,是天一觀的道長蹦骑。 經常有香客問我,道長臀防,這世上最難降的妖魔是什么眠菇? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮袱衷,結果婚禮上捎废,老公的妹妹穿的比我還像新娘。我一直安慰自己致燥,他們只是感情好登疗,可當我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著嫌蚤,像睡著了一般辐益。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上脱吱,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天智政,我揣著相機與錄音,去河邊找鬼箱蝠。 笑死续捂,一個胖子當著我的面吹牛垦垂,可吹牛的內容都是我干的。 我是一名探鬼主播牙瓢,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼劫拗,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了矾克?” 一聲冷哼從身側響起杨幼,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎聂渊,沒想到半個月后差购,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡汉嗽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年欲逃,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片饼暑。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡稳析,死狀恐怖,靈堂內的尸體忽然破棺而出弓叛,到底是詐尸還是另有隱情彰居,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布撰筷,位于F島的核電站陈惰,受9級特大地震影響,放射性物質發(fā)生泄漏毕籽。R本人自食惡果不足惜抬闯,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望关筒。 院中可真熱鬧溶握,春花似錦、人聲如沸蒸播。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽袍榆。三九已至胀屿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蜡塌,已是汗流浹背碉纳。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工勿负, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留馏艾,地道東北人劳曹。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像琅摩,于是被迫代替她去往敵國和親铁孵。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內容