烦恼一般都是想太多了。

0%

Redux的基本概念

实际上之前听过群友说过这个东西,但是具体不知道什么作用的,大概想着也不会有用着的地方。但是在最近想要用 Reactive Native 来写一个 APP 的时候,突然发现,原来这是一个比较实用的神器的。所以,有必要来简单的学一下。这里不具体的介绍如何使用它,而是从上而下对其的工作的机制有一个大概的了解。

Redux 的中文网站 有详细的介绍。

基本的概念

Redux 是 一个专为 JavaScript 应用设计可预测的状态容器。我基本的限制就是将所有的状态(数据)放在一个统一的地方进行存储(Store),然后将此这些 State 暴露给所有的组件使用。

这样的话,就不会在各个组件(控件)间进行复杂的传值,依赖的解决等等。

首先先来一张图:

术语

上面的图中展示了几个概念:

  • Action。行为(动作)是把数据从应用(译者注:这里之所以不叫 view 是因为这些数据有可能是服务器响应,用户输入或其它非 view 的数据 )传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch() 将 action 传到 store。
  • Reducers 指定了应用状态的变化如何响应 actions 并发送到 store 的,记住 actions 只是描述了有事情发生了这一事实,并没有描述应用如何更新 state。 上图中, Store 会有一个总的 Reducers ,然后其可以是由多个小的 Reducer 进行组成的。
  • Store ,负责将 Actions, Reducers,State 组合在一起。负责管理整个 State。调用 Reducers 等等。就我个人理解而言,其主要就是根据响应的 Action,然后来调用 Reducer ,更新 State 。通知订阅了此 State 的相关订阅者。其职能为:
  • Middleware。这个东西,位于 Store 和 Actions 之间。这个的设计主要是为了来进行异步的调用。在这里我们就可以进行一些副作用的操作,因为 Redux 强烈建议,不要在 Reducer 中进行任何有副作用的操作。可以有多个 Middleware,这里可以在拦截 Actions。

selectors

官方推荐是使用 Reselect 来进行创建。

Reselect 库可以创建可记忆的(Memoized)、可组合的 selector 函数。Reselect selectors 可以用来高效地计算 Redux store 里的衍生数据。

就比如,我们的组件应该都是关心 State 中的一部分数据,而不是全部都关注。因此,我们都会在 mapStateToProps 做一些运算,得出我们想要的数据,这个运算函数,我们将把个叫做 selectors 。但是,每次都进行重新计算,或当 State 的数据 过大,或者计算逻辑比较复杂的时候,就会有性能的损失。Reselect 就是为了让我们来干这活的。

这个抽空再看了。

创建可记忆的 Selector

Reselect 提供 createSelector 函数来创建可记忆的 selector。createSelector 接收一个 input-selectors 数组和一个转换函数作为参数。如果 state tree 的改变会引起 input-selector 值变化,那么 selector 会调用转换函数,传入 input-selectors 作为参数,并返回结果。如果 input-selectors 的值和前一次的一样,它将会直接返回前一次计算的数据,而不会再调用一次转换函数。

import { createSelector } from 'reselect'

const getVisibilityFilter = state => state.visibilityFilter
const getTodos = state => state.todos

