Redux 概要教程

Redux

Redux 是一個面向 JavaScript 應用的狀態管理工具。它能夠幫助咱們寫出更清晰,更容易測試的代碼,而且可使用在任何不一樣的環境下。Redux 是 Flux 的一種實現,它簡化了 Flux 繁瑣的 store 而採用單一數據源的方式,大大減少了狀態管理的複雜度。相比 Flux 更容易被你們接受。react

你能夠在 React 中使用。與其它 JavaScript 一塊兒使用也是能夠的。它是與框架無關的。git

Note: 面試送命題,被問到 Vuex 和 Redux 哪一個好?這個真的是送命題,尤爲是遇到那種主觀技術傾向嚴重的面試官。好比偏好 Vue 或者 React 的,畢竟 Redux 的用法繁瑣,須要多寫不少代碼而被國人所詬病。可是不少人卻沒有看到 Redux 使代碼結構更清晰。 Note: 以前發表在掘金的 Redux 源碼解析github

Redux 包含 reducers,middleware,store enhancers,可是卻很是簡單。若是你以前構建過 Flux 應用,那麼對於你來講就更簡單了。即便你沒有使用過 Flux,依然也是很簡單的。面試

Actions

Actions 是應用程序將數據發送到 store 的載體。能夠經過 store.dispatch 來將 action 發送到 store 中。ajax

下面是幾個例子:數據庫

const ADD_TODO = 'ADD_TODO';

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

Actions 是一個原生 JavaScript 對象,而且必須帶有一個 type 屬性做爲識別行爲的標示。type 是一個靜態字符串。若是你的應用很龐大,那麼你須要把他們移到一個模塊中:express

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

Action Creators

Action Creators 是用於建立 action 對象的函數:json

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

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

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)
  )
複製代碼
相關文章
相關標籤/搜索