Redux 概要教程

YaHuiLiang(Ryou)發表於2018-07-11

Redux

Redux 是一個面向 JavaScript 應用的狀態管理工具。它可以幫助我們寫出更清晰,更容易測試的程式碼,並且可以使用在任何不同的環境下。Redux 是 Flux 的一種實現,它簡化了 Flux 繁瑣的 store 而採用單一資料來源的方式,大大減小了狀態管理的複雜度。相比 Flux 更容易被大家接受。

你可以在 React 中使用。與其它 JavaScript 一起使用也是可以的。它是與框架無關的。

Note: 面試送命題,被問到 Vuex 和 Redux 哪個好?這個真的是送命題,尤其是遇到那種主觀技術傾向嚴重的面試官。比如偏好 Vue 或者 React 的,畢竟 Redux 的用法繁瑣,需要多寫很多程式碼而被國人所詬病。但是很多人卻沒有看到 Redux 使程式碼結構更清晰。 Note: 之前發表在掘金的 Redux 原始碼解析

Redux 包含 reducers,middleware,store enhancers,但是卻非常簡單。如果你之前構建過 Flux 應用,那麼對於你來說就更簡單了。即使你沒有使用過 Flux,依然也是很簡單的。

Actions

Actions 是應用程式將資料傳送到 store 的載體。可以通過 store.dispatch 來將 action 傳送到 store 中。

下面是幾個例子:

  const ADD_TODO = 'ADD_TODO';

  {
    type: ADD_TODO,
    text: 'Build my first Redux app'
  }
複製程式碼

Actions 是一個原生 JavaScript 物件,並且必須帶有一個 type 屬性作為識別行為的標示。type 是一個靜態字串。如果你的應用很龐大,那麼你需要把他們移到一個模組中:

  import { ADD_TODO, REMOVE_TODO } from '../actionTypes'
複製程式碼

Action Creators

Action Creators 是用於建立 action 物件的函式:

  function addTodo(text) {
    return {
      type: ADD_TODO,
      text
    }
  }
複製程式碼

在一些複雜應用中,我們還需要在 Action Creator 中派發其它 action:

  function addTodoWithDispatch(text) {
    const action = {
      type: ADD_TODO,
      text
    }
    dispatch(action)
  }
複製程式碼

當然還有更復雜的,直接派發一個 Action Creator:

  dispatch(addTodo(text))
  dispatch(completeTodo(index))
複製程式碼

Reducers

Reducers 用於根據接受的 action 物件,對 store 內的資料進行相應的處理,action 只描述發生了什麼,並不描述應用程式的狀態改變,改變發生在 reducer 中。

在 Redux 中,所有的狀態儲存在一個單一物件中,在某些複雜的應用中,你需要設計複雜的實體。我們建議你保持你的狀態儘可能普通,不要巢狀。保持每一個實體和一個 ID 這樣的 key 關聯。然後用 ID 在其它實體中訪問這個實體。把應用的狀態想象成資料庫。

Reducer 必須是一個純函式,它的引數是之前的狀態和接收的 action,然後返回一個新的狀態物件。

  (previousState, action) => newState
複製程式碼

之所以叫做 reducer 是因為它被作為一種函式被傳入到 Array.prototype.reduce(reducer, ?initialValue)。這是保持 reducer 是一個純函式是非常重要的。不要在裡面做下面的事情:

  • 改變引數
  • 執行 API 請求,或者路由切換
  • 呼叫非純函式,比如 Date.now()

下面的程式碼將會是一個非常簡單的 reducer 實現:

  import { VisibilityFilters } from './actions'const initialState = {
    visibilityFilter: VisibilityFilters.SHOW_ALL,
    todos: []
  }
  ​
  function todoApp(state, action) {
    if (typeof state === 'undefined') {
      return initialState
    }
  ​
    // For now, don't handle any actions
    // and just return the state given to us.
    return state
  }
複製程式碼

我們必須在 reducer 中處理完 action 後建立一個新的 state 並作為返回值。像下面這樣:

  { ...state, ...newState }
複製程式碼

reducer 在預設情況下或者遇到未知 action 的時候,需要返回傳入的 state 。

  import {
    ADD_TODO,
    TOGGLE_TODO,
    SET_VISIBILITY_FILTER,
    VisibilityFilters
  } from './actions'
  ​
  ...
  ​
  function todoApp(state = initialState, action) {
    switch (action.type) {
      case SET_VISIBILITY_FILTER:
        return Object.assign({}, state, {
          visibilityFilter: action.filter
        })
      case ADD_TODO:
        return Object.assign({}, state, {
          todos: [
            ...state.todos,
            {
              text: action.text,
              completed: false
            }
          ]
        })
      default:
        return state
    }
  }
複製程式碼

就像之前提到的,我們並不是直接操作 state 或者它的屬性,而是返回一個新的物件。

