Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redux 入门摘要 #32

Open
creeperyang opened this issue Jun 5, 2017 · 1 comment
Open

Redux 入门摘要 #32

creeperyang opened this issue Jun 5, 2017 · 1 comment

Comments

@creeperyang
Copy link
Owner

creeperyang commented Jun 5, 2017

原文: Redux 官方文档 EN | Redux 官方文档 CN
1. 有删减重组,需要细读的请直接浏览官方文档。
2. 专注 Redux 核心概念和开发流程,希望可以通过 15-20 分钟的阅读,对 Redux 有比较全面的了解,可以快速上手。

Redux 入门介绍

Redux is a predictable state container for JavaScript apps.

Redux 是一个给JavaScript app使用的可预测的状态容器。

为什么需要Redux?(动机)

JavaScript单页应用越来越复杂,代码必须管理远比以前多的状态(state)。这个状态包括服务端返回数据,缓存数据,本地创建的数据(未同步到服务器);也包括UI状态,如需要管理激活的路由,选中的标签,是否显示加载动效或者分页器等等。

管理不断变化的状态是很难的。如果一个 model 可以更新另一个 model ,那么一个 view 也可以更新一个 model 并导致另一个 model 更新,然后相应地,可能导致另一个 view 更新 —— 你理不清你的 app 发生了什么,失去了对 state 什么时候,为什么,怎么变化的控制 。当系统变得 不透明和不确定,就很难去重现 bug 和增加 feature 了。

通过 限制何时以及怎么更新,Redux 试图让 state 的变化可以预测

这里可以配合阅读 You Might Not Need Redux : Redux 的引入并不一定改善开发体验,必须权衡它的限制与好处。

Redux本身很简单,我们下面首先阐述它的核心概念和三大原则。

核心概念

想象一下用普通 JavaScript 对象 来描述 app 的 state

// 一个 todo app 的 state 可能是这样的:
{
  todos: [{
    text: 'Eat food',
    completed: true
  }, {
    text: 'Exercise',
    completed: false
  }],
  visibilityFilter: 'SHOW_COMPLETED'
}

这个对象就像没有 setter 的 model,所以其它部分的代码不能随意修改它而造成难以复现的 bug 。

如果要改变 state ,我们必须 dispatch 一个 action。action 是描述发生了什么的普通 JavaScript 对象。

// 下面都是action:
{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }

强制 每个 change 都必须用 action 来描述,可以让我们清楚 app 里正在发生什么, state 是为什么改变的。最后,把 state 和 actions 联结起来,我们需要 reducer 。

reducer 就是函数,以之前的 state 和 action 为参数,返回新的 state :

// 关注 visibilityFilter 的 reducer
function visibilityFilter(state = 'SHOW_ALL', action) {
  if (action.type === 'SET_VISIBILITY_FILTER') {
    return action.filter;
  } else {
    return state;
  }
}

function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action)
  };
}

以上就是 Redux 的核心概念,注意到我们并没有用任何 Redux 的 API,没加入任何 魔法。 Redux 里有一些工具来简化这种模式,但是主要的想法是描述如何根据这些 action 对象来更新 state。

三大原则

1. 单一数据源

整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。

console.log(store.getState())

/* Prints
{
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      text: 'Consider using Redux',
      completed: true,
    },
    {
      text: 'Keep all state in a single tree',
      completed: false
    }
  ]
}
*/

2. State 是只读的

改变 state 的唯一方式是触发 (emit) action,action 是描述发生了什么的对象。
这确保了视图和网络请求等都不能直接修改 state,相反它们只能表达想要修改的意图。因为所有的修改都被集中化处理,且严格按照一个接一个的顺序执行,因此不用担心 race condition 的出现。

store.dispatch({
  type: 'COMPLETE_TODO',
  index: 1
})

store.dispatch({
  type: 'SET_VISIBILITY_FILTER',
  filter: 'SHOW_COMPLETED'
})

3. 使用纯函数来执行修改

