个性化阅读
专注于IT技术分析

使用Redux在JavaScript中保持不变

点击下载

本文概述

在一个日趋丰富且复杂的JavaScript应用程序生态系统中, 要管理的状态比以往任何时候都多:当前用户, 已加载的帖子列表等。

需要事件历史记录的任何数据集都可以被认为是有状态的。管理状态可能很困难且容易出错, 但是使用不可变数据(而不是可变数据)和某些支持技术(就本文而言, 即Redux)可以提供很大帮助。

不可变数据具有局限性, 即一旦创建就不能更改, 但它也有很多好处, 特别是在引用与值相等方面, 这可以大大加快依赖经常比较数据的应用程序的速度(检查是否需要更新) , 例如)。

使用不可变状态使我们可以编写代码, 从而可以快速判断状态是否已更改, 而无需对数据进行递归比较, 这通常要快得多。

本文将介绍Redux在通过动作创建者, 纯函数, 组成的化简器, Redux-saga和Redux Thunk的不纯动作以及最终将Redux与React一起使用来管理状态时的实际应用。也就是说, Redux有很多替代方案, 例如基于MobX, Relay和Flux的库。

Redux组件。

为什么要使用Redux?

将Redux与其他大多数状态容器(例如MobX, Relay和大多数其他基于Flux的实现)区分开的关键方面是Redux具有只能通过”操作”(纯JavaScript对象)进行修改的单个状态, 该状态会分配给Redux商店。大多数其他数据存储区的状态都包含在React组件本身中, 允许你拥有多个存储区和/或使用可变状态。

反过来, 这会导致商店的reducer(一种对不变数据进行操作的纯函数)执行并可能更新状态。此过程强制执行单向数据流, 这更易于理解和确定性。

Redux流。

由于Redux减速器是对不变数据进行操作的纯函数, 因此在输入相同的情况下, 它们始终会产生相同的输出, 从而使其易于测试。这是一个减速器的例子:

import Immutable from 'seamless-immutable'

const initialState = Immutable([]) // create immutable array via seamless-immutable

/**
 * a reducer takes a state (the current state) and an action object (a plain JavaScript object that was dispatched via dispatch(..) and potentially returns a new state.
 */
function addUserReducer(state = initialState, action) {
    if (action.type === 'USERS_ADD') {
      return state.concat(action.payload)
    }
    
    return state // note that a reducer MUST return a value
}

// somewhere else...

store.dispatch({ type: 'USERS_ADD', payload: user }) // dispatch an action that causes the reducer to execute and add the user

通过处理纯函数, Redux可以轻松支持许多用变异状态通常不易完成的用例, 例如:

  • 时间旅行(回到过去的状态)
  • 记录(跟踪每个动作以找出导致商店中的突变的原因)
  • 协作环境(例如GoogleDocs, 其中的操作是纯JavaScript对象, 可以序列化, 通过有线发送并在另一台计算机上重播)
  • 轻松的错误报告(只需发送已分派的动作列表, 然后重播它们以获取完全相同的状态)
  • 优化的渲染(至少在将虚拟DOM作为状态函数进行渲染的框架中, 例如React:由于不可变性, 你可以通过比较引用(而不是递归比较对象)来轻松判断是否发生了更改)
  • 可以轻松测试减速器, 因为可以轻松地对纯功能进行单元测试

动作创建者

Redux的动作创建者有助于保持代码的清洁和可测试。请记住, Redux中的”动作”无非就是描述应发生的突变的普通JavaScript对象。话虽如此, 一次又一次地写出相同的对象是重复的并且容易出错。

Redux中的动作创建者只是一个辅助函数, 它返回一个描述突变的普通JavaScript对象。这有助于减少重复的代码, 并使所有动作都集中在一个位置:

    export function usersFetched(users) {
      return {
        type: 'USERS_FETCHED', payload: users, }
    }
    
    export function usersFetchFailed(err) {
      return {
        type: 'USERS_FETCH_FAILED', payload: err, }
    }
    
    // reducer somewhere else...
    const initialState = Immutable([]) // create immutable array via seamless-immutable
    
    /**
     * a reducer takes a state (the current state) and an action object (a plain JavaScript object that was dispatched via dispatch(..) and potentially returns a new state.
     */
    function usersFetchedReducer(state = initialState, action) {
        if (action.type === 'USERS_FETCHED') {
          return Immutable(action.payload)
        }
        
        return state // note that a reducer MUST return a value
    }

将Redux与不可变库一起使用

虽然化简器和操作的本质使它们易于测试, 而没有不变性帮助程序库, 但是没有什么可以保护你免受对象变异的影响, 这意味着对所有化简器的测试必须特别健壮。

考虑以下代码示例, 该示例在没有库来保护你的情况下会遇到问题:

const initialState = []

function addUserReducer(state = initialState, action) {
    if (action.type === 'USERS_ADD') {
      state.push(action.payload) // NOTE: mutating action!!
      return state
    }
    
    return state // note that a reducer MUST return a value
}

在此代码示例中, 时间旅行将被破坏, 因为以前的状态现在将与当前状态相同, 纯组件可能不会更新(或重新渲染), 因为对该状态的引用即使数据已更改也未更改包含已更改, 并且突变很难通过推理得出。

