1. 程式人生 > >Redux 入門教程,應用的狀態管理器

Redux 入門教程,應用的狀態管理器

http://www.jianshu.com/p/d296a8c34936?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io

ReduxFlux演變而來,提供幾個簡單的API來實現狀態管理,所謂狀態指的是應用資料,所以,Redux本質上是用來管理資料的。
進一步,Redux配合支援資料繫結的檢視庫使用,就可以將應用狀態和檢視一一對應,開發者不需要再去關心DOM操作,只關心如何組織資料即可。

由於Redux對於資料的管理拆分很細,一時間會有很多概念,並且Redux有自己豐富的生態,所以容易眼花繚亂。
所以強烈建議從頭開始一步一步的來,深入體驗並理解Redux的思想,不要步子邁太大。
✦ 不要一開始過多的糾結程式碼放在哪個目錄
✦ 不要一開始就想對action

reducer的程式碼做精簡
✦ 不要一開始就考慮資料快取,離線資料等問題
✦ 不要一開始就過度設計資料,考慮資料扁平化的問題
反正一句話,飯要一口一口的吃,路要一步一步的走,Redux對於狀態管理的東西拆得太細,需要多花一些時間去體會。

Redux是什麼?

Redux其實很簡單,總結起來就三句話:
✦ 將整個應用的state儲存在唯一的store物件中。
✦ state只能通過觸發action來修改,其中action就是一個描述性的普通物件。
✦ 使用reducer來描述action如何改變state。

是的,簡而言之就是:Redux讓應用的資料被集中管理,並且只能通過觸發action

的方式來修改,而具體如何修改state,是由reducer來決定的。

那麼問題來了:
✦ store是什麼鬼?
✦ action是什麼鬼?
✦ reducer是什麼鬼?
✦ 最重要的是,為啥要使用Redux,它能給我們帶什麼什麼好處?或者說,引入這麼一個狀態理器到底有啥用?

接下來,我們先捉這三隻鬼。

store是什麼鬼?

前面提過,Redux的目的就是為了對應用資料進行集中管理,也就是state,而state是個普通物件。為了防止state被不小心更新,Redux建立了store物件,專門用來管理state資料。

所以,store就是state的守門員,管理並維護應用資料。

建立store

我們通過createStore(reducer, [initialState], enhancer)的方式來建立store。需要注意的是,應用中應該有且只有一個store。

import { createStore } from 'redux'

// 這是reducer,後文會詳細介紹
function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([ action.text ])
    default:
      return state
  }
}

// 建立store,並且給state一個初始值['HTML']
let store = createStore(todos, [ 'HTML' ])

// state.dispatch(),最常用的API
// 修改state的唯一方式就是呼叫store.dispatch()方法
// 顯然,其中的描述性物件
// {
//  type: 'ADD_TODO',
//  text: 'CSS'
// }
// 就是action
store.dispatch({
  type: 'ADD_TODO',
  text: 'CSS'
})

// store.getState(),另一個常用的API
// 用來獲取state的值
console.log(store.getState());  // [ 'HTML', 'CSS' ]

store的API們

store的API很簡單,這兒我按重要順序列出所有的API,主要記住前兩個。
✦ dispatch(action):觸發action,再次宣告,這是改變state的唯一方式,請默唸兩次
✦ getState():獲取當前的state
✦ subscribe(listener):註冊一個監聽函式,state發生變化時觸發
✦ replaceReducer(nextReducer):替換reducer,用得較少

總結一下,store提供了簡單的API,用來管理應用內的資料,它限制了只能通過dispatch(action)來修改state,那麼這個action是什麼呢?

action是什麼鬼?

前文提過,action就是一個描述性的普通物件,所以它非常簡單!說白了,就是一坨資料,然後這坨資料有名字。

action

action是一個描述性的普通物件。推薦如下的action結構,type是action的名稱,payload是附帶的資料。

{
    // 顯然,這個名字取得很淺顯易懂
    type: UPDATE_ARTICLES_LIST,
    payload: {
        articles: articles,
        lastkey: lastkey
    }
}

