1. 程式人生 > >redux 官方示例 todomvc 中的 todoList 過濾事件解析

redux 官方示例 todomvc 中的 todoList 過濾事件解析

官方 todomvc 示例原始碼

如果已經安裝 Git for Windows 客戶端工具(傳送門),在工作資料夾下,右鍵 -> Git Bash Here,依次執行下面的程式碼,檢視執行效果,執行之後,可以修改原始碼,如果編譯通過,頁面會自動重新整理。

git clone https://github.com/reduxjs/redux.git
cd redux/examples/todomvc/
cnpm i
npm start

理解程式碼邏輯

點選【All】、【Active】、【Completed】,頁面做了哪些操作?在哪裡呼叫了重新獲取 todoList 並更新到頁面上的呢?
點選以上三個連結的時候,FilterLink 元件做的事情,僅僅是把自身資料 state.visibilityFilter 的值改成點選連結的 props.filter。
FilterLink 的 props 有一個成員,叫:filter,它是在哪兒賦值的呢?
看下面的程式碼(src/components/Footer.js

  <ul className="filters">
    {Object.keys(FILTER_TITLES).map(filter =>
      <li key={filter}>
        <FilterLink filter={filter}>
          {FILTER_TITLES[filter]}
        </FilterLink>
      </li>
    )}
  </ul>

從以上程式碼可知,是通過陣列 FILTER_TITLES 的 key 來初始化過濾連結(全部、待辦、完成)的。
再看一下陣列 FILTER_TITLES 的定義:

const FILTER_TITLES = {
  [SHOW_ALL]: 'All',
  [SHOW_ACTIVE]: 'Active',
  [SHOW_COMPLETED]: 'Completed'
}

上面的程式碼,SHOW_ALL、SHOW_ACTIVE、SHOW_COMPLETED 是常量,定義在 src/constants/TodoFilters.js
程式碼如下:

export const SHOW_ALL = 'show_all'
export const SHOW_COMPLETED = 'show_completed'
export const SHOW_ACTIVE = 'show_active'

通過 WebStorm 的除錯視窗(在 WebStorm 除錯 react 專案的方法,傳送門),可以看到,陣列 FILTER_TITLES,實際的值是:

{
  "show_all": "All",
  "show_active": "Active",
  "show_completed": "Completed"
}

看到這裡,就知道了下面的程式碼中,傳給 FilterLink 元件的 props 成員 filter 的值,其實就是陣列 FILTER_TITLES 的 key,即:show_all、show_active、show_completed。

  <ul className="filters">
    {Object.keys(FILTER_TITLES).map(filter =>
      <li key={filter}>
        <FilterLink filter={filter}>
          {FILTER_TITLES[filter]}
        </FilterLink>
      </li>
    )}
  </ul>

在介面上,頁面末尾那三個連結【All】、【Active】、【Completed】,就是三個 FilterLink 元件,通過上面的分析,這三個元件的 props.filter 分別是 show_all、show_active、show_completed。
FilterLink 元件,又用了一個UI元件 Link,在 Link 元件中,執行的點選事件是: onClick={() => setFilter()}
setFilter 函式是在容器元件 FilterLink 的 mapDispatchToProps 中定義的:

const mapDispatchToProps = (dispatch, ownProps) => ({
  setFilter: () => {
    dispatch(setVisibilityFilter(ownProps.filter))
  }
})

它要做的事,只是向 store 發出一個 dispatch,那麼,最終執行者在哪兒呢?
回答這個問題,需要了解 redux 原理。

  • redux 的 store 由函式 createStore 返回,該函式的第一個引數是 reducers,是包含了各個模組的 reducer。
  • 而 reducers,在這個例子中,是用 redux 提供的 combineReducers() 函式來整合得到的一個集合(更準確的說,是一個數組),這樣,store 就可以根據各個模組的 reducer key 來統一管理各個模組的 state 以及 actions(模組的行為,體現在自己模組的 reducer 中定義的各個函式)。
  • 對於 combineReducers(),官方是這樣描述的:combineReducers() 所做的只是生成一個函式,這個函式來呼叫你的一系列 reducer,每個 reducer 根據它們的 key 來篩選出 state 中的一部分資料並處理,然後這個生成的函式再將所有 reducer 的結果合併成一個大的物件。

瞭解了這個原理之後,回到剛才的問題,Link 元件的點選事件,最終的行為,是 reducer(檔案 src/reducers/visibilityFilter.js 定義的函式) visibilityFilter,其定義如下

const visibilityFilter = (state = SHOW_ALL, action) => {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter
    default:
      return state
  }
};

該方法的預設行為是返回 SHOW_ALL(常量),即返回字串 show_all,點選某一個連結時,返回的是 action 傳過來的 filter。那這個 action 又是在哪兒定義的呢?
從原始碼中可以看出,派發的 dispatch 是:dispatch(setVisibilityFilter(ownProps.filter))
再看上下文,不難發現,該 action 是在 src/actions/index.js 下定義的,setVisibilityFilter 的定義如下:

export const setVisibilityFilter = filter => ({ type: types.SET_VISIBILITY_FILTER, filter });

分析到這裡,問題來了,visibilityFilter 接收到這個 action 並執行之後,直接返回的是 action.filter,接下來又發生了什麼?
先看一下 rcux 文件關於 reducer 的描述(傳送門):

  1. reducers 指定了應用狀態的變化如何響應 actions 併發送到 store 的,記住 actions 只是描述了有事情發生了這一事實,並沒有描述應用如何更新 state。
  2. reducer 就是一個純函式,接收舊的 state 和 action,返回新的 state。
  3. 注意每個 reducer 只負責管理全域性 state 中它負責的一部分。每個 reducer 的 state 引數都不同,分別對應它管理的那部分 state 資料。

再回到 visibilityFilter,執行之後,返回的 filter 其實是會更新到 visibilityFilter 這個 reducer 負責的 state。
通過瀏覽器的 Redux DevTools 外掛,我們很容易看到,在 state 樹上,有兩個物件,key 分別為:todos 和 visibilityFilter。

接下來,因為 state 發生了變化,這會導致頁面重新渲染,而頁面渲染的時候,todoList 會根據 visibilityFilter 的值進行過濾,從而實現了三個連結應該有的功能。
相關程式碼如下:

import { createSelector } from 'reselect'
import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters'

const getVisibilityFilter = state => state.visibilityFilter
const getTodos = state => state.todos

export const getVisibleTodos = createSelector(
  [getVisibilityFilter, getTodos],
  (visibilityFilter, todos) => {
    switch (visibilityFilter) {
      case SHOW_ALL:
        return todos
      case SHOW_COMPLETED:
        return todos.filter(t => t.completed)
      case SHOW_ACTIVE:
        return todos.filter(t => !t.completed)
      default:
        throw new Error('Unknown filter: ' + visibilityFilter)
    }
  }
)

export const getCompletedTodoCount = createSelector(
  [getTodos],
  todos => (
    todos.reduce((count, todo) =>
      todo.completed ? count + 1 : count,
      0
    )
  )
)

createSelectorg

上面的程式碼,用到了 createSelectorg 來優化效能,有關 createSelectorg 方法,這裡不做分析,請參考: