2

注:这篇是16年10月的文章,搬运自本人 blog...
https://github.com/BuptStEve/...

零、环境搭建

参考资料

首先要明确一点,虽然 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. 计数器例子

  • 纯 JavaScript 不涉及界面(可以在右侧 console 中尝试 store.dispatch)

{% iframe http://jsbin.com/kejezih/edit... 100% 600 %}

  • 增加界面

{% iframe http://jsbin.com/jihara/edit?... 100% 600 %}

三、与 React 进行结合

3.1. 通过 script 标签导入 react

实现同样功能的 Counter

{% iframe http://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 http://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.mapFn(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 http://jsbin.com/fumihi/edit?... 100% 800 %}

四、总结

本文从 Redux 的理论基础和源码出发,介绍了 Redux 的各项基础 api。

接着一步一步地介绍如何与 React 进行结合,从过程中的各个痛点引出 react-redux 的作用和原理。

然鹅,还有好多的坑没填,比如:大型项目的文件结构、前端路由(react-router)、中间件(middlewares)、网络请求等各类异步操作、服务器端同构直出...

以上 to be continued...


佯真愚
1k 声望738 粉丝

[链接]