值得注意的是:實際專案中,我們應該儘量減少action中附帶的資料,比如想要更新某篇文章的標題,我們只需要攜帶文章id和文章新標題即可,而不需要攜帶整個新文章欄位。
為了讓action更便於維護,我們通常使用action creator而不是action。

action creator

action create就是一個簡單的函式,直接將action作為返回值。

// action creator,返回一個action
// 除此之外,沒有其他的動作
function updateArticlesList(normalizeData, lastkey) {
    return {
        type: UPDATE_ARTICLES_LIST,
        payload: {
            normalizeData: normalizeData,
            listLastkey: lastkey
        }
    }
}

// 通過dispatch觸發一個action,這是我們修改state的唯一方式
dispatch(updateArticlesList(
    normalizeData,
    lastkey
));

// 將dispatch(action)整個動作取個別名,方便呼叫
const updatePosts = (normalizeData, lastkey) => {
    return dispatch(updateArticlesList(
        normalizeData,
        lastkey
    ));
}
updatePosts(...);

那麼為什麼需要action creatore呢?
試想一個場景,我們有好幾處dispatch(action),現在突然想要修改這個action的定義,那麼我們需要修改所有地方,程式碼也比較冗餘!
而使用action creator,相當於對action做了簡單的封裝,避免了這些問題。既靈活又便於維護!

非同步action creator

我們已經知道,修改state的唯一方式就是觸發action,也就是dispatch(action)
但是如果是非同步操作,比如一個網路請求,我們需要等到請求返回之後才會返回action,怎麼辦呢?

function updateArticlesList() {
    return GET(url).then(function(res) {
        // 難道直接return action?
        // 顯然是不行的,這兒的返回值並不是updateArticlesList函式的返回值
        return action;
    }).catch(function(err) {
        console.log(err);
    });
}

對於非同步場景,我們的解決方案是返回函式而不是直接返回action。就像下面這樣。
為了讓dispatch方法可以接受函式作為引數,我們需要使用redux-thunk這個中介軟體。

import thunk from 'redux-thunk';
import { rootReducer } from './reducer.js';

const store = createStore(
    rootReducer,
    applyMiddleware(thunk)
);

然後你就可以dispatch一個函數了

function fetchArticlesList() {
    // 傳入dispatch/getstate,當然是為了獲取state以及更新state
    return (dispatch, getState) => {
        return GET(url).then(function(res) {
            dispatch(updateArticlesList(
                normalizeData,
                lastkey
            ));
        }).catch(function(err) {
            console.log(err);
        });
    }
}

看起來有點迷糊?其實就是把非同步請求抽象成action creator,然後放到了redux的程式碼中。
試想一下,如果沒有這種方式,你會怎麼去處理非同步請求?
是不是會在元件或者頁面中去發非同步請求,然後在回撥函式中dispatch(action)更新state。本質上也沒太大區別。但是好處卻是很明顯的。

稍微提一下,如果我們可以使用async/await的話,非同步action creator可以長得和同步action creator差不多。

action就是一坨資料,它並沒有告訴Redux應該怎麼去更新state,接下來介紹的reducer就是負責如何更新state這個工作的。

reducer是什麼鬼?

action本身沒有任何意義,就是一個描述性的普通物件。它並沒有說明這個資料應該如何更新state。
具體如何更新state,是由reducer決定的。reducer的核心就一行程式碼:(state, action) => newstate

// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
    [UPDATE_ARTICLES_DETAIL]: (articles, action) => articles,
    [UPDATE_ARTICLES_LIST]: (articles, action) => {
        let payload = action.payload,
            normalizeData = payload.normalizeData,
            list = articles.list.concat(normalizeData.result),
            listLastkey = payload.listLastkey;

        // 更新articles.list欄位和articles.lastkey欄位
        // 這兒為什麼不是state,而是articles呢?留著後文介紹
        return updateObject(articles, {
            list,
            listLastkey
        });
    }
}

// ------------------------------------
// Reducer
// ------------------------------------
export function articlesReducer(articles = {
    list: [],
    listLastkey: 0
}, action) {
    const handler = ACTION_HANDLERS[action.type]

    return handler ? handler(articles, action) : articles
}

