Redux 基礎 - react 全家桶學習筆記(一)

佯真愚發表於2018-08-12

注:這篇是16年10月的文章,搬運自本人 blog...

github.com/BuptStEve/b…

零、環境搭建

參考資料

首先要明確一點,雖然 redux 是由 flux 演變而來,但我們完全可以並且也應該拋開 react 進行學習,這樣可以避免一開始就陷入各種細節之中。

所以推薦使用 jsbin 進行除錯學習,或者使用 create-react-app 作為專案腳手架。

一、Redux 是什麼?

Redux is a predictable state container for JavaScript apps.
Redux 是一個 JavaScript 狀態容器,提供可預測化的狀態管理。

overview

先不要在意那些細節

  • 總的來說,redux 使用 store 儲存並管理頁面中的各種狀態(state)
  • 當需要改變 state 時,使用 dispatch 呼叫 action creators 觸發 action
  • 接著使用純函式(pure function)reducer 來處理這些 action,它會根據當前 state 和 action 返回(注意這裡不是修改)新的 state
  • view 層可以對於 state 進行訂閱(subscribe),這樣就可以得到新的 state,從而可以重新整理介面(所以十分適合資料驅動的前端框架)

純函式:簡單的說就是對於同樣的輸入總是返回同樣的輸出,並且沒有副作用的函式。(推薦學習瞭解下函數語言程式設計)

1.1. 為什麼選擇 redux?

  • 隨著 JavaScript 單頁應用開發日趨複雜,JavaScript 需要管理比任何時候都要多的 state (狀態)。 這些 state 可能包括伺服器響應、快取資料、本地生成尚未持久化到伺服器的資料,也包括 UI 狀態,如啟用的路由,被選中的標籤,是否顯示載入動效或者分頁器等等。
  • 管理不斷變化的 state 非常困難。如果一個 model 的變化會引起另一個 model 變化,那麼當 view 變化時,就可能引起對應 model 以及另一個 model 的變化,依次地,可能會引起另一個 view 的變化。直至你搞不清楚到底發生了什麼。state 在什麼時候,由於什麼原因,如何變化已然不受控制。 當系統變得錯綜複雜的時候,想重現問題或者新增新功能就會變得舉步維艱。
  • 如果這還不夠糟糕,考慮一些來自前端開發領域的新需求,如更新調優、服務端渲染、路由跳轉前請求資料等等。前端開發者正在經受前所未有的複雜性,難道就這麼放棄了嗎?當然不是。
  • 這裡的複雜性很大程度上來自於:我們總是將兩個難以釐清的概念混淆在一起:變化非同步。 我稱它們為曼妥思和可樂。如果把二者分開,能做的很好,但混到一起,就變得一團糟。一些庫如 React 試圖在檢視層禁止非同步和直接操作 DOM 來解決這個問題。美中不足的是,React 依舊把處理 state 中資料的問題留給了你。Redux就是為了幫你解決這個問題。
  • 跟隨 Flux、CQRS 和 Event Sourcing 的腳步,通過限制更新發生的時間和方式,Redux 試圖讓 state 的變化變得可預測。這些限制條件反映在 Redux 的 三大原則中。

簡單總結就是使用 Redux 我們就可以沒有蛀牙(大霧)

  • 擁有可預測(predictable)的應用狀態,所以應用的行為也是可預測的
  • 因為 reducer 是純函式,所以方便對於狀態遷移進行自動化測試
  • 方便地記錄日誌,甚至實現時間旅行(time travel)

1.2. 三大原則(哲♂學)

1.2.1. 單一資料來源(Single source of truth)

整個應用的 state 被儲存在一棵 object tree 中,並且這個 object tree 只存在於唯一一個 store 中。

  • 來自服務端的 state 可以在無需編寫更多程式碼的情況下被序列化並注入到客戶端中
  • 便於除錯,在開發時可以將狀態儲存在本地
  • Undo/Redo 可以輕鬆實現,從而實現時間旅行

1.2.2. State 是隻讀的(State is read-only)

惟一改變 state 的方法就是觸發 action,action 是一個用於描述已發生事件的普通物件。

因為所有的修改都被集中化處理,且嚴格按照一個接一個的順序執行,(dispatch 同步呼叫 reduce 函式)因此不用擔心 race condition 的出現。 Action 就是普通物件而已,因此它們可以被日誌列印、序列化、儲存、後期除錯或測試時回放出來。

1.2.3. 使用純函式來執行修改(Changes are made with pure functions)

為了描述 action 如何改變 state tree ,你需要編寫 reducer。

Reducer 只是純函式,它接收先前的 state 和 action,並返回新的 state。剛開始你可以只有一個 reducer,隨著應用變大,你可以把它拆成多個小的 reducers,分別獨立地操作 state tree 的不同部分。

二、Redux 基礎

2.1. action

Action 就是一個普通的 JavaScript Object。

redux 唯一限制的一點是必須有一個 type 屬性用來表示執行哪種操作,值最好用字串,而不是 Symbols,因為字串是可被序列化的。

其他屬性用來傳遞此次操作所需傳遞的資料,redux 對此不作限制,但是在設計時可以參照 Flux 標準 Action

簡單總結 Flux Standard action 就是

  1. 一個 action 必須是一個 JavaScript Object,並且有一個 type 屬性。
  2. 一個 action 可以有 payload/error/meta 屬性。
  3. 一個 action 不能有其他屬性。

2.2. reducer

Reducer 的工作就是接收舊的 state 和 action,返回新的 state。

(previousState, action) => newState

之所以稱作 reducer 是因為它將被傳遞給 Array.prototype.reduce(reducer, ?initialValue) 方法。保持 reducer 純淨非常重要。永遠不要在 reducer 裡做這些操作:

  • 修改傳入引數;
  • 執行有副作用的操作,如 API 請求和路由跳轉;
  • 呼叫非純函式,如 Date.now() 或 Math.random()。

2.3. store

Store 就是用來維持應用所有的 state 樹的一個物件。

在 redux 中只有一個 store(區別於 flux 的多個 store),在 store 中儲存所有的 state,可以把它當成一個封裝了 state 的類。而除了對其 dispatch 一個 action 以外無法改變內部的 state。

在實際操作中我們只需要把根部的 reducer 函式傳遞給 createStore 就可以得到一個 store。

import { createStore } from 'redux';

function reducer(state, action) {
    switch (action.type) {
        case 'SOME_ACTION':
            // 一些操作
            return newState; // 返回新狀態
        default:
            return state;
    }
}

const store = createStore(reducer);
複製程式碼

redux 中提供了這幾個 api 操作 store

2.3.1. getState

返回當前的整個 state 樹。

2.3.2. dispatch(action)

分發 action 給對應的 reducer。

該函式會呼叫 getState() 和傳入的 action 以【同步】的方式呼叫 store 的 reduce 函式,然後返回新的 state。從而 state 得到了更新,並且變化監聽器(change listener)會被觸發。(對於非同步操作則將其放到了 action creator 這個步驟)

2.3.3. subscribe(listener)

為 store 新增一個變化監聽器,每當 dispatch 的時候就會執行,你可以在 listener(回撥函式)中使用 getState() 來得到當前的 state。

這個 api 設計的挺有意思,它會返回一個函式,而你執行這個函式後就可以取消訂閱。

2.3.4. replaceReducer(nextReducer)

替換 store 當前用來計算 state 的 reducer。

這是一個高階 API。只有在你需要實現程式碼分隔,而且需要立即載入一些 reducer 的時候才可能會用到它。在實現 Redux 熱載入機制的時候也可能會用到。

2.4. createStore

忽略各種型別判斷,實現一個最簡的 createStore 可以用以下程式碼。參考資料

const createStore = (reducer) => {
    let state;
    let listeners = [];

    const getState = () => state;

    const dispatch = (action) => {
        state = reducer(state, action); // 呼叫 reducer
        listeners.forEach(listener => listener()); // 呼叫所有變化監聽器
    };

    const subscribe = (listener) => {
        listeners.push(listener);

        return () => {
            // 返回解除監聽函式
            listeners = listeners.filter(l => l !== listener);
        };
    }

    dispatch({}); // 初始化

    return { getState, dispatch, subscribe };
};