没有不变性库, 我们将失去Redux提供的所有好处。因此, 强烈建议使用不变性帮助程序库, 例如immutable.js或Seamless-immutable, 尤其是在一个大型团队中, 需要多动手接触代码时。

无论使用哪种库, Redux的行为都相同。让我们比较一下两者的优缺点, 以便你可以选择最适合你的用例的一种:

Immutable.js

Immutable.js是一个由Facebook建立的库, 具有更具功能性的样式, 可处理数据结构, 例如Maps, Lists, Sets和Sequences。它的不可变持久数据结构库在不同状态之间执行的复制量最少。

优点:

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

缺点:

  • 无法与现有的JS库(即lodash, ramda)无缝运行
  • 需要与(toJS / fromJS)之间来回转换, 尤其是在水合/脱水和渲染过程中

无缝不变

Seamless-immutable是一个用于不可变数据的库, 该库一直向下兼容ES5。

它基于ES5属性定义函数(例如defineProperty(..))来禁用对象的突变。因此, 它与现有的库(例如lodash和Ramda)完全兼容。也可以在生产版本中禁用它, 从而提供潜在的显着性能提升。

优点:

  • 与现有的JS库(即lodash, ramda)无缝兼容
  • 无需额外的代码即可支持转换
  • 可以在生产版本中禁用检查, 从而提高性能

缺点:

  • 没有结构共享-对象/数组复制较浅, 因此处理大型数据集的速度较慢
  • 内存效率不高

Redux和多个Reducer

Redux的另一个有用功能是可以将reducer组合在一起。这使你可以创建更复杂的应用程序, 并且在任何大小的应用程序中, 你不可避免地会具有多种状态类型(当前用户, 已加载的帖子列表, 等等)。 Redux通过自然提供功能CombineReducers支持(并鼓励)此用例:

import { combineReducers } from 'redux'
import currentUserReducer from './currentUserReducer'
import postsListReducer from './postsListReducer'

export default combineReducers({
  currentUser: currentUserReducer, postsList: postsListReducer, })

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

Redux中的不正确操作

默认情况下, 你只能将纯JavaScript对象调度到Redux。但是, 借助中间件, Redux可以支持不纯净的操作, 例如获取当前时间, 执行网络请求, 将文件写入磁盘等等。

“中间件”是指可以拦截正在调度的动作的功能的术语。一旦被拦截, 它就可以执行诸如转换操作或调度异步操作之类的操作, 这与其他框架(例如Express.js)中的中间件非常相似。

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

Redux Thunk

Redux Thunk通过使用thunk(返回其他可链接函数的函数)来支持Redux中的不正确操作。要使用Redux-Thunk, 你必须首先将Redux Thunk中间件安装到商店:

    import { createStore, applyMiddleware } from 'redux'
    import thunk from 'redux-thunk'
    
    const store = createStore(
      myRootReducer, applyMiddleware(thunk), // here, we apply the thunk middleware to R
    )

现在, 我们可以通过将一个thunk分配给Redux存储来执行不正确的操作(例如执行API调用):

    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
      }
    )

需要特别注意的是, 使用重排程序可能会使你的代码难以测试, 并且使得通过代码流进行推理变得更加困难。

Redux传奇

Redux-saga通过称为生成器的ES6(ES2015)功能和功能/纯辅助函数库支持不纯行为。生成器的优点在于可以恢复和暂停它们, 并且它们的API合约使它们非常易于测试。

让我们看看如何使用sagas来提高以前的重击方法的可读性和可测试性!

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

    import { createStore, applyMiddleware } from 'redux'
    import createSagaMiddleware from 'redux-saga'
    
    import rootReducer from './rootReducer'
    import rootSaga from './rootSaga'
    
    // create the saga middleware
    const sagaMiddleware = createSagaMiddleware()
    
    // mount the middleware to the store
    const store = createStore(
      rootReducer, applyMiddleware(sagaMiddleware), )
    
    // run our saga!
    sagaMiddleware.run(rootSaga)

请注意, 必须以传奇形式调用run(..)函数才能开始执行。

现在, 创建我们的传奇:

    import { call, put, takeEvery } from 'redux-saga/effects' // these are saga effects we'll use
    
    export function *fetchUsers(action) {
      try {
        const users = yield call(api.fetchUsers)
        yield put(usersFetched(users))
      } catch (err) {
        yield put(usersFetchFailed(err))
      }
    }
    
    export default function *rootSaga() {
      yield takeEvery('USERS_FETCH', fetchUsers)
    }

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

rootSaga会对名为takeEvery的函数进行一次调用, 该函数执行以USERS_FETCH类型调度的每个动作, 并以执行的动作调用fetchUsers传奇。如我们所见, 这为Redux创建了一个非常可预测的副作用模型, 使其易于测试!

测试Sagas

让我们看看发生器如何使我们的sagas易于测试。在本部分中, 我们将使用mocha来运行单元测试和chai进行断言。