为描述 action 怎么改变 state tree,你要编写 reducers。

Reducer 只是一些纯函数,它接收之前的 state 和 action,并返回新的 state。刚开始你可能只需要一个 reducer ,但随着应用变大,你会需要拆分 reducer 。

以 todo app 为例迅速上手 Redux

1. 定义 actions

Action 就是把数据从应用(这些数据有可能是服务器响应,用户输入或其它非 view 的数据)发送到 store 的有效载荷。 它是 store 数据的唯一来源,你通过 store.dispatch(action) 来发送它到 store。

添加新 todo 任务的 action 是这样的:

const ADD_TODO = 'ADD_TODO'

{
  type: ADD_TODO,
  text: 'Build my first Redux app'
}

Action 本质上是 JavaScript 普通对象。Action 必须有一个字符串类型的 type 字段来表示将要执行的动作。多数情况下,type 会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块/文件来存放 action。

除了 type 字段外,action 对象的结构完全由你自己决定。但通常,我们希望减少 action 中传递的数据。

Action 创建函数 (action creator)

Action 创建函数 就是生成 action 的方法。“action” 和 “action 创建函数” 这两个概念很容易混在一起,使用时最好注意区分。

// 生成一个 ADD_TODO 类型的 action
function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

2. Reducers

Action 只是描述了有事情发生了这一事实,并没有指明应用如何更新 state。而这正是 reducer 要做的事情。

设计 State 结构

在 Redux 应用中,所有的 state 都被保存在一个单一对象中。最好可以在写代码之前想好 state tree 应该是什么形状的。

通常,这个 state tree 需要存放一些数据,以及一些 UI 相关的 state。这样做没问题,但尽量把数据与 UI 相关的 state 分开。

// todo app 的 state
{
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      text: 'Consider using Redux',
      completed: true,
    },
    {
      text: 'Keep all state in a single tree',
      completed: false
    }
  ]
}

处理 Action

有了 state 结构后,我们可以来写 reducer 了。 reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。

(previousState, action) => newState

保持 reducer 纯净非常重要。永远不要在 reducer 里做这些操作:

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

在高级篇里会介绍如何执行有副作用的操作。现在只需要记住 reducer 一定要保持纯净。只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。

import { VisibilityFilters } from './actions'

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

注意:

  • 不要修改 state。 使用 Object.assign({}, ...) 新建了一个副本。
  • default 情况下返回旧的 state。 遇到未知的 action 时,一定要返回旧的 state。

我们看到,多个 action 下,reducer 开始变得复杂。是否可以更通俗易懂?这里的 todosvisibilityFilter 的更新看起来是相互独立的,我们可以尝试拆分到单独的函数里。

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 Object.assign({}, todo, {
            completed: !todo.completed
          })
        }
        return todo
      })
    default:
      return state
  }
}

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

注意 todos 依旧接收 state,但它变成了一个数组!现在 todoApp 只把需要更新的一部分 state 传给 todos 函数,todos 函数自己确定如何更新这部分数据。这就是所谓的 reducer 合成,它是开发 Redux 应用最基础的模式。

现在更进一步,把 visibilityFilter 独立出去。那么我们可以有个主 reducer,它调用多个子 reducer 分别处理 state 中的一部分数据,然后再把这些数据合成一个大的单一对象。主 reducer 不再需要知道完整的 initial state。初始时,如果传入 undefined, 子 reducer 将负责返回它们(负责部分)的默认值。

// 彻底地拆分:
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 Object.assign({}, todo, {
            completed: !todo.completed
          })
        }
        return todo
      })
    default:
      return state
  }
}

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

function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  }
}

注意每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据。

当应用越来越复杂,我们还可以将拆分后的 reducer 放到不同的文件中, 以保持其独立性并用于专门处理不同的数据域。

最后,Redux 提供了 combineReducers() 工具来做上面 todoApp 做的事情。可以用它这样重构 todoApp:

import { combineReducers } from 'redux';

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

// 完全等价于
function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  }
}