reducer函式應該是純函式,它要保證:只要傳入引數相同,那麼返回的新state就一定相同。
所以永遠不要再reducer中做如下操作:
✦ 修改傳入的state引數
✦ 執行有副作用的操作,比如API請求,路由跳轉等
✦ 呼叫非純函式,比如Math.random()或Date.now()

而一旦state變得複雜、層級較多的時候,如何設計reducer就是一個比較複雜的話題了。
關於如何設計state?如何分拆reducer?reducer之間如何共享資料?以及如何重構reducer的程式碼?可以移步另一篇部落格:如何最佳實踐的設計reducer

那麼,回到最初的話題,引入Redux到我們的應用中,到底有什麼好處?我們為什麼需要一個專門的狀態管理器?

為啥要使用redux?

早些時候,前端並沒有這麼複雜,幾乎不怎麼涉及資料管理。
隨著前端的發展,前端也開始引入MVC之類的架構,對資料、檢視、邏輯進行拆分處理。為了保持資料和檢視的同步,我們會頻繁的操作DOM元素。簡直是噩夢。
而後KnockoutJS,angularJS等出現了,他們都支援資料繫結,終於讓開發可以不在頻繁的操作DOM,而是僅僅修改資料,然後自動同步到view。
但這還不夠徹底,資料仍然是分散的。我們會在controller中寫很多操作資料、操作檢視的程式碼,甚至存在冗餘資料,想要修改、更新、同步的話,有很大的隱患。
另外,Redux還讓前後端徹底分離變成了可能,這一點也有極大的意義。

Redux的資料流

Redux通過一些限制告訴你:資料只能儲存在我這兒,別想太分散!想要修改資料?告訴我一個帶新資料的action,我會通過reducer自動修改,然後返回修改後的資料給你!
是的,redux很想“資料庫”,資料被集中儲存,並且只能通過“預先定義的action操作”來修改。

更厲害的是,配上支援資料繫結的檢視庫,你會發現一個神奇的事情:
之前我們是面向view和controller程式設計,隨著專案的複雜,程式碼會彼此影響而且資料會分散到各處。
而引入redux之後,我們單純的面向資料程式設計即可,我們在Redux中統一的管理資料,然後資料變換會反映到view上,而資料上的互動,本質上也是觸發了Redux中的action。如下圖


Redux資料流

所以,設計redux程式的時候,提前想清楚state的結構尤其重要,就好比設計資料庫表結構之於後臺。

伺服器渲染讓前後端徹底分離成為了可能

上圖也可以看出,Redux構建出一份單向資料流。這讓服務端渲染變成了可能,而這個特性,讓前後端徹底分離變成了可能,還不用擔心SEO的問題。
想當初,為了解決前後端分離的問題,大家費盡心思,奈何進展甚微,淘寶甚至提出中途島midway專案,通過中間搭建由前端維護的Nodejs伺服器來實現簡單的渲染然後返回HTML,但其實這個Nodejs伺服器一點都不簡單,需要考慮太多東西,比如安全、效能、快取等。

總結說點啥?

Redux主要用於對資料進行集中管理,並且讓整個應用的資料流變得清晰。讓應用開發更流暢,資料管理更有效。有了Redux,開發者們慢慢的轉化為面向資料程式設計,而不再是頻繁的操作DOM,維護越來越複雜的controller邏輯。
簡單來說,Redux的東西不多,更重要的是理解它的思路:
✦ 將整個應用的state儲存在唯一的store物件中。
✦ state只能通過觸發action來修改,其中action就是一個描述性的普通物件。
✦ 使用reducer來描述action如何改變state。
✦ Redux的單向資料流,可以實現服務端渲染,讓前後端徹底分離成為可能,這個有里程碑的意義。
✦ Redux非常適合複雜的應用,尤其是多互動、多資料來源的應用。

還是那句話,Redux將資料管理拆得很細,所以會有很多新東西去了解,但其實只要瞭解它的思想,其他的就很順其自然了。