愿你坚持不懈,努力进步,进阶成自己的巨人

—— 2017.09, 写给3年后的自己

Redux学习笔记(一):入门

由于React只是DOM的一个抽象层,不是一个完整的Web应用的解决方案。为了开发大型的Web应用,我们还需要有一个好的架构来实现代码结构、组件间的通信。Redux应运而生

一、什么是Redux

Redux是一个JavaScript状态容器,提供可预测化的状态管理。Redux除了可以和React一起使用外,还支持其他的界面库。Redux的开发理念源自于2014年Facebook所提出的flux,并将flux的核心思想与函数式编程结合在一起而成。Redux是一个JavaScript状态容器,提供可预测化的状态管理。Redux除了可以和React一起使用外,还支持其他的界面库。Redux的开发理念源自于2014年Facebook所提出的flux,并将flux的核心思想与函数式编程结合在一起而成。

1、Redux的设计动机

随着前端应用的日趋复杂,JavaScript需要管理比任何时候都来得多的state(状态),这些状态有服务器响应数据、缓存数据、本地暂存数据、UI状态等等,正是由于状态之间彼此具有关联性、且状态是不断变化的,所以要管理好这些状态成为了一件复杂的事。再加上一些前端领域的新需求,如更新调优、服务端渲染、路由跳转前的数据请求等,都在增加着数据状态处理的复杂度。
虽然React解放了手动操作DOM的过程,但是如何处理state这个问题依然遗留了出来,而这也便是Redux设计出来的原因:让state的变化变得可预测、统一管理应用中的state,并提供易用的调试功能

2、三大原则

使用Redux,需要记住三大原则:

2-1、单一数据源

整个应用的state只存储在一个object tree中,且这个object tree也只存在于唯一的store中。这样子做的好处在于:

  • 对于同构应用,无需编写更多的代码的情况下能够将状态序列化并注入到客户端中
  • 方便调试,如可以把应用的状态保存在本地,加快开发速度

2-2、state是只读的

唯一改变state的方法是触发actionaction是一个用于描述已发生事件的普通对象。
这样子可以确保视图和网络请求都不能直接修改state,能做的就是表达想修改的意图。而所有的修改都会被集中化、依次处理,从而不会出现条件竞争的问题,而action就是一个对象,从而可以被日志打印、序列化、存储、后期调试、回放

2-3、使用纯函数执行修改

action只是一种描述,而修改state tree的过程则交由一个纯函数进行,而这个纯函数叫做reducer
reducer接受先前的state和action,然后返回新的state。在应用初始时可以只有一个reducer,不过随着应用的变大,可以拆分为多个reducer,然后分别独立地操作state tree的不同部分。

具体来说,Redux适用于多交互、多数据源的场景,即:
1)某个组件的状态,需要共享
2)某个状态需要在任何地方都能可以拿到
3)一个组件需要改变全局状态
4)一个组件需要改变另一个组件的状态

Redux的设计思想主要总计为两句话:
1)Web应用是一个状态机,视图与状态是一一对应的
2)所有的状态(state),保存在一个对象里(称之为store)


二、安装

可以用npm安装稳定版:

npm install --save redux

多数情况下,还可以使用React绑定库和开发者工具:

npm install --save react-redux
npm install --save-dev redux-devtools


三、基本概念

Redux中涉及的主要概念有:StoreStateActionReducer,解释如下:

1、store

store是Redux中保存数据的仓库,它的主要职责有:
1)维持应用的state
2)提供getState()方法获取state
3)提供dispatch(action)方法更新state
4)通过subscribe(listener)注册监听器,subscribe(listener),方法返回的函数可以注销监听器
创建一个store,可以使用createStore()方法,如:

import { createStore } from 'redux';
const store = createStore(reducer, defaultState);

createStore()的第一个参数用于传入一个reducer,而第二个参数则用于设置state的初始状态(不过第二个参数是可选的)。

2、state

state则是应用的状态,redux中,store对象包含了所有的数据,如果我们想得到某个时间点的数据,就需要对store生成快照,而这种特定时间点所采集的数据集合,就是state。获取当前时刻的state,采用getState()方法,示例如下:

import { createStore } from 'redux';
const store = createStore(reducer);
const state = store.getState();

在Redux中,一个state对应于一个view,只要state相同,view就相同。即它们是一一对应的:

state <------> view

3、action

在Redux中,用户不能直接修改state,如果我们需要修改state,则需要通过action和reducer。action其实是一种消息,来发出通知表示state应该要发生变化了。一个action的示例如:

const ADD_TODO = 'ADD_TODO'
const action = {
    type: ADD_TODO,
    title: 'Learn Redux' 
}

这里需要注意的是:
1)一个action中应该包含有一个type,表示动作名称(在大型应用中,推荐采用常量并分离到一个单独的文件中,如actionTypes.js
2)除了type外,action中的其他属性都是随意的,其他的属性称之为载荷(payload)
3)应该尽量减少在action中传递的数据

action创建函数

我们可以定义一个函数用来生成action,避免写出太多的样板代码,如:

const ADD_TODO = 'ADD_TODO';
function addTodo(title, isCompleted) {
    return {
        type: ADD_TODO,
        title,
        isCompleted
    }
}
const action = addTodo('Learn Redux', false);

这样子做的好处是更方便测试和具有更高的可移植性。
而有了action之后,我们可以使用store.dispatch()来分发这个action,如:

store.dispatch(addTodo('Learn Redux', false));

store里,会直接通过store.dispatch()来调用dispatch()方法。不过多数情况下,我们会采用react-redux提供的connect()来帮助调用。此外,还有辅助函数bindActionCreators(),可以自动把多个action创建函数绑定到dispatch()方法上。

4、reducer

action的职责是描述事情的发生,却没有指明事情发生后应用如何更新state。而reducer做的事情正是接收action,然后给出新的state,即:

(previousState, action) => newState

而之所以称为reducer,是因为它将被传递给Array.prototype.reduce(reducer, initialValue?)方法,所以reducer应该是一个纯函数,它里面不应该包含有以下操作:

  • 修改传入参数
  • 执行有副作用的操作,如API请求、路由跳转
  • 调用非纯函数,如Date.now()Math.random()

纯函数即:只要传入的参数相同,返回计算得到的下一个state就一定相同。没有特殊情况、没有副作用、没有API请求、没有变量修改,只单纯执行计算。
Redux首次执行时,其state为undefined,所以此时可以返回应用的初始状态:

import { VisibilityFilters } from './actions'
const initialState = {
    visibilityFilter: VisibilityFilters.SHOW_ALL,
    todos: []
}
function todoApp(state, action) {
    if (typeof state === 'undefined') {
        return initialState
    }
    return state
}
// 也可以用ES6的写法简写todoApp,如
function todoApp(state = initialState, action) {
    return state
}

接下来,可以拓展todoApp,进行处理过程:

function todoApp(state = initialState, action) {
    const { type } = action
    switch (type) {
        case SET_VISIBILITY_FILTER:
            return Object.assign({}, state, {
                visibilityFilter: action.filter
            })
            // 这里也可以用ES7简写为:
            // const newState = { visibilityFilter: action.filter }
            // { ...state, ...newState }
        default:
            return state
    }
}

注意点:
1)不能修改state,所以这里使用Object.assign(),第一个参数传入的是一个新对象
2)在default的情况下返回旧的state,即遇到未知的action时,一定要返回旧的state

1、处理多个action

处理多个action并不复杂,在reducer里多添加几个处理分支即可,如:

function todoApp(state = initialState, action) {
    const { type } = action
    switch (type) {
        case SET_VISIBILITY_FILTER:
            const newState = { visibilityFilter: action.filter }
            return { ...state, ...newState }
        case ADD_TODO:
            const newState = {
                todos: [
                    ...state.todos,
                    {
                        text: action.text,
                        completed: false
                    }
                ]
            }
            return { ...state, ...newState }
        case TOGGLE_TODO:
            const newState = {
                todos: state.todos.map((todo, index) => {
                    if (index === action.index) {
                        return {
                            ...todo,
                            completed: !todo.completed
                        }
                    }
                    return todo
                })
            }
            return { ...state, ...newState }
        default: return state
    }
}

2、拆分reducer

我们会发现以上的reducer看起来很冗长,那么有没有办法可以减少冗长呢?拆分示例如下:

function todos(state = [], action) {
    const { type } = action
    switch (type) {
        case ADD_TODO:
            return [
                ...state,
                {
                    text: action.text,
                    completed: false
                }
            ]
        case TOGGLE_TODO:
            return state.map((todo, index) => {
                if (index === action.index) {
                    return {
                        ...todo,
                        completed: !todo.completed
                    }
                }
                return todo
            })
        default:
            return state
    }
}

function todoApp(state = initialState, action) {
    switch (action.type) {
        case SET_VISIBILITY_FILTER:
            return {
                ...state,
                visibilityFilter: action.filter
            }
        case ADD_TODO:
        case TOGGLE_TODO:
            return {
                ...state,
                todos: todos(state.todos, action)
            }
        default:
            return state
    }
}

即,现在todoApp()接收的state是完整的整个state tree,而todos()接收的则是state tree中的一部分(todos),这种模式便是reducer合成:把一部分状态传给子reducer,由子reducer自己决定如何更新部分数据。而这也是开发Redux应用最基础的模式。
以下是完整的做reducer合成的例子:
1)首先,可以专门抽出一个reducer来管理visibilityFilter,如:

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

2)然后,开发一个函数来作为主reducer主reducer调用多个子reducer分别处理state中的一部分数据,然后再把这些数据合成一个大的单一对象。主reducer不需要设置初始化时完整的state,如果初始时传入undefined,那么子reducer将负责返回它们的默认值

// 子reducer,负责todos部分的状态
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 {
                        ...todo,
                        completed: !todo.completed
                    }
                }
                return todo
            })
        default:
            return state
    }
}

// 子reducer,负责visibilityFilter部分的状态
function visibilityFilter(state = SHOW_ALL, action) {
    switch (action.type) {
        case SET_VISIBILITY_FILTER:
            return action.filter
        default:
            return state
    }
}

// 主reducer
function todoApp(state = {}, action) {
    return {
        visibilityFilter: visibilityFilter(state.visibility, action),
        todos: todos(state.todos, action)
    }
}

可见,每个reducer只负责全局state中它负责的一部分,所以每个reducerstate参数都不同,对应的也是它独自管理的那部分state数据

4、combineReducers()

可以使用Redux提供的combineReducers()工具来做todoApp所做的事情,如下:

import { combineReducers } from 'redux'
const todoApp = combineReducers({
    visibilityFilter,
    todos
})
export default todoApp

5、store.subscribe()

store允许使用store.subscribe()方法设置监听函数,一旦state发生了变化,就自动执行该函数。如:

import { createStore } from 'redux';
const store = createStore(reducer);
store.subscribe(listener);

如此一来,当state发生了变化,那么listener函数就会被自动执行。如何应用这个函数呢?我们可以使用它来实现view的自动渲染,如在react中,我们可以把组件的render()方法或者setState()方法放入listener中,就可以实现view的自动渲染了。
subscribe方法会返回一个函数,调用该函数,就能注销这个监听器了:

const unsubscribe = store.subscribe(/* ... */);
unsubscribe();


四、搭配React

Redux和React之间没有关系,Redux可以支持React、Angular、Ember、jQuery甚至纯javascript,不过Redux的最佳搭配仍然是React或者Deku

1、安装ReactRedux

Redux默认是不带React绑定库的,所以需要单独安装:

npm install --save react-redux

2、容器组件和展示组件

ReduxReact有个开发思想:容器组件和展示组件相分离
推荐的做法是:只在最顶层组件里使用Redux,其余内部组件仅仅是展示性的,故数据都可以通过props传入:

+-----------------+------------------------+---------------------------+
|                 | 容器组件                | 展示组件                  |
+-----------------+------------------------+---------------------------+
| 位置            | 最顶层,路由处理         | 中间和子组件               |
+-----------------+------------------------+---------------------------+
| 能感知Redux?    | 是                     | 否                        |
+-----------------+------------------------+---------------------------+
| 读取数据         | 从Redux获取state       | 从props获取数据            |
+-----------------+------------------------+---------------------------+
| 修改数据         | 向Redux派发actions     | 从props调用回调函数         |
+-----------------+------------------------+---------------------------+

在复杂应用中,可以有多个容器组件,虽然也可以嵌套使用容器组件,但应该尽可能地使用传递props的形式

3、设计组件层次结构

现在我们以设计一个todoApp为例:

我们要显示一个todo项的列表,在一个todo项被点击后,会增加一条删除线并标记completed。
还能够显示用户新增一个todo字段,并在footer里显示一个 显示全部/已完成/未完成 的切换按钮

根据这个需求,可以设计出以下的结构:

  • AddTodo 新增任务,有输入框和按钮
    • onAddClick(text: string) 当按钮被点击时调用的回调函数
  • TodoList 用于显示todos列表
    • todos: Array{ text, completed }形式显示的todo项数组
    • onTodoClick(index: number) 当todo项被点击时调用的回调函数
  • Todo 一个todo项
    • text: string
    • completed: boolean
    • onClick()
  • Footer 一个允许用户改变todo过滤器的组件
    • filter: string 当前的过滤器为:SHOW_ALLSHOW_COMPLETEDSHOW_ACTIVE
    • onFilterChange(nextFilter: string)

而以上全都是展示组件:不关心数据来源和变化,传入什么就渲染什么。
接下来,让我们编写相应的React组件,如下:
components/AddTodo.js

import React, { findDOMNode, Component, PropTypes } from 'react'
export default class AddTodo extends Component {
    render() {
        return (
            <div>
                <input type="text" ref="input" />
                <button onClick={e => this.handleClick(e)}>Add</button>
            </div>
        )
    }
    handleClick(e) {
        const node = findDOMNode(this.refs.input)
        const text = node.value.trim()
        this.props.onAddClick(text)
        node.value = ''
    }
}
AddTodo.propTypes = {
    onAddClick: PropTypes.func.isRequired
}

components/Todo.js

import React, { Component, PropTypes } from 'react'
export default class Todo extends Component {
    render() {
        return (
            <li
                onClick={this.props.onClick}
                style={{
                    textDecoration: this.props.completed 
                        ? 'line-through'
                        : 'none',
                    cursor: this.props.completed
                        ? 'default'
                        : 'pointer'
                }}>
                {this.props.text}
            </li>
        )
    }
}
Todo.propTypes = {
    onClick: PropTypes.func.isRequired,
    text: PropTypes.string.isRequired,
    completed: PropTypes.bool.isRequired
}

components/TodoList.js

import React, { Component, PropTypes } from 'react'
import Todo from './Todo'
export default class TodoList extends Component {
    render() {
        return (
            <ul>
                {this.props.todos.map((todo, index) => 
                    <Todo {...todo} key={index} onClick={
                        () => this.props.onTodoClick(index)
                    } />
                )}
            </ul>
        )
    }
}
TodoList.propTypes = {
    onTodoClick: PropTypes.func.isRequired,
    todos: PropTypes.arrayOf(PropTypes.shape({
        text: PropTypes.string.isRequired,
        completed: PropTypes.bool.isRequired
    }).isRequired).isRequired
}

components/Footer.js

import React, { Component, PropTypes } from 'react'
export default class Footer extends Component {
    renderFilter(filter, name) {
        if (filter === this.props.filter) {
            return name
        }
        return (
            <a href="javascript:;" onClick={e => {
                e.preventDefault()
                this.props.onFilterChange(filter)
            }}>{name}</a>
        )
    }
    render() {
        return (
            <p>
                Show:
                {' '}
                {this.renderFilter('SHOW_ALL', 'All')}
                {', '}
                {this.renderFilter('SHOW_COMPLETED', 'Completed')}
                {', '}
                {this.renderFilter('SHOW_ACTIVE', 'Active')}
            </p>
        )
    }
}
Footer.propTypes = {
    onFilterChange: PropTypes.func.isRequired,
    filter: PropTypes.oneOf([
        'SHOW_ALL',
        'SHOW_COMPLETED',
        'SHOW_ACTIVE'
    ]).isRequired
}

在未和redux关联的情况下,容器组件containers/App.js可以编写如下:

import React, { Component } from 'react'
import AddTodo from '../components/AddTodo'
import TodoList from '../components/TodoList'
import Footer from '../components/Footer'

export default class App extends Component {
    render() {
        return (
            <div>
                <AddTodo onAddClick={text => console.log('add todo', text)} />
                <TodoList
                    todos={[
                        { text: 'Task1', completed: true },
                        { text: 'Task2', completed: false }
                    ]}
                    onTodoClick={todo => console.log('todo clicked', todo)}
                />
                <Footer filter="SHOW_ALL" onFilterChange={filter => 
                    console.log('filter change', filter)
                } />
            </div>
        )
    }
}