3. 创建 store

前面两小节中,我们学会了使用 action 来描述“发生了什么”,和使用 reducers 来根据 action 更新 state 的用法。

Store 就是把它们联系到一起的对象。Store 有以下职责:

  • 维持应用的 state;
  • 提供 getState() 方法获取 state;
  • 提供 dispatch(action) 方法更新 state;
  • 通过 subscribe(listener) 注册监听器;
  • 通过 subscribe(listener) 返回的函数注销监听器。

再次强调一下 Redux 应用只有一个 单一 的 store。当需要拆分数据处理逻辑时,你应该使用 reducer 组合 而不是创建多个 store。

根据已有的 reducer 来创建 store 是非常容易的。在前面我们使用 combineReducers() 将多个 reducer 合并成为一个。现在我们将其导入,并传给 createStore()

import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp)

你可以把初始状态 intialState 作为第二个参数传给 createStore()。这对开发同构应用时非常有用,服务器端 redux 应用的 state 结构可以与客户端保持一致, 那么客户端可以将从网络接收到的服务端 state 直接用于本地数据初始化。

let store = createStore(todoApp, window.STATE_FROM_SERVER)
发起 actions

现在我们已经创建好了 store ,可以验证一下:

import { addTodo, toggleTodo, setVisibilityFilter, VisibilityFilters } from './actions'

// 打印初始状态
console.log(store.getState())

// 每次 state 更新时,打印日志
// 注意 subscribe() 返回一个函数用来注销监听器
let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
)

// 发起一系列 action
store.dispatch(addTodo('Learn about actions'))
store.dispatch(addTodo('Learn about reducers'))
store.dispatch(addTodo('Learn about store'))
store.dispatch(toggleTodo(0))
store.dispatch(toggleTodo(1))
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))

// 停止监听 state 更新
unsubscribe();

4. 数据流

严格的单向数据流 是 Redux 架构的设计核心。

这意味着应用中所有的数据都遵循相同的生命周期,这样可以让应用变得更加可预测且容易理解。同时也鼓励做数据范式化,这样可以避免使用多个且独立的无法相互引用的重复数据。

Redux 应用中数据的生命周期遵循下面 4 个步骤:

  1. 调用 store.dispatch(action)

  2. Redux store 调用传入的 reducer 函数。

  3. 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。

  4. Redux store 保存了根 reducer 返回的完整 state 树。

这个新的树就是应用的下一个 state!所有订阅 store.subscribe(listener) 的监听器都将被调用;监听器里可以调用 store.getState() 获得当前 state。

搭配 React 一起使用

首先强调一下:Redux 和 React 之间没有关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。

尽管如此,Redux 还是和 React 和 Deku 这类框架搭配起来用最好,因为这类框架允许你以 state 函数的形式来描述界面,Redux 通过 action 的形式来发起 state 变化。

安装 react-redux

Redux 自身并不包含对 React 的绑定库,我们需要单独安装 react-redux

Presentational and Container Components

绑定库是基于 容器组件和展示组件相分离 的开发思想。建议先读完这篇文章。

  展示组件 容器组件
作用 描述如何展现(骨架、样式) 描述如何运行(数据获取、状态更新)
直接使用 Redux
数据来源 props 监听 Redux state
数据修改 从 props 调用回调函数 向 Redux 派发 actions
调用方式 手动 通常由 React Redux 生成

技术上讲,我们可以手动用 store.subscribe() 来编写容器组件,但这就无法使用 React Redux 做的大量性能优化了。一般使用 React Redux 的 connect() 方法来生成容器组件。(不必为了性能而手动实现 shouldComponentUpdate 方法)

设计组件层次结构

还记得前面 设计 state 根对象的结构 吗?现在就要定义与它匹配的界面的层次结构。这不是 Redux 相关的工作,React 开发思想在这方面解释的非常棒。

  1. 展示组件: 纯粹的UI组件,定义外观而不关心数据怎么来,怎么变。传入什么就渲染什么。

  2. 容器组件: 把展示组件连接到 Redux。监听 Redux store 变化并处理如何过滤出要显示的数据。

  3. 其它组件 有时很难分清到底该使用容器组件还是展示组件,并且组件并不复杂,这时可以混合使用。

