Redux 基础 - react 全家桶学习笔记(一)

注:这篇是16年10月的文章,搬运自本人 blog...javascript

github.com/BuptStEve/b…html

零、环境搭建

参考资料前端

首先要明确一点,虽然 redux 是由 flux 演变而来,但咱们彻底能够而且也应该抛开 react 进行学习,这样能够避免一开始就陷入各类细节之中。java

因此推荐使用 jsbin 进行调试学习,或者使用 create-react-app 做为项目脚手架。node

1、Redux 是什么?

Redux is a predictable state container for JavaScript apps.
Redux 是一个 JavaScript 状态容器,提供可预测化的状态管理。react

overview

先不要在乎那些细节git

  • 总的来讲,redux 使用 store 保存并管理页面中的各类状态(state)
  • 当须要改变 state 时,使用 dispatch 调用 action creators 触发 action
  • 接着使用纯函数(pure function)reducer 来处理这些 action,它会根据当前 state 和 action 返回(注意这里不是修改)新的 state
  • view 层能够对于 state 进行订阅(subscribe),这样就能够获得新的 state,从而能够刷新界面(因此十分适合数据驱动的前端框架)

纯函数:简单的说就是对于一样的输入老是返回一样的输出,而且没有反作用的函数。(推荐学习了解下函数式编程)es6

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 咱们就能够没有蛀牙(大雾)github

  • 拥有可预测(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 的不一样部分。

2、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. 计数器例子

3、与 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 %}

相关文章
相关标签/搜索