複製程式碼

2.5. 計數器例子

三、與 React 進行結合

3.1. 通過 script 標籤匯入 react

實現同樣功能的 Counter

{% iframe jsbin.com/qalevu/edit… 100% 800 %}

3.2. 用 Redux 和 React 實現 TodoApp

在新增 react-redux 之前,為了體會下 react-redux 的作用,首先來實現一個比計數器更復雜一點兒的 TodoApp 栗子~

3.2.1. 分析與設計

1. 容器元件 V.S. 展示元件

元件一般分為

  • 容器元件(Smart/Container Components)
  • 展示元件(Dumb/Presentational Components)
- 容器元件 展示元件
Location 最頂層,路由處理 中間和子元件
Aware of Redux
讀取資料 從 Redux 獲取 state 從 props 獲取資料
修改資料 向 Redux 派發 actions 從 props 呼叫回撥函式

最佳實踐一般是由容器元件負責一些資料的獲取,進行 dispatch 等操作。而展示元件元件不應該關心邏輯,所有資料都通過 props 傳入。

這樣才能達到展示元件可以在多處複用,在具體複用時就是通過容器元件將其包裝,為其提供所需的各種資料。

2. 應用設計
  • 一個 TodoApp 包含了三個部分:
    • 頂部的 AddTodo 輸入部分
    • 中間的 TodoList 展示部分
    • 底部的 Footer 過濾部分
  • State 應該包含:
    • filter:過濾 todos 的條件
      • SHOW_ALL
      • SHOW_ACTIVE
      • SHOW_COMPLETED
    • todos:所有的 todo
      • todo:包含 id、text 和 completed
  • 然而傳到應用中的 props 只需要:
    • visibleTodos:過濾後的 todos
    • filter:過濾條件
  • Action 應該有三種:
    • ADD_TODO
    • TOGGLE_TODO
    • SET_VISIBILITY_FILTER

3.2.2. 編碼實現

1. action 部分
// 暫且使用數字作為 id
let nextTodoId = 0;

/*-- action creators --*/
const addTodo = (text) => (
    { type: 'ADD_TODO', id: nextTodoId++, text }
);

const toggleTodo = (id) => (
    { type: 'TOGGLE_TODO', id }
);

const setVisibilityFilter = (filter) => (
    { type: 'SET_VISIBILITY_FILTER', filter }
);
複製程式碼
2. reducer 部分
// 預設初始狀態
const initialState = { filter: 'SHOW_ALL', todos: [] };

function rootReducer(state = initialState, action) {
    switch (action.type) {
        case 'ADD_TODO':
            // 物件解構
            const { id, text } = action;

            return {
                ...state,
                todos: {
                    ...state.todos,
                    { id, text, completed: false },
                },
            };

        case 'TOGGLE_TODO':
            return {
                ...state,
                todos: state.todos.map(todo => {
                    if (todo.id !== action.id) return todo;

                    return {
                        ...todo,
                        completed: !todo.completed,
                    };
                }),
            };

        case 'SET_VISIBILITY_FILTER':
            return {
                ...state,
                filter: action.filter,
            };

        default:
            return state;
    }
}
複製程式碼

注意!

  1. 不要直接修改原有的 state,而是返回一個新的 state。可以使用 Object.assign() 新建一個新的 state。不能這樣使用 Object.assign(state, { visibilityFilter: action.filter }),因為它會改變第一個引數的值。你必須把第一個引數設定為空物件。你也可以開啟對 ES7 提案物件展開運算子的支援, 從而使用 { ...state, ...newState } 達到相同的目的。
  2. 在 default 的情況下返回舊的 state,用來相容遇到未知的 action 這樣的錯誤。

拆分 reducer 目前程式碼看著比較冗長,其實在邏輯上 todos 的處理和 filter 的處理應該分開,所以在 state 沒有互相耦合時,可以將其拆分,從而讓 reducer 精細地對於對應 state 的子樹進行處理。