实现组件

省略其它部分,主要讲讲容器组件一般怎么写。

import { connect } from 'react-redux'

// 3. connect 生成 容器组件
const ContainerComponent = connect(
  mapStateToProps,
  mapDispatchToProps
)(PresentationalComponent)

// 2. mapStateToProps 指定如何把当前 Redux store state 映射到展示组件的 props 中
const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case 'SHOW_ALL':
      return todos
    case 'SHOW_COMPLETED':
      return todos.filter(t => t.completed)
    case 'SHOW_ACTIVE':
      return todos.filter(t => !t.completed)
  }
}

const mapStateToProps = (state) => {
  return {
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
  }
}

// 1. mapDispatchToProps() 方法接收 dispatch() 方法并返回期望注入到展示组件的 props 中的回调方法。
const mapDispatchToProps = (dispatch) => {
  return {
    onTodoClick: (id) => {
      dispatch(toggleTodo(id))
    }
  }
}
// 可以使用 Redux 的 bindActionCreators 把所有的暴露出来的 actionCreators 转成方法注入 props

export default ContainerComponent

connect 本身还是很明确的,指定我们注入哪些 data 和 function 到展示组件的 props ,给展示组件使用。

@creeperyang
Copy link
Owner Author

creeperyang commented Jun 7, 2017

API 探索

Redux API

1. createStore(reducer, [preloadedState], enhancer)

创建一个 Redux store 来以存放应用中所有的 state。详情可见 Redux API,这里主要强调两点:

  1. preloadedState:初始时的 state。在同构中会用到,比如从一个session恢复数据。

当 store 创建后,Redux 会 dispatch action({ type: ActionTypes.INIT })) 到 reducer 上,得到初始的 state 来填充 store。所以你的初始 state 是 preloadedState 在 reducers 处理 ActionTypes.INIT action 后的结果。 https://github.com/reactjs/redux/blob/v3.6.0/src/createStore.js#L241-L244

  1. enhancer:如果有 enhancer,那么会首先得到增强的 createStore,然后再createStore(reducer, [preloadedState])

https://github.com/reactjs/redux/blob/v3.6.0/src/createStore.js#L50 可结合下面 applyMiddleware一起看。

2. middleware 与 applyMiddleware(...middlewares)

我们可以用 middleware 来扩展 Redux。Middleware 可以让你包装 store 的 dispatch 方法来达到你想要的目的。同时, middleware 还拥有“可组合”这一关键特性。多个 middleware 可以被组合到一起使用,形成 middleware 链。其中,每个 middleware 都不需要关心链中它前后的 middleware 的任何信息。

middleware 的函数签名是 ({ getState, dispatch }) => next => action

如下是两个 middleware:

// logger middleware
function logger({ getState }) {
  return (next) => (action) => {
    console.log('will dispatch', action)

    // 调用 middleware 链中下一个 middleware 的 dispatch。
    let returnValue = next(action)

    console.log('state after dispatch', getState())

    // 一般会是 action 本身,除非
    // 后面的 middleware 修改了它。
    return returnValue
  }
}

// thunk middleware
function thunk({ dispatch, getState }) {
  return (next) => (action) => {
    if (typeof action === 'function') {
      return action(dispatch, getState);
    }

    return next(action);
  }
}

applyMiddleware 返回一个应用了 middleware 后的 store enhancer。这个 store enhancer 的签名是 createStore => createStore,但是最简单的使用方法就是直接作为最后一个 enhancer 参数传递给 createStore() 函数。