现在,我们要把这个APP连接到Redux,从而能够dispatch actions和获取store里的状态
首先,我们需要获取从之前安装好的react-redux提供的Provider,然后将根组件包装在<Provider>里,如:

import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './containers/App'
import todoApp from './reducers'

let store = createStore(todoApp)
render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
)

接下来,可以使用ReactRedux提供的connect()方法将包装好的组件连接到Redux。注意:我们应该尽量只做一个顶层的组件,或者route处理,虽然可以将任一组件connect()到store中,但是应该避免这么做否则会导致数据流难以追踪。
任何一个被connect()包装好的组件都可以得到一个dispatch()方法作为组件的props,以及得到全局state中所需的任何内容。connect()接收参数selector,从而从组件中筛选出需要的props,改造后的containers/App.js如下:

import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import {
    addTodo, completeTodo, setVisibilityFilter, VisibilityFilters
} from '../actions'
import AddTodo from '../components/AddTodo'
import TodoList from '../components/TodoList'
import Footer from '../components/Footer'

class App extends Component {
    render() {
        // 通过调用 connect() 注入
        const { dispatch, visibleTodos, visibilityFilter } = this.props
        return (
            <div>
                <AddTodo onAddClick={text => dispatch(addTodo(text))} />
                <TodoList
                    todos={this.props.visibleTodos}
                    onTodoClick={index => dispatch(completeTodo(index))}
                />
                <Footer
                    filter={visibilityFilter}
                    onFilterChange={nextFilter => 
                        dispatch(setVisibilityFilter(nextFilter))
                    }
                />
            </div>
        )
    }
}

App.propTypes = {
    visibleTodos: PropTypes.arrayOf(PropTypes.shape({
        text: PropTypes.string.isRequired,
        completed: PropTypes.bool.isRequired
    })),
    visibilityFilter: PropTypes.oneOf([
        'SHOW_ALL',
        'SHOW_COMPLETED',
        'SHOW_ACTIVE'
    ]).isRequired
}

function selectTodos(todos, filter) {
    switch (filter) {
        case VisibilityFilters.SHOW_ALL:
            return todos
        case VisibilityFilters.SHOW_COMPLETED:
            return todos.filter(todo => todo.completed)
        case VisibilityFilter.SHOW_ACTIVE:
            return todos.filter(todo => !todo.completed)
    }
}

function select(state) {
    return {
        visibleTodos: selectTodos(state.todos, state.visibilityFilter),
        visibilityFilter: state.visibilityFilter
    }
}

// 包装component。注入 dispatch 和 state
export default connect(select)(App)


五、小结

1、数据流

Redux架构的设计核心是严格的单向数据流:应用中的所有数据都遵循相同的生命周期,从而可以让应用变得更加可预测且容易理解。这也同时鼓励了数据的范式化,避免使用多个且独立的无法互相引用的重复数据。
Redux的工作流程,如下图所示

说明:
1)首先,调用store.dispatch(action),action即为一个描述“发生了什么”的普通对象,如:

{ type: 'LIKE_ARTICLE', articleId: 42 }
{ type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Mary' } }
{ type: 'ADD_TODO', text: 'Read the Redux docs' }

可以在任何地方调用store.dispatch(action)(如组件中、XHR回调中、定时器中)
2)然后,store自动调用Reducer,并传入两个参数:当前state和收到的action,reducer会返回新的state:即nextState = someReducer(previousState, action)

// 当前应用的state
let previousState = {
    visibleTodoFilter: 'SHOW_ALL',
    todos: [
        {
            text: 'Read the docs',
            complete: false
        }
    ]
}
// 将要执行的action
let action = {
    type: 'ADD_TODO',
    text: 'Understand the flow'
}

let nextState = todoApp(previousState, action)

仍然需要注意的是:reducer是纯函数,其状态是可预测的,相同的输入必然得到相同的输出。那些带有副作用的操作,如API调用、路由跳转,需要在dispatch action之前发生
3)所有订阅store.subscribe(listener)的监听器在状态变更时都会被调用。在listener中,它可以使用store.getState()获得当前状态,如果使用的是react,那么就会触发重新渲染得到新的view,如:

function listener() {
    let newState = store.getState();
    component.setState(newState);
}