有的時候,我們的系統過於龐大,這樣 reducer 就會變得複雜而龐大。這個時候我們就需要將 reducer 拆分

  function todos(state = [], action) {
    switch (action.type) {
      case ADD_TODO:
        return [
          ...state,
          {
            text: action.text,
            completed: false
          }
        ]
      case TOGGLE_TODO:
        return state.map((todo, index) => {
          if (index === action.index) {
            return Object.assign({}, todo, {
              completed: !todo.completed
            })
          }
          return todo
        })
      default:
        return state
    }
  }
  ​
  function visibilityFilter(state = SHOW_ALL, action) {
    switch (action.type) {
      case SET_VISIBILITY_FILTER:
        return action.filter
      default:
        return state
    }
  }
  ​
  function todoApp(state = {}, action) {
    return {
      visibilityFilter: visibilityFilter(state.visibilityFilter, action),
      todos: todos(state.todos, action)
    }
  }
複製程式碼

每一個 reducer 都只管理屬於自己那部分狀態。而每一個 reducer 返回的狀態都會成為 store 的一部分。這裡我們需要通過 combineReducers() 來將這些 reducer 組合到一起

  import { combineReducers } from 'redux'const todoApp = combineReducers({
    visibilityFilter,
    todos
  })
  ​
  export default todoApp
複製程式碼

Store

Store 就是一堆物件的集合。Store 包含以下功能:

  • 保持應用中的狀態
  • 允許通過 getState 訪問狀態
  • 允許通過 dispatch 更新狀態
  • 註冊訂閱者
  • 取消註冊的訂閱者
  import { createStore } from 'redux'
  import todoApp from './reducers'
  const store = createStore(todoApp)
複製程式碼

createStore 具有一個可選引數,可以初始化 store 中的狀態。這對於部分場景很重要,比如說內建入後端預先處理的資料,直接注入到 store 中,這樣頁面就避免了 ajax 請求的響應時間提升了頁面顯示速度,如果沒有 SEO 要求的話,這種方式是一個成本非常低的提高首屏載入速度的方式,之前我在專案中使用過。

  const store = createStore(todoApp, window.STATE_FROM_SERVER)
複製程式碼

我們可以通過 dispatch 派發 action 物件來改變 store 內部儲存的狀態:

  import {
    addTodo,
    toggleTodo,
    setVisibilityFilter,
    VisibilityFilters
  } from './actions'// Log the initial state
  console.log(store.getState())
  ​
  // Every time the state changes, log it
  // Note that subscribe() returns a function for unregistering the listener
  const unsubscribe = store.subscribe(() =>
    console.log(store.getState())
  )
  ​
  // Dispatch some actions
  store.dispatch(addTodo('Learn about actions'))
  store.dispatch(addTodo('Learn about reducers'))
  store.dispatch(addTodo('Learn about store'))
  store.dispatch(toggleTodo(0))
  store.dispatch(toggleTodo(1))
  store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))
  ​
  // Stop listening to state updates
  unsubscribe()
複製程式碼

Redux 資料流

Redux 遵循嚴格的單向資料流。意味著所有的應用都要遵循相同邏輯來管理狀態,也正因如此,程式碼變得更加清晰,易於維護。並且由於採用單一資料來源。避免了 Flux 複雜而難以管理狀態的問題。但是,會讓開發人員覺得繁瑣。需要定義非常多的 action 和 reducer。

基於 Redux 的應用中,資料的生命週期要遵循一下幾步:

  1. 通過 dispatch 派發 action 物件
  2. store 執行通過 combineReducers 註冊的 reducer,根據 action 的 type 做對應的狀態更新
  3. 通過 combineReducers 組合的 reducers 將所有 reducer 返回的狀態集中到一個狀態樹中
  4. store 將返回的新狀態樹儲存起來

非同步 action

當我們使用一個非同步 api 的時候,一般會有兩個階段:發起請求,收到迴應。

這兩個階段通常會更新應用的狀態,因此你需要 dispatch 的 action 被同步處理。通常,對於 API 請求你希望 dispatch 三個不同的 action:

  • 一個用於告訴 reducer 請求開始的 action (通常會設定一個 isFetching 標誌告知 UI 需要顯示一個載入動畫)
  • 一個用於告訴 reducer 請求成功的 action (這裡我們需要將接收到的資料更新到 store 中,並重置 isFetching)
  • 一個用於告訴 reducer 請求異常的 action (重置 isFetching,更新 store 中一個可以通知 UI 發生錯誤的狀態)
  { type: 'FETCH_POSTS' }
  { type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
  { type: 'FETCH_POSTS', status: 'success', response: { ... } }
複製程式碼

通常,我們需要在非同步開始前和回撥中通過 store.dispatch 來派發這些 action 來告知 store 更新狀態。

Note: 這裡要注意 action 派發的順序。因為非同步的返回時間是無法確定的。所以我們需要藉助 Promise 或者 async/await Generator 來控制非同步流,保證 dispatch 的 action 有一個合理的順序。

同步 action

對於同步 action,我們只需要在 action creator 中返回一個 action 純物件即可。

  export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT'export function selectSubreddit(subreddit) {
    return {
      type: SELECT_SUBREDDIT,
      subreddit
    }
  }