再来看下 applyMiddleware

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    var store = createStore(reducer, preloadedState, enhancer)
    var dispatch = store.dispatch
    var chain = []

    var middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    // chain 是 [(next) => (action) => action, ...]
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    // compose(...chain) 返回这样一个函数:
    // 对 chain 进行 reduce,从右向左执行,每次的结果作为下次执行的输入
    dispatch = compose(...chain)(store.dispatch)
    // 最终的 dispatch 是这样的:(action) => action

    return {
      ...store,
      dispatch
    }
  }
}

可以看到(假设 enhancerTest = applyMiddleware(A, B, C)):

  1. middleware 其实只是劫持/包装了 dispatch
  2. dispatch 本质上是同步的,但我们可以通过 thunk 等延迟执行 dispatch
  3. chain[index](dispatch) --> (action) => action,即我们得到的 dispatch 是一个层层嵌套的 (action) => action 函数。
  4. 除了最右侧的 C 得到的 next 是原本的 dispatch,剩下的都是被层层嵌套的 (action) => action 函数,并且越右侧越嵌套在里面,所以当 dispatch(action) 调用时,将会以下面顺序执行:A -> B -> C -> B -> AC 之前 A/B 都只执行了 next 之前的逻辑,之后各自完全执行。

3. combineReducers(reducers)

把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数。

真的很简单,从逻辑上来讲,就是:

combineReducers({
  keyA: reducerA,
  keyB: reducerB
})

// --->

function recuderAll (prevState, action) {
  return {
    keyA: reducerA(prevState.keyA, action),
    keyB: reducerB(prevState.keyB, action)
  }
}

核心就是干了上面的事,只是多了一些判断和检查。

4. bindActionCreators(actionCreators, dispatch)

把 action creators 转成拥有同名 keys 的对象,但使用 dispatch 把每个 action creator 包围起来,这样可以直接调用它们。

const actionCreators = {
  updateOrAddFilter(filter) {
    type: UPDATE_OR_ADD_FILTER,
    filter
  },
  removeFilter(type) {
    type: REMOVE_FILTER,
    filterType: type
  }
}

bindActionCreators(actionCreators, dispatch)

// -->

{
  updateOrAddFilter: (...args) => dispatch(original_updateOrAddFilter(...args)),
  removeFilter: (...args) => dispatch(original_removeFilter(...args)),
}

核心就是自动 dispatch ,这样我们可以在 react 组件里直接调用,Redux store 就能收到 action。


React Redux API

1. Provider

用法:

ReactDOM.render(
  <Provider store={store}>
    <MyRootComponent />
  </Provider>,
  rootEl
)

源码:

// storeKey 默认是 'store'
class Provider extends Component {
    getChildContext() {
      return { [storeKey]: this[storeKey], [subscriptionKey]: null }
    }

    constructor(props, context) {
      super(props, context)
      this[storeKey] = props.store;
    }

    render() {
      return Children.only(this.props.children)
    }
}
Provider.propTypes = {
    store: storeShape.isRequired,
    children: PropTypes.element.isRequired,
}
Provider.childContextTypes = {
    [storeKey]: storeShape.isRequired,
    [subscriptionKey]: subscriptionShape,
}

注意到, Provider 应用了 React Context,子组件都可以去访问 store

2. connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

connect 的函数签名是 ([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]) => (WrappedComponent) => ConnectComponent,最后返回的 onnectComponent 可以通过 context 去访问 store

connect API 比较复杂,这里主要讲下前两个参数。

  • [mapStateToProps(state, [ownProps]): stateProps] (Function): 如果定义该参数,组件将会监听 Redux store 的变化。任何时候,只要 Redux store 发生改变,mapStateToProps 函数就会被调用。该回调函数必须返回一个纯对象,这个对象会与组件的 props 合并。如果你省略了这个参数,你的组件将不会监听 Redux store。
  • [mapDispatchToProps(dispatch, [ownProps]): dispatchProps] (Object or Function): 如果传递的是一个对象,那么每个定义在该对象的函数都将被当作 Redux action creator,而且这个对象会与 Redux store 绑定在一起,其中所定义的方法名将作为属性名,合并到组件的 props 中。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant