作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
大卫·徐的头像

David Xu

作为Castle Global的首席架构师,David已经将几个移动应用程序从一个想法发展到全球数百万用户, Inc.

Expertise

Previously At

HIVE
Share

在丰富而复杂的JavaScript应用程序不断增长的生态系统中, 需要管理的状态比以往任何时候都多:当前用户, 加载的帖子列表, etc.

任何需要事件历史记录的数据集都可以被认为是有状态的. 管理状态可能很困难,而且容易出错, 而是处理不可变数据(而不是可变数据)和某些支持技术——即Redux, 对于本文的目的来说-可以有很大的帮助.

不可变数据有限制, 也就是说,它一旦被创建就不能被更改, 但它也有很多好处, 特别是在参照与价值平等方面, 这可以大大加快依赖于频繁比较数据的应用程序(检查是否需要更新, for example).

使用不可变状态允许我们编写能够快速判断状态是否发生变化的代码, 而不需要对数据进行递归比较, 这通常是很多, much faster.

本文将介绍Redux在通过操作创建者管理状态时的实际应用, 纯函数, 由还原剂, 用Redux-saga和Redux Thunk和, finally, 在React中使用Redux. 也就是说,Redux有很多替代品,比如MobX、Relay和基于Flux的库.

Why Redux?

将Redux与其他大多数状态容器(如MobX)区分开来的关键方面, Relay, 和大多数其他基于Flux的实现一样,Redux只有一个状态,只能通过“动作”(普通JavaScript对象)来修改。, 会被派往Redux仓库吗. 大多数其他数据存储的状态都包含在React组件本身中, 允许您拥有多个存储和/或使用可变状态.

这反过来又导致了商店的减速器, 对不可变数据进行操作的纯函数, 执行并可能更新状态. 此过程强制单向数据流,更容易理解且更具确定性.

The Redux Flow.

因为Redux reducer是对不可变数据进行操作的纯函数, 在相同的输入下,它们总是产生相同的输出, 使它们易于测试. 这里有一个减速器的例子:

从'seamless-immutable'中导入Immutable

const initialState = Immutable([]) //通过seamless-immutable创建不可变数组

/**
 *一个reducer接受一个状态(当前状态)和一个action对象(一个通过dispatch()方法分派的普通JavaScript对象)。..),并可能返回一个新状态.
 */
addUserReducer(state = initialState, action) {
    if (action.type === 'USERS_ADD') {
      return state.concat(action.payload)
    }
    
    return state //注意,reducer必须返回一个值
}

//在其他地方...

store.调度({类型:'USERS_ADD'), Payload: user}) //调度一个动作,导致reducer执行并添加用户

纯函数处理允许Redux轻松地支持许多用例,这些用例通常不容易通过变化状态完成, such as:

  • 时间旅行(回到以前的状态)
  • 日志记录(跟踪每一个操作,找出是什么导致了存储中的突变)
  • 协作环境(如GoogleDocs), 哪里的动作是纯JavaScript对象,可以序列化, 通过电报发送, 在另一台机器上重播)
  • 简单的bug报告(只需发送已调度的操作列表), 然后重新播放以获得完全相同的状态)
  • 优化的呈现(至少在将虚拟DOM呈现为状态函数的框架中是这样), 比如React:由于不变性, 通过比较参考资料,你可以很容易地判断出是否发生了变化, 而不是递归地比较对象)
  • 轻松测试reducer,因为纯函数可以轻松进行单元测试

行动的创造者

Redux的动作创建器有助于保持代码的整洁和可测试性. 请记住,Redux中的“动作”只不过是描述应该发生的变化的普通JavaScript对象. 话虽如此,一遍又一遍地写出相同的对象是重复的,而且容易出错.

Redux中的操作创建器只是一个辅助函数,它返回一个描述突变的普通JavaScript对象. 这有助于减少重复的代码,并保持所有的动作在一个地方:

    导出功能usersFetched(用户){
      return {
        类型:“USERS_FETCHED”,
        有效载荷:用户,
      }
    }
    
    导出函数usersFetchFailed(err) {
      return {
        类型:“USERS_FETCH_FAILED”,
        payload: err,
      }
    }
    
    //其他地方的reducer...
    const initialState = Immutable([]) //通过seamless-immutable创建不可变数组
    
    /**
     *一个reducer接受一个状态(当前状态)和一个action对象(一个通过dispatch()方法分派的普通JavaScript对象)。..),并可能返回一个新状态.
     */
    函数usersFetchedReducer(state = initialState, action) {
        if (action.type === ' users_fetch_') {
          返回不变(行动.payload)
        }
        
        return state //注意,reducer必须返回一个值
    }