因为sagas产生普通的JavaScript对象并在生成器中运行, 所以我们可以轻松地测试它们执行正确的行为而根本没有任何模拟!请记住, call, take, put等仅仅是被Redux-saga中间件截获的普通JavaScript对象。

    import { take, call } from 'redux-saga/effects'
    import { expect } from 'chai'
    import { rootSaga, fetchUsers } from '../rootSaga'
    
    describe('saga unit test', () => {
      it('should take every USERS_FETCH action', () => {
        const gen = rootSaga() // create our generator iterable
        expect(gen.next().value).to.be.eql(take('USERS_FETCH')) // assert the yield block does have the expected value
        expect(gen.next().done).to.be.equal(false) // assert that the generator loops infinitely
      })
      
      it('should fetch the users if successful', () => {
        const gen = fetchUsers()
        expect(gen.next().value).to.be.eql(call(api.fetchUsers)) // expect that the call effect was yielded
        
        const users = [ user1, user2 ] // some mock response
        expect(gen.next(users).value).to.be.eql(put(usersFetched(users))
      })
      
      it('should fail if API fails', () => {
        const gen = fetchUsers()
        expect(gen.next().value).to.be.eql(call(api.fetchUsers)) // expect that the call effect was yielded
        
        const err = { message: 'authentication failed' } // some mock error
        expect(gen.throw(err).value).to.be.eql(put(usersFetchFailed(err))
      })
    })

使用React

尽管Redux并没有绑定到任何特定的伴随库, 但它与React.js的结合尤其出色, 因为React组件是纯函数, 它们以状态为输入, 并生成虚拟DOM作为输出。

React-Redux是React和Redux的帮助程序库, 消除了连接两者的大部分繁琐工作。为了最有效地使用React-Redux, 让我们回顾一下呈现组件和容器组件的概念。

呈现组件描述了事物在视觉上的外观, 仅取决于其呈现的道具;他们从道具中调用回调来调度动作。它们是纯手工编写的, 与Redux之类的状态管理系统无关。

另一方面, 容器组件描述了事物应该如何工作, 了解Redux, 直接调度Redux操作以执行变异, 并且通常由React-Redux生成。它们通常与演示组件配对, 以提供其道具。

Redux中的演示组件和容器组件。

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

    const HelloWorld = ({ count, onButtonClicked }) => (
      <div>
        <span>Hello! You've clicked the button {count} times!</span>
        <button onClick={onButtonClicked}>Click me</button>
      </div>
    )
    
    HelloWorld.propTypes = {
      count: PropTypes.number.isRequired, onButtonClicked: PropTypes.func.isRequired, }

请注意, 这是一个”哑巴”组件, 完全依赖于其功能支柱。这很棒, 因为它使React组件易于测试且易于编写。让我们看一下现在如何将此组件连接到Redux, 但首先让我们介绍一下高级组件。

高阶组件

React-Redux提供了一个名为connect(..)的辅助函数, 该函数从知道Redux的”哑” React组件创建一个更高阶的组件。

React通过组合来强调可扩展性和可重用性, 即在将组件包装在其他组件中时。包装这些组件可以更改其行为或添加新功能。让我们看看如何从知道Redux的展示性组件(容器组件)中创建更高阶的组件。

方法如下:

    import { connect } from 'react-redux'
    
    const mapStateToProps = state => { // state is the state of our store
      // return the props that we want to use for our component
      return {
        count: state.count, }
    }
    
    const mapDispatchToProps = dispatch => { // dispatch is our store dispatch function
      // return the props that we want to use for our component
      return {
        onButtonClicked: () => {
          dispatch({ type: 'BUTTON_CLICKED' })
        }, }
    }
    
    // create our enhancer function
    const enhancer = connect(mapStateToProps, mapDispatchToProps)
    
    // wrap our "dumb" component with the enhancer
    const HelloWorldContainer = enhancer(HelloWorld)
    
    // and finally we export it
    export default HelloWorldContainer

请注意, 我们定义了两个函数mapStateToProps和mapDispatchToProps。

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

mapDispatchToProps也是一个纯函数, 但是(dispatch:(Action)=> void)之一返回从Redux调度函数计算出的对象。同样, 该对象将与传递给包装组件的道具合并。

现在要使用我们的容器组件, 我们必须使用React-Redux中的Provider组件来告诉容器组件要使用什么存储:

    import { Provider } from 'react-redux'
    import { render } from 'react-dom'
    
    import store from './store' // where ever your Redux store resides
    import HelloWorld from './HelloWorld'
    
    render(
      (
        <Provider store={store}>
          <HelloWorld />
        </Provider>
      ), document.getElementById('container')
    )

Provider组件将存储向下传播到订阅Redux存储的所有子组件, 从而将所有内容都保留在一个位置, 并减少了错误或突变点!

使用Redux建立代码置信度

借助Redux的最新知识, 其众多支持库以及与React.js的框架连接, 你可以通过状态控制轻松地限制应用程序中的突变数。反过来, 强大的状态控制使你可以更快地移动并更加自信地创建可靠的代码库。

赞(0)
未经允许不得转载:srcmini » 使用Redux在JavaScript中保持不变

评论 抢沙发

评论前必须登录!