// 處理單個 todo
const todoReducer = (state, action) => {
    switch (action.type) {
        case 'ADD_TODO':
            return {
                id: action.id,
                text: action.text,
                completed: false,
            };

        case 'TOGGLE_TODO':
            if (state.id !== action.id) return state;

            return {
                ...state,
                completed: !state.completed,
            };

        default:
            return state;
    }
};

// 處理 todos
const todosReducer = (state = [], action) => {
    switch (action.type) {
        case 'ADD_TODO':
            return [
                ...state,
                todoReducer(undefined, action),
            ];

        case 'TOGGLE_TODO':
            return state.map(t => todoReducer(t, action));

        default:
            return state;
    };
};

// 處理 filter
const filterReducer = (state = 'SHOW_ALL', action) => {
    switch (action.type) {
        case 'SET_VISIBILITY_FILTER':
            return action.filter;

        default:
            return state;
    };
};

const rootReducer = (state = initialState, action) => ({
    todos: todosReducer(state.todos, action),
    filter: filterReducer(state.filter, action),
});
複製程式碼

注意觀察最後的 rootReducer 函式,返回的是一個經過各種 reducer 處理過併合並後的新 state。

然鵝,注意這裡 todos: todos(state.todos, action), 傳入 state.todos,返回的一定也是 todos(因為都是 state 樹上的節點)。

所以 redux 提供了很實用的 combineReducers api,用於簡化 reducer 的合併。

import { combineReducers } from 'redux';

const rootReducer = combineReducers({
    todos: todosReducer,
    filter: filterReducer,
});

// initialState 可以作為第二個引數傳入
const store = createStore(rootReducer, initialState);
複製程式碼

並且如果 reducer 與 state 節點同名的話(即 todosReducer -> todos)還能通過 es6 的語法更進一步地簡化

import { combineReducers } from 'redux';

const rootReducer = combineReducers({ todos, filter });

// initialState 可以作為第二個引數傳入
const store = createStore(rootReducer, initialState);
複製程式碼

隨著應用的膨脹,我們還可以將拆分後的 reducer 放到不同的檔案中, 以保持其獨立性並用於專門處理不同的資料域。

3. view 部分
1. 只有根元件

首先只寫一個根元件 <TodoApp />,store 通過 props 傳入 TodoApp,並在生命週期的 componentDidMount 和 componentWillUnmount 時分別訂閱與取消訂閱。

import React, { Component } from 'react';

class TodoApp extends Component {
    // 訂閱 store 的變化
    componentDidMount() {
        const { store } = this.props;

        this.unsubscribe = store.subscribe(
            this.forceUpdate.bind(this)
        );
    }

    // 取消訂閱
    componentWillUnmount() {
        this.unsubscribe();
    }

    // 渲染單個 todo
    _renderTodo(todo) {
        const { store } = this.props;

        return (
            <li
                key={todo.id}
                onClick={() => store.dispatch(toggleTodo(todo.id))}
                style={{
                    textDecoration: todo.completed
                        ? 'line-through'
                        : 'none',
                    cursor: todo.completed
                        ? 'default'
                        : 'pointer',
                }}
            >
                {todo.text}
            </li>
        );
    }

    // 根據當前 filter 是否匹配,返回字串或是 a 連結
    _renderFilter(renderFilter, name) {
        const { store } = this.props;
        const { filter } = store.getState();

        if (renderFilter === filter) return name;

        return (
            <a href='#' onClick={e => {
                e.preventDefault();
                store.dispatch(setVisibilityFilter(renderFilter))
            }}>
                {name}
            </a>
        );
    }

    // 根據當前 filter 過濾需要渲染的 todos
    _getVisibleTodos(todos, filter) {
        switch (filter) {
            case 'SHOW_ALL':
                return todos;

            case 'SHOW_COMPLETED':
                return todos.filter(todo => todo.completed);

            case 'SHOW_ACTIVE':
                return todos.filter(todo => !todo.completed);

            default:
                return todos;
        }
    }