在不可变库中使用Redux

虽然减速器和动作的本质使它们易于测试, 没有不变性助手库, 没有什么能保护你不受变异物体的伤害, 这意味着对所有减速器的测试必须特别可靠.

考虑下面的代码示例,您将遇到一个没有库来保护您的问题:

const initialState = []

addUserReducer(state = initialState, action) {
    if (action.type === 'USERS_ADD') {
      state.push(action.//注意:改变动作!!
      return state
    }
    
    return state //注意,reducer必须返回一个值
}

在这个代码示例中, 时间旅行将被打破,因为之前的状态现在将与当前状态相同, 纯组件可能不会更新(或重新呈现),因为对状态的引用没有改变,即使它包含的数据已经改变, 而突变是很难解释清楚的.

没有不变性库,我们就失去了Redux提供的所有好处. 因此,强烈建议使用不变性助手库,例如immutable.Js或无缝不可变,特别是当在一个多人接触代码的大型团队中工作时.

无论您使用哪个库,Redux的行为都是相同的. 让我们比较一下两者的优点和缺点,以便您能够选择最适合您的用例的一个:

Immutable.js

Immutable.Js是一个库, 由Facebook建立, 用更函数化的风格来处理数据结构, such as Maps, Lists, Sets, and Sequences. 它的不可变持久数据结构库在不同状态之间执行尽可能少的复制.

Pros:

  • 结构共享
  • 更新更有效率
  • 更高的内存效率
  • 有一套帮助器方法来管理更新

Cons:

  • 不能与现有的JS库(i.哦,是吗?
  • 需要转换到和从(toJS / fromJS), 尤其是在水合/脱水和渲染过程中

Seamless-immutable

Seamless-immutable是一个用于不可变数据的库,一直向后兼容到ES5.

它基于ES5属性定义函数,例如 defineProperty (..) 禁用对象上的突变. 因此,它与lodash和Ramda等现有库完全兼容. 它也可以在生产构建中禁用,从而提供潜在的显著性能增益.

Pros:

  • 与现有的JS库(如JS库)无缝协作.哦,是吗?
  • 不需要额外的代码来支持转换
  • 可以在生产构建中禁用检查,从而提高性能

Cons:

  • 没有结构共享-对象/数组是浅复制的,这使得大数据集的速度变慢
  • 内存效率不高

Redux和多重还原器

Redux的另一个有用特性是将reducer组合在一起的能力.这允许您创建更复杂的应用程序, 在任何规模可观的应用程序中, 您将不可避免地拥有多种类型的状态(当前用户), 加载的帖子列表, etc). Redux通过自然地提供函数来支持(并鼓励)这个用例 combineReducers:

从'redux'中导入{combineReducers}
导入currentUserReducer./ currentUserReducer '
导入postsListReducer./ postsListReducer '

导出默认combineReducers({
  currentUser: currentUserReducer,
  postsList: postsListReducer,
})

使用上面的代码,您可以拥有一个依赖于 currentUser 另一个依赖于 postsList. 这也提高了性能,因为任何单个组件将只订阅树中与它们相关的分支.

Redux中的不纯操作

默认情况下,您只能将普通JavaScript对象分派给Redux. 与中间件, however, Redux可以支持非纯操作,比如获取当前时间, 执行网络请求, 将文件写入磁盘, and so on.

“中间件”是用于拦截正在调度的操作的函数的术语. 一旦拦截, 它可以做一些事情,比如转换动作或分派异步动作, 与其他框架(如Express)中的中间件非常相似.js).

两个非常常见的中间件库是Redux Thunk和Redux-saga. Redux Thunk采用命令式风格编写,而Redux-saga采用函数式风格编写. 我们来比较一下.

Redux Thunk