複製程式碼

Async Flow

Redux 僅支援同步的資料流,只能在中介軟體中處理非同步。因此我們需要在 中介軟體中才能處理非同步的資料流。

Redux-Thunk 是一個非常好的非同步 action 處理中介軟體,可以幫我們處理非同步 action 更加方便和清晰。

下面是一個通過 Redux-Thunk 處理非同步 action 的例子:

  import fetch from 'cross-fetch'
  import thunkMiddleware from 'redux-thunk'
  import { createLogger } from 'redux-logger'
  import { createStore, applyMiddleware } from 'redux'
  import { selectSubreddit, fetchPosts } from './actions'
  import rootReducer from './reducers'const loggerMiddleware = createLogger()
  ​
  const store = createStore(
    rootReducer,
    applyMiddleware(
      thunkMiddleware, // lets us dispatch() functions
      loggerMiddleware // neat middleware that logs actions
    )
  )
  ​
  store.dispatch(selectSubreddit('reactjs'))
  store
    .dispatch(fetchPosts('reactjs'))
    .then(() => console.log(store.getState()))
  ​
  export const REQUEST_POSTS = 'REQUEST_POSTS'
  function requestPosts(subreddit) {
    return {
      type: REQUEST_POSTS,
      subreddit
    }
  }
  ​
  export const RECEIVE_POSTS = 'RECEIVE_POSTS'
  function receivePosts(subreddit, json) {
    return {
      type: RECEIVE_POSTS,
      subreddit,
      posts: json.data.children.map(child => child.data),
      receivedAt: Date.now()
    }
  }
  ​
  export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'
  export function invalidateSubreddit(subreddit) {
    return {
      type: INVALIDATE_SUBREDDIT,
      subreddit
    }
  }
  ​
  // Meet our first thunk action creator!
  // Though its insides are different, you would use it just like any other action creator:
  // store.dispatch(fetchPosts('reactjs'))export function fetchPosts(subreddit) {
    // Thunk middleware knows how to handle functions.
    // It passes the dispatch method as an argument to the function,
    // thus making it able to dispatch actions itself.return function (dispatch) {
      // First dispatch: the app state is updated to inform
      // that the API call is starting.
  ​
      dispatch(requestPosts(subreddit))
  ​
      // The function called by the thunk middleware can return a value,
      // that is passed on as the return value of the dispatch method.// In this case, we return a promise to wait for.
      // This is not required by thunk middleware, but it is convenient for us.return fetch(`https://www.reddit.com/r/\${subreddit}.json`)
        .then(
          response => response.json(),
          // Do not use catch, because that will also catch
          // any errors in the dispatch and resulting render,
          // causing a loop of 'Unexpected batch number' errors.
          // https://github.com/facebook/react/issues/6895
          error => console.log('An error occurred.', error)
        )
        .then(json =>
          // We can dispatch many times!
          // Here, we update the app state with the results of the API call.
  ​
          dispatch(receivePosts(subreddit, json))
        )
    }
  }
複製程式碼

中介軟體

在前面,我們看到,我們可以通過中介軟體來完成非同步 action 處理。如果你使用過 express 或者 koa,那麼就更容易理解中介軟體。中介軟體就是一些程式碼,會在接收到請求的時候作出迴應。

Redux 的中介軟體解決的是和 express 或者 koa 完全不同的問題,但是原理上差不多。它提供一種第三方外掛機制,來在 dispatch 和 reducer 之間做一些特殊處理。就像下面這樣:

  const next = store.dispatch
  store.dispatch = function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
複製程式碼

那麼我們如何完成一個自己的中介軟體呢?下面是一個典型的例子:

  // 其中 next 就是 dispatch
  const logger = store => next => action => {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
  ​
  const crashReporter = store => next => action => {
    try {
      return next(action)
    } catch (err) {
      console.error('Caught an exception!', err)
      Raven.captureException(err, {
        extra: {
          action,
          state: store.getState()
        }
      })
      throw err
    }
  }

  // 通過 appliMiddleware 來註冊自己的中介軟體
  import { createStore, combineReducers, applyMiddleware } from 'redux'const todoApp = combineReducers(reducers)
  const store = createStore(
    todoApp,
    // applyMiddleware() tells createStore() how to handle middleware
    applyMiddleware(logger, crashReporter)
  )
複製程式碼

相關文章