    render() {
        const { store } = this.props;
        const { todos, filter } = store.getState();

        let input;

        return (
            <div>
                {/* AddTodo */}
                <input type="text" ref={node => input = node} />
                <button onClick={() => {
                    if (!input.value) return;

                    store.dispatch(addTodo(input.value));
                    input.value = '';
                }}>
                    addTodo
                </button>

                {/* TodoList */}
                <ul>
                    {this._getVisibleTodos(todos, filter)
                        .map(this._renderTodo.bind(this))
                    }
                </ul>

                {/* Footer */}
                <p>
                    Show:
                    {' '}
                    {this._renderFilter('SHOW_ALL', 'all')}
                    {', '}
                    {this._renderFilter('SHOW_COMPLETED', 'completed')}
                    {', '}
                    {this._renderFilter('SHOW_ACTIVE', 'active')}
                </p>
            </div>
        );
    }
}

複製程式碼

TodoApp 只有根元件 {% iframe jsbin.com/bodise/edit… 100% 800 %}

2. 元件拆分

將所有介面內容全寫在 TodoApp 中實在是太臃腫了,接下來根據之前的分析結果將其分為以下子元件(全是展示元件)

  • AddTodo
  • TodoList
    • Todo
  • Footer
    • FilterLink
const AddTodo = ({ onAddClick }) => {
    let input;

    return (
        <div>
            <input type="text" ref={node => input = node} />
            <button onClick={() => {
                onAddClick(input.value);
                input.value = '';
            }}>
                addTodo
            </button>
        </div>
    );
};

const Todo = ({ text, onClick, completed }) => (
    <li
        onClick={onClick}
        style={{
            textDecoration: completed
                ? 'line-through'
                : 'none',
            cursor: completed
                ? 'default'
                : 'pointer',
        }}
    >
        {text}
    </li>
);

const TodoList = ({ todos, onTodoClick }) => (
    <ul>
        {todos.map(todo =>
            <Todo
                key={todo.id}
                {...todo}
                onClick={() => onTodoClick(todo.id)}
            />
        )}
    </ul>
);

const FilterLink = ({ filter, onClick, renderFilter, children }) => {
    if (renderFilter === filter) return (<span>{children}</span>);

    return (
        <a href='#' onClick={e => {
            e.preventDefault();
            onClick(renderFilter);
        }}>
            {children}
        </a>
    );
};

const Footer = ({ filter, onFilterClick }) => (
    <p>
        Show:
        {' '}
        <FilterLink
            filter={filter}
            renderFilter="SHOW_ALL"
            onClick={onFilterClick}
        >
            all
        </FilterLink>
        {', '}
        <FilterLink
            filter={filter}
            renderFilter="SHOW_COMPLETED"
            onClick={onFilterClick}
        >
            completed
        </FilterLink>
        {', '}
        <FilterLink
            filter={filter}
            renderFilter="SHOW_ACTIVE"
            onClick={onFilterClick}
        >
            active
        </FilterLink>
    </p>
);
複製程式碼

所以 TodoApp 精簡後是這樣~

class TodoApp extends Component {
    // ...

    render() {
        const { store } = this.props;
        const { todos, filter } = store.getState();

        return (
            <div>
                <AddTodo
                    onAddClick={text => {
                        if (!text) return;

                        store.dispatch(addTodo(text));
                    }}
                />

                <TodoList
                    todos={this._getVisibleTodos(todos, filter)}
                    onTodoClick={id => store.dispatch(toggleTodo(id))}
                />

                <Footer
                    filter={filter}
                    onFilterClick={filter => {
                        store.dispatch(setVisibilityFilter(filter));
                    }}
                />
            </div>
        );
    }
}
複製程式碼
3. 增加容器元件

現在我們仍然是以 TodoApp 作為容器元件,其中各個子元件都是展示元件。

但是這樣做的話一旦子元件需要某個屬性,就需要從根元件層層傳遞下來,比如 FilterLink 中的 filter 屬性。

所以下面我們增加容器元件,讓展示元件通過容器元件獲得所需屬性。

  • AddTodo(container)
  • VisibleTodoList(container)
    • TodoList
      • Todo
  • Footer
    • FilterLink(container)
      • Link