Redux Thunk通过使用thks支持Redux内部的非纯操作, 返回其他可链函数的函数. 要使用Redux-Thunk,您必须首先将Redux Thunk中间件挂载到存储中:

    从redux中导入{createStore, applyMiddleware}
    从'redux-thunk'中导入thunk
    
    const store = createStore()
      myRootReducer,
      applyMiddleware(thunk), //这里,我们将thunk中间件应用到R
    )

现在我们可以执行非纯操作(比如执行API调用),通过向Redux存储分派一个数据:

    store.dispatch(
      dispatch => {
        return api.fetchUsers()
          .then(users  => dispatch(usersFetched(users)) // usersFetched is a function that returns a plain JavaScript object (Action)
          .catch(err => dispatch(usersFetchError(err)) // same with usersFetchError
      }
    )

重要的是要注意,使用thks会使代码难以测试,并且更难通过代码流进行推理.

Redux-saga

Redux-saga支持非纯操作 ES6 (ES2015) 称为生成器的特性和一个函数/纯帮助程序库. 生成器的伟大之处在于它们可以被恢复和暂停, 它们的API契约使得它们非常容易测试.

让我们看看如何使用传奇来提高之前的思维方法的可读性和可测试性!

首先,让我们将Redux-saga中间件安装到我们的存储中:

    从redux中导入{createStore, applyMiddleware}
    从“redux-saga”中导入createsagmiddleware
    
    导入rootReducer./rootReducer'
    导入rootSaga./rootSaga'
    
    //创建saga中间件
    const sagaMiddleware = createSagaMiddleware()
    
    //将中间件挂载到存储中
    const store = createStore()
      rootReducer,
      applyMiddleware (sagaMiddleware),
    )
    
    //运行我们的saga!
    sagaMiddleware.run(rootSaga)

Note that the run(..) 函数必须与saga一起调用,才能开始执行.

现在让我们创建我们的传奇:

    import {call, put, takeEvery} from 'redux-saga/effects' //这些是我们将要使用的saga特效
    
    导出函数*fetchUsers(action) {
      try {
        Const users = yield call(api.fetchUsers)
        收益率(usersFetched(用户)
      } catch (err) {
        收益率put (usersFetchFailed (err))
      }
    }
    
    *rootSaga() {
      yield takeEvery('USERS_FETCH', fetchUsers)
    }

我们定义了两个生成器函数,一个获取用户列表,另一个获取用户列表 rootSaga. 注意,我们没有调用 api.fetchUsers 直接,而是在调用对象中产生它. 这是因为Redux-saga拦截调用对象并执行其中包含的函数以创建一个纯环境(就您的生成器而言).

rootSaga 产生对所调用的函数的单个调用 takeEvery, 它将每个动作分配为类型 USERS_FETCH and calls the fetchUsers 和它所采取的行动. As we can see, 这为Redux创建了一个非常可预测的副作用模型, 这使得测试变得容易!

Testing Sagas

让我们看看生成器如何使我们的传奇易于测试. 我们将使用 mocha 在本部分中运行我们的单元测试和 chai 对断言.

因为传奇生成纯JavaScript对象,并在生成器中运行, 我们可以很容易地测试它们是否执行正确的行为,而不需要任何模拟! 记住这一点 call , take , put等都是被Redux-saga中间件拦截的普通JavaScript对象.

    从'redux-saga/effects'中导入{take, call}
    从“chai”导入{expect}
    导入{rootSaga, fetchUsers}../rootSaga'
    
    describe('saga unit test', () => {
      it('should take every USERS_FETCH action', () => {
        const gen = rootSaga() //创建生成器可迭代对象
        expect(gen.next().value).to.be.eql(take('USERS_FETCH')) //断言yield块是否有预期的值
        expect(gen.next().done).to.be.Equal (false) //断言生成器无限循环
      })
      
      it('should fetch the users if successful', () => {
        const gen = fetchUsers()
        expect(gen.next().value).to.be.eql(call(api.fetchUsers)) //期望产生调用效果
        
        Const users = [user1, user2] //一些模拟响应
        expect(gen.next(users).value).to.be.eql (put (usersFetched(用户)
      })
      
      it('should fail if API fails', () => {
        const gen = fetchUsers()
        expect(gen.next().value).to.be.eql(call(api.fetchUsers)) //期望产生调用效果
        
        Const err = {message: 'authentication failed'} //一些模拟错误
        expect(gen.throw(err).value).to.be.eql (put (usersFetchFailed (err))
      })
    })

使用React

虽然Redux没有绑定到任何特定的配套库,但它可以很好地与 React.js 因为React组件是纯函数,它以状态作为输入,并产生一个虚拟DOM作为输出.

React-Redux是React和Redux的辅助库,它消除了连接两者的大部分艰苦工作. 为了最有效地使用React-Redux, 让我们回顾一下表示组件和容器组件的概念.

表示组件描述了事物在视觉上应该是什么样子, depending solely on their props to render; they invoke callbacks from props to dispatch actions. 它们是手工编写的,完全纯净,并且不依赖于Redux这样的状态管理系统.

容器组件, 另一方面, 描述事物应该如何运作, 知道Redux吗, 直接调度Redux操作来执行变更,通常由React-Redux生成. 它们通常与表示组件配对,提供其道具.

Redux中的表示组件和容器组件.

让我们编写一个表示组件,并通过React-Redux将其连接到Redux:

    const HelloWorld = ({ count, onButtonClicked }) => (
      
Hello! 您已经点击{count}按钮多次!
) HelloWorld.propTypes = { 数:proptype.number.isRequired, onButtonClicked: proptype.func.isRequired, }

请注意,这是一个“哑”组件,完全依赖于它的道具来运行. 这很好,因为它使 React组件易于测试和编写. 现在让我们看看如何将这个组件连接到Redux, 但首先我们来了解一下什么是高阶分量.

高阶分量

React-Redux提供了一个名为 connect( .. ) 它可以从一个感知Redux的“愚蠢”React组件中创建一个更高阶的组件.

React通过组合强调可扩展性和可重用性, 哪一种情况是将组件封装在其他组件中. 包装这些组件可以改变它们的行为或添加新的功能. 让我们看看如何从我们的表示组件中创建一个更高阶的组件,该组件支持Redux——一个容器组件.

你应该这样做:

    从“react-redux”中导入{connect}
    
    const mapStateToProps = state => { // state is the state of our store
      //返回我们想要用于组件的道具
      return {
        count: state.count,
      }
    }
    
    const mapDispatchToProps = dispatch => { // dispatch is our store dispatch function
      //返回我们想要用于组件的道具
      return {
        onButtonClicked: () => {
          调度({type: 'BUTTON_CLICKED'})
        },
      }
    }
    
    //创建增强器函数
    const enhancer = connect(mapStateToProps, mapDispatchToProps)
    
    //用增强器包装我们的“哑”组件
    const HelloWorldContainer = enhancer(HelloWorld)
    
    //最后导出它
    导出默认HelloWorldContainer

注意,我们定义了两个函数, mapStateToProps and mapDispatchToProps .

mapStateToProps 是(state: Object)的一个纯函数,返回一个从Redux状态计算的对象. 该对象将与传递给包装组件的道具合并. 这也被称为选择器, 因为它选择了Redux状态的一部分来合并到组件的props中.

mapDispatchToProps 也是一个纯函数吗, but one of (dispatch: (Action) => void) that returns an object computed from the Redux dispatch function. 这个对象同样会与传递给包装组件的props合并.

现在,要使用容器组件,我们必须使用 Provider 组件来告诉容器组件要使用哪个store:

    从“react-redux”中导入{Provider}
    从react-dom中导入{render}
    
    从'导入存储'./store' //你的Redux存储所在的地方
    导入HelloWorld./HelloWorld'
    
    render(
      (
        
          
        
      ), document.getElementById(容器)
    )

The Provider 组件将存储向下传播到订阅Redux存储的任何子组件, 把所有东西都放在一个地方,减少错误或突变点!

用Redux建立代码信心

有了关于Redux的新知识, 它的众多支持库以及与React的框架连接.在Js中,您可以通过状态控制轻松地限制应用程序中的突变数量. 强大的国家控制, in turn, 让您更快地移动并更有信心地创建坚实的代码基础.

聘请Toptal这方面的专家.
Hire Now
大卫·徐的头像
David Xu

Located in 丹佛,科罗拉多州,美国

Member since 2016年10月20日

作者简介

作为Castle Global的首席架构师,David已经将几个移动应用程序从一个想法发展到全球数百万用户, Inc.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

Previously At

HIVE

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® community.