export const getVisibleTodos = createSelector(
[getVisibilityFilter, getTodos],
(visibilityFilter, todos) => {
switch (visibilityFilter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
)

组合 Selector

可记忆的 selector 自身可以作为其它可记忆的 selector 的 input-selector。下面的 getVisibleTodos 被当作另一个 selector 的 input-selector,来进一步通过关键字(keyword)过滤 todos。

const getKeyword = state => state.keyword

const getVisibleTodosFilteredByKeyword = createSelector(
[getVisibleTodos, getKeyword],
(visibleTodos, keyword) =>
visibleTodos.filter(todo => todo.text.indexOf(keyword) > -1)
)

Middleware

相对于 Express 或者 Koa 的 middleware,Redux middleware 被用于解决不同的问题,但其中的概念是类似的。它提供的是位于 action 被发起之后,到达 reducer 之前的扩展点。 你可以利用 Redux middleware 来进行日志记录、创建崩溃报告、调用异步接口或者路由等等。

React Redux

这个玩意,是 Redux 专门为 React 所设计的库。简化了我们的使用。这个库的使用比较简单,关于再于其提供的 connect() 函数。其有两个关键的内容:

  • Provider 这个组件,能让我们在整个应用的剩余部分 都能访问 Redux 的 Store。
  • connect() 。这个函数能让我们的 React 组件,与 Redux 的 Store 连接起来。而不用我们手动进行绑定。

connect()

原型:

function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)

connect 函数,会将 store 中存储的 State 与本地组件相关联,更确切的说,组件用这个函数来订阅和关于 State 里面的内容。

mapStateToProps?: (state, ownProps?) => Object

如果定义该参数,组件将会监听 Redux store 的变化。任何时候,只要 Redux store 发生改变,mapStateToProps 函数就会被调用。该回调函数必须返回一个纯对象,这个对象会与组件的 props 合并。如果你省略了这个参数,你的组件将不会监听 Redux store。

如果指定了该回调函数中的第二个参数 ownProps,则该参数的值为传递到组件的 props,而且只要组件接收到新的 props,mapStateToProps 也会被调用(例如,当 props 接收到来自父组件一个小小的改动,那么你所使用的 ownProps 参数,mapStateToProps 都会被重新计算)。

顾名思义,这个函数的用途:就是将 state 里面的内容,映射成为组件的属性。

同时,这个函数定义的参数个数决定了这个函数何时会被调用。

  • 如果只有 state 参数。那么 Store 更新时会调用,同时将 State 作为唯一的参数进行传递。

    const mapStateToProps = state => ({ todos: state.todos })
  • ownProps 如果还有这个参数。在 Store 更新或者 组件收到新的 props 参数时都会被调用。

    const mapStateToProps = (state, ownProps) => ({
    todo: state.todos[ownProps.id]
    })

mapDispatchToProps?: Object | (dispatch, ownProps?) => Object`

这个参数比较复杂,其根据传递的参数,类型的不同而有不同的语义。

这个的作用,是将我们要进行 dispatch() 的动作,进行包装,然后映射到属性。

具体而言,对于 connect() 的这个参数,我们可以:不提供;提供一个对象;提供一个函数

不提供

如果不提供这个参数,那么在我们 connect() 后的组件中,不会包装 dispathc(),因此组件将会拥有一个属性方法 dispatch() ,我们可以用其来进行事件的派发。

提供一个函数

如果我们提供一个函数,这个参数最大可以拥有两个参数,而根据其拥有参数的不同,意义又是不一样的。

但是,无论是几个参数,最终我们提供的这个函数,其第一个参数都会是 dispatch(),我们在这个函数中调用此函数来进行事件的派发。

我们在这个函数中,通过将 dispatch 函数与 action creator 以某种方式绑定在一起来返回一个纯对象。

只有一个参数

在这样的情况下,返回对象中的内容,都会作为组件的 props 而存在,我们可以直接用 this.increment() 的形式来派发事件。

const mapDispatchToProps = dispatch => {
return {
// dispatching plain actions
increment: () => dispatch({ type: 'INCREMENT' }),
decrement: () => dispatch({ type: 'DECREMENT' }),
reset: () => dispatch({ type: 'RESET' })
}
}

当然,我们也可以将其转发到一个 actionCreator

const mapDispatchToProps = dispatch => {
return {
// explicitly forwarding arguments
onClick: event => dispatch(trackClick(event)),

// implicitly forwarding arguments
onReceiveImpressions: (...impressions) =>
dispatch(trackImpressions(impressions))
}
}

有两个参数

如果函数有两个参数,那么第二个参数,将会是我们传递给 组件的 props ,而每当这个组件收到新的 prop 的时候,此函数都会被调用。

这就意味着,我们可能不是在组件进行重新渲染的时候重新绑定新的 props 到 action dispatchers,而是在 组件的 props 改变时干这个事情。

// Binds on component re-rendering
render() {
return <button onClick={() => this.props.toggleTodo(this.props.todoId)} />
}

const mapDispatchToProps = dispatch => {
return {
toggleTodo: todoId => dispatch(toggleTodo(todoId))
}
}
// Binds on props change
render() {
return <button onClick={() => this.props.toggleTodo()} />
}

const mapDispatchToProps = (dispatch, ownProps) => {
return {
toggleTodo: () => dispatch(toggleTodo(ownProps.todoId))
}
}

作为函数的返回值

mapDispatchToProps 函数必须返回纯对象:

  • 这个对象中的所有字段都会被注入到我们的属性中,其值通常是一个被调用时会分发事件的函数。
  • 如果作为值的函数中使用了 action creator ,那么一个约定就是将此函数的键命名来和 action creator 一致。
const increment = () => ({ type: 'INCREMENT' })
const decrement = () => ({ type: 'DECREMENT' })
const reset = () => ({ type: 'RESET' })

const mapDispatchToProps = dispatch => {
return {
// dispatching actions returned by action creators
increment: () => dispatch(increment()),
decrement: () => dispatch(decrement()),
reset: () => dispatch(reset())
}
}

mapDispatchToProps 的返回值将会 合并 到我们 connect 后的组件的 props 中。我们就可以直接调用他进行事件的分发了。

bindActionCreators

上面的一节中,我们来手动在 mapDispatchToProps 这个函数进行处理我们要派发的动作,比较麻烦。所以呢,Redux 提供了一个函数来简化我们的事情。

bindActionCreators 将一个对象中值是 action creators 的对象转换为一个 键相同,但是每个 action creator 都被一个 dispatch() 进行了包装的函数,这样我们就可以直接调用这个 action creator 的包装函数来分发事件了。

bindActionCreators 接收两个参数:

  1. 一个 函数(一个 action creator),或者是一对象(每个字段都是一个 action creator)
  2. dispatch

通过 bindActionCreators包装后的函数会自动的转发这些参数,因此我们不需要进行手动处理。

import { bindActionCreators } from 'redux'

const increment = () => ({ type: 'INCREMENT' })
const decrement = () => ({ type: 'DECREMENT' })
const reset = () => ({ type: 'RESET' })

// binding an action creator
// returns (...args) => dispatch(increment(...args))
const boundIncrement = bindActionCreators(increment, dispatch)

// binding an object full of action creators
const boundActionCreators = bindActionCreators(
{ increment, decrement, reset },
dispatch
)
// returns
// {
// increment: (...args) => dispatch(increment(...args)),
// decrement: (...args) => dispatch(decrement(...args)),
// reset: (...args) => dispatch(reset(...args)),
// }

在我们的 mapDispatchToProps 这个参数中我们就可以这样使用:

import { bindActionCreators } from 'redux'
// ...

function mapDispatchToProps(dispatch) {
return bindActionCreators({ increment, decrement, reset }, dispatch)
}

// component receives props.increment, props.decrement, props.reset
connect(
null,
mapDispatchToProps
)(Counter)

mapDispatchToProps之前我们说过,如果我们提供了这个参数,那么就不会再收到默认的dispatch() 。我们可以手动他把他加在我们的 mapDispatchToProps 的返回中。虽然大多数时候我们不需要这样。

import { bindActionCreators } from 'redux'
// ...

function mapDispatchToProps(dispatch) {
return {
dispatch,
...bindActionCreators({ increment, decrement, reset }, dispatch)
}
}

提供一个对象

我们在 React 中分发事件的流程其实也很传统:定义一个 action creator,然后将其包装在一个类似 (…args) => dispatch(actionCreator(…args)) 的函数中,然后将这个包装函数传递成为组件的属性。

因此呢,我们的 connect() 也支持将 mapDispatchToProps 传递为一个对象。如果我们传递的这个对象中都是 action creator,那么,connect() 会自动的调用 bindActionCreators

官方推荐的就是,一直使用这个方式。

但是需要注意的是:

mapDispatchToProps 对象每个字段都被假设为一个 action creator

我们的组件不会再拥有一个 dispatch prop

// React Redux does this for you automatically:
dispatch => bindActionCreators(mapDispatchToProps, dispatch)

因此,我们的 mapDIspatchToProps 可以简单写成这样:

const mapDispatchToProps = {
increment,
decrement,
reset
}

总结

  1. 不提供 mapDispatchToPropsstore.dispatch 会注入到我们组件中 this.props.dispatch
  2. mapDispatchToProps 是一个完全由 action creator 组成的对象。那么将此对象中的所有 action creator 放在一个包装函数中(可直接调用派发事件),注入到我们的组件中。this.props.action_creator_wrap_function … ,当然,这个名称由我们自己定。
  3. mapDispatchToProps 是一个函数。

mergeProps?: (stateProps, dispatchProps, ownProps) => Object

这个参数,这个决定了我们包装后的组件的属性是如何确定的。如果我们不提供这个参数,我们包装后的组件默认情况下将会拥有 { ...ownProps, ...stateProps, ...dispatchProps }

这个函数最多可以拥有三个参数:他们将会是mapStateToProps(), mapDispatchToProps() 的返回值,及包装后组件的 props

  • stateProps
  • dispatchProps
  • ownProps

相当于在此对我们的组件最终拥有的属性进行一个过滤和选择。

options?: Object

connect() Returns

connect() 的返回值,是一个包装函数,其将我们的组件进行包装,包装后的组件会拥有一些由 connect() 注入的属性。

import { login, logout } from './actionCreators'

const mapState = state => state.user
const mapDispatch = { login, logout }

// first call: returns a hoc that you can use to wrap any component
const connectUser = connect(
mapState,
mapDispatch
)

// second call: returns the wrapper component with mergedProps
// you may use the hoc to enable different components to get the same behavior
const ConnectedUserLogin = connectUser(Login)
const ConnectedUserProfile = connectUser(Profile)

大多数的情况下,这个包装函数都会被正确的调用,而不需要我们存储在一个临时变量中。

import { login, logout } from './actionCreators'

const mapState = state => state.user
const mapDispatch = { login, logout }

// call connect to generate the wrapper function, and immediately call
// the wrapper function to generate the final wrapper component.

export default connect(
mapState,
mapDispatch
)(Login)

使用示例

注入 dispatch,不监听 Store

export default connect()(TodoApp)

注入所有的 action creator,但不监听 Store

import * as actionCreators from './actionCreators'

export default connect(
null,
actionCreators
)(TodoApp)

注入dispatch和所有全局 State 的字段 (别这样干)

这样干会让任何的性能优化失效。因为每个状态的变更都会导致全部重新渲染。通常我们的做法是在不同的组件上,connect(),并监听 State 中的一部分。

// don't do this!
export default connect(state => state)(TodoApp)

注入 dispatch 和 todos

function mapStateToProps(state) {
return { todos: state.todos }
}

export default connect(mapStateToProps)(TodoApp)

注入 todos 和所有的 action creators

import * as actionCreators from './actionCreators'

function mapStateToProps(state) {
return { todos: state.todos }
}

export default connect(
mapStateToProps,
actionCreators
)(TodoApp)

将所有的 action creators 注入成为一个属性

import * as actionCreators from './actionCreators'
import { bindActionCreators } from 'redux'

function mapStateToProps(state) {
return { todos: state.todos }
}

function mapDispatchToProps(dispatch) {
return { actions: bindActionCreators(actionCreators, dispatch) }
}

export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoApp)

注入特定的 action creator

import { addTodo } from './actionCreators'
import { bindActionCreators } from 'redux'

function mapStateToProps(state) {
return { todos: state.todos }
}

function mapDispatchToProps(dispatch) {
return bindActionCreators({ addTodo }, dispatch)
}

export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoApp)

以 mapDispatchToProps 是对象的形式注入 action creators

import { addTodo, deleteTodo } from './actionCreators'

function mapStateToProps(state) {
return { todos: state.todos }
}

const mapDispatchToProps = {
addTodo,
deleteTodo
}

export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoApp)

将不同的action creators 注入到不同的属性中(分组)

import * as todoActionCreators from './todoActionCreators'
import * as counterActionCreators from './counterActionCreators'
import { bindActionCreators } from 'redux'

function mapStateToProps(state) {
return { todos: state.todos }
}

function mapDispatchToProps(dispatch) {
return {
todoActions: bindActionCreators(todoActionCreators, dispatch),
counterActions: bindActionCreators(counterActionCreators, dispatch)
}
}

export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoApp)

根据包装后的组件选择性的注入一些属性

import * as actionCreators from './actionCreators'

function mapStateToProps(state) {
return { todos: state.todos }
}

function mergeProps(stateProps, dispatchProps, ownProps) {
return Object.assign({}, ownProps, {
todos: stateProps.todos[ownProps.userId],
addTodo: text => dispatchProps.addTodo(ownProps.userId, text)
})
}

export default connect(
mapStateToProps,
actionCreators,
mergeProps
)(TodoApp)