// store.dispatch 又被放回來了,
// 因為暫時我們只在 AddTodo 元件中使用 addTodo 這個 action
// 以後增加了新的 form 之後可以考慮再將 store.dispatch 移出去
const AddTodo = ({ store }) => {
    let input;

    return (
        <div>
            <input type="text" ref={node => input = node} />
            <button onClick={() => {
                if (!input.value) return;

                store.dispatch(addTodo(input.value));
                input.value = '';
            }}>
                addTodo
            </button>
        </div>
    );
};

const Todo = ({ text, onClick, completed }) => (
    <li
        onClick={onClick}
        style={{
            textDecoration: completed
                ? 'line-through'
                : 'none',
            cursor: completed
                ? 'default'
                : 'pointer',
        }}
    >
        {text}
    </li>
);

const TodoList = ({ todos, onTodoClick }) => (
    <ul>
        {todos.map(todo =>
            <Todo
                key={todo.id}
                {...todo}
                onClick={() => onTodoClick(todo.id)}
            />
        )}
    </ul>
);

// 容器元件
class VisibleTodoList extends Component {
    // 訂閱 store 的變化
    componentDidMount() {
        const { store } = this.props;

        this.unsubscribe = store.subscribe(
            this.forceUpdate.bind(this)
        );
    }

    // 取消訂閱
    componentWillUnmount() {
        this.unsubscribe();
    }

    // 根據當前 filter 過濾需要渲染的 todos
    _getVisibleTodos(todos, filter) {
        switch (filter) {
            case 'SHOW_ALL':
                return todos;

            case 'SHOW_COMPLETED':
                return todos.filter(todo => todo.completed);

            case 'SHOW_ACTIVE':
                return todos.filter(todo => !todo.completed);

            default:
                return todos;
        }
    }

    render() {
        const { store } = this.props;
        const { todos, filter } = store.getState();

        return (
            <TodoList
                todos={this._getVisibleTodos(todos, filter)}
                onTodoClick={id => {
                    store.dispatch(toggleTodo(id))
                }}
            />
        );
    }
}

// 原本的 FilterLink 改成 Link,去掉 filter 和 renderFilter 屬性,改為傳入 active
const Link = ({ active, onClick, children }) => {
    if (active) return (<span>{children}</span>);

    return (
        <a href='#' onClick={e => {
            e.preventDefault();
            onClick();
        }}>
            {children}
        </a>
    );
};

// 容器元件
class FilterLink extends Component {
    // 訂閱 store 的變化
    componentDidMount() {
        const { store } = this.props;

        this.unsubscribe = store.subscribe(
            this.forceUpdate.bind(this)
        );
    }

    // 取消訂閱
    componentWillUnmount() {
        this.unsubscribe();
    }

    render() {
        const { store, renderFilter, children } = this.props;
        const { filter } = store.getState();

        return (
            <Link
                active={filter === renderFilter}
                onClick={() => store.dispatch(
                    setVisibilityFilter(renderFilter)
                )}
            >
                {children}
            </Link>
        );
    }
}

// 展示元件
const Footer = ({ store }) => (
    <p>
        Show:
        {' '}
        <FilterLink
            store={store}
            renderFilter="SHOW_ALL"
        >
            all
        </FilterLink>
        {', '}
        <FilterLink
            store={store}
            renderFilter="SHOW_COMPLETED"
        >
            completed
        </FilterLink>
        {', '}
        <FilterLink
            store={store}
            renderFilter="SHOW_ACTIVE"
        >
            active
        </FilterLink>
    </p>
);

// 在不使用全域性變數 store 的情況下,
// 暫時只能通過 props 傳遞進來,
// Don't worry~很快就不會這麼麻煩了~
const TodoApp = ({ store }) => (
    <div>
        <AddTodo store={store} />
        <VisibleTodoList store={store} />
        <Footer store={store} />
    </div>
);
複製程式碼

通過觀察重構後的程式碼可以發現有三點麻煩的地方

  1. 根元件需要通過 props 將 store 傳給各個子元件
  2. 容器元件都要定義 componentDidMount 進行訂閱和 componentWillUnmount 取消訂閱
  3. 應用其實並不需要渲染所有的 todos,所以內部很麻煩地定義了 _getVisibleTodos 函式
4. Provider

讓我們先來解決第一個麻煩~,利用 React 提供的 context 特性

class Provider extends Component {
    // 通過該方法向 children 的 context 注入 store
    getChildContext() {
        return { store: this.props.store };
    }

    render() {
        return this.props.children;
    }
}

// 必須要宣告傳入 context 的 store 的型別
Provider.childContextTypes = {
    store: React.PropTypes.object,
};
複製程式碼

自頂向下地看一下如何使用到 TodoApp 中

// 1. 使用 Provider 包裹 TodoApp,並將 store 作為 props 傳入
ReactDOM.render(
    <Provider store={createStore(rootReducer, initialState)}>
        <TodoApp />
    </Provider>,
    document.getElementById('container'),
);

// 2. 根元件 TodoApp: 和 store say goodbye~,
// 因為 TodoApp 並不是容器元件~
const TodoApp = () => (
    <div>
        <AddTodo />
        <VisibleTodoList />
        <Footer />
    </div>
);

// 3. AddTodo: 由於 props 固定作為第一個傳入子元件的引數,
// 所以 { store } 要宣告在第二位,然鵝需要宣告 contextTypes...
const AddTodo = (props, { store }) => {
    // ...
};
// 必須宣告
AddTodo.contextTypes = {
    store: React.PropTypes.object,
};

// 4. VisibleTodoList: 從 props 改成從 context 中獲取 store,
// 同樣宣告 contextTypes...
class VisibleTodoList extends Component {
    // 訂閱 store 的變化
    componentDidMount() {
        const { store } = this.context; // props -> context

        // ...
    }

    // ...

    render() {
        const { store } = this.context; // props -> context
        const { todos, filter } = store.getState();

        // ...
    }
}
// 必須宣告
VisibleTodoList.contextTypes = {
    store: React.PropTypes.object,
};

// -- TodoList 和 Todo 不變 --

// 5. Footer:和 store say goodbye...
const Footer = () => (
    <p>
        Show:
        {' '}
        <FilterLink renderFilter="SHOW_ALL">
            all
        </FilterLink>
        {', '}
        <FilterLink renderFilter="SHOW_COMPLETED">
            completed
        </FilterLink>
        {', '}
        <FilterLink renderFilter="SHOW_ACTIVE">
            active
        </FilterLink>
    </p>
);

// 6. FilterLink: 同 VisibleTodoList(props + contextTypes...)
class FilterLink extends Component {
    // 訂閱 store 的變化
    componentDidMount() {
        const { store } = this.context; // props -> context

        // ...
    }

    // ...

    render() {
        const { renderFilter, children } = this.props;
        const { store } = this.context; // props -> context
        const { filter } = store.getState();

        // ...
    }
}
// 必須宣告
FilterLink.contextTypes = {
    store: React.PropTypes.object,
};

// -- Link 不變 --
複製程式碼

現在中間的非容器元件完全不用為了自己的孩子而費勁地傳遞 store={store} 所以以上我們就實現了簡化版的由 react-redux 提供的第一個元件 <Provider />

然鵝,有木有覺得老寫 contextTypes 好煩啊,而且 context 特性並不穩定,所以 context 並不應該直接寫在我們的應用程式碼裡。

計將安出?

5. connect
  • OOP思維:這還不簡單?寫個函式把容器元件傳進去作為父類,然後返回寫好了 componentDidMount,componentWillUnmount 和 contextTypes 的子類不就好啦~

恭喜你~物件導向的思想學的很不錯~

雖然 JavaScript 底層各種東西都是物件導向,然而在前端一旦與介面相關,照搬物件導向的方法實現起來會很麻煩...

  • React 早期使用者:這還不簡單?寫個 mixin 豈不美哉~~?

作為 react 親生的 mixin 確實在多元件間共享方法提供了一些便利,然而使用 mixin 的元件需要了解細節,從而避免狀態汙染,所以一旦 mixin 數量多了之後會越來越難維護。

Unfortunately, we will not launch any mixin support for ES6 classes in React. That would defeat the purpose of only using idiomatic JavaScript concepts.

所以官方也放棄了在 ES6 class 中對 mixin 的支援。

  • 函式式(FP):高階元件 High Order Component(下稱 hoc)才是終極解決方案~~

hocFactory:: W: React.Component => E: React.Component

如上所示 hoc 的建構函式接收一個 W(代表 WrappedComponent)返回一個 E(代表 Enhanced Component),而 E 就是這個高階元件。

假設我們有一箇舊元件 Comp,然鵝現在接收引數有些變動。

當然你可以複製貼上再修改舊元件的程式碼...(大俠受窩一拜)

也可以這麼寫,返回一個新元件來包裹舊元件。

class NewComp extends Component {
    mapProps(props) {
        return {/* new props */};
    }

    render() {
        return (<Comp {...this.mapProps(this.props)} />);
    }
}
複製程式碼

然鵝,如果有同樣邏輯的更多的元件需要適配呢???總不能有幾個抄幾遍吧...

所以騷年你聽說過高階元件麼~?

// 先返回一個函式,而那個函式再返回新元件
const mapProps = mapFn => Comp => {
    return class extends Component {
        render() {
            return (<Comp {...this.mapProps(this.props)} />);
        }
    };
};

const NewComp = mapProps(mapFn)(Comp); // 注意呼叫了兩次
複製程式碼

可以看到藉助高階元件我們將 mapFn 和 Comp 解耦合,這樣就算需要再巢狀多少修改邏輯都沒問題~天黑都不怕~

ok,扯了這麼多的淡,終於要說到 connect 了 是噠,你木有猜錯,react-redux 提供的第二個也是最後一個 api —— connect 返回的就是一個高階元件。

使用的時候只需要 connect()(WrappedComponent) 返回的 component 自動就完成了在 componentDidMount 中訂閱 store,在 componentWillUnmount 中取消訂閱和宣告 contextTypes。

這樣就只剩下最後一個麻煩

3.應用其實並不需要渲染所有的 todos,所以內部很麻煩地定義了 _getVisibleTodos 函式

其實 connect 函式的第一個引數叫做 mapStateToProps,作用就是將 store 中的資料提前處理或過濾後作為 props 傳入內部元件,以便內部元件高效地直接呼叫。這樣最後一個麻煩也解決了~

然鵝,我們問自己這樣就夠了麼?並沒有...

還有最後一個細節,以 FilterLink 為例。

class FilterLink extends Component {
    // ...

    render() {
        const { store, renderFilter, children } = this.props;
        const { filter } = store.getState();

        return (
            <Link
                active={filter === renderFilter}
                onClick={() => store.dispatch(
                    setVisibilityFilter(renderFilter)
                )}
            >
                {children}
            </Link>
        );
    }
}
複製程式碼

除了從 store 中獲取資料(filter),我們還從中獲取了 dispatch,以便觸發 action。如果將回撥函式 onClick 的內容也加到 props 中,那麼藉助 connect 整個 FilterLink 的邏輯豈不是都被我們抽象完了?

是噠,connect 的第二個引數叫做 mapDispatchToProps,作用就是將各個呼叫到 dispatch 的地方都抽象成函式加到 props 中的傳給內部元件。這樣最後一個麻煩終於真的被解決了~

const mapStateToLinkProps = (state, ownProps) => ({
    // ownProps 是原元件的 props,
    // 這裡為了和高階元件的 props 區分
    active: ownProps.renderFilter === state.filter,
});

const mapDispatchToLinkProps = (dispatch, ownProps) => ({
    onClick: () => {
        dispatch(
            setVisibilityFilter(ownProps.renderFilter)
        );
    },
});

// 注意原 FilterLink 整個都被我們刪了
const FilterLink = connect(
    mapStateToLinkProps,
    mapDispatchToLinkProps
)(Link);
複製程式碼

TodoApp 使用 react-redux {% iframe jsbin.com/fumihi/edit… 100% 800 %}

相關文章