redux在react中的应用(基础篇)

作者: Jack_Lo | 来源:发表于2017-02-25 01:27 被阅读8843次

    上一篇【react的SPA实践】里,我们留下了一些问题,比如深层嵌套的组件之间的通讯问题。

    虽然我们通过尽量减少深层次嵌套的方式,可以规避这个问题,但是这毕竟没有解决问题。

    这一篇我们主要讲react如何搭配 redux 使用,从而构建一个更完(niu)善(bi)的react应用。

    官方文档对redux的介绍:

    Redux是JavaScript状态容器,提供可预测化的状态管理。

    注意,redux的集成是非必选的。通过之前的内容介绍我们了解到,单枪匹马的react构建的页面也可以运作得很好。

    你可能会对redux的编程方式感到无所适从。

    然而,不管是出于构建大型应用考虑,还是作为进一步的技能提升,redux绝对是一个值得你花时间去学习的框架。

    我到底是想说什么。。。

    好吧,啰嗦就到此为止,下面介绍一下本篇的内容大纲:

    • store
    • action
    • reducer
    • dispatch
    • middleware

    引子

    我们知道在react中数据是单向流动的。

    啥?我之前没有说过???

    好吧,漏了。

    所谓的数据单向流动,顾名思义,就是数据是从一个方向传播下去的意思。

    我们假设数据中心是一个负责发行身份证的地方政府,每个人都可以到这里来领取自己的身份证,但是如果你对自己的身份证头像不满意,你不能自己去村口拍个十块钱的大头贴贴上去,也不能自己修改上面的个人信息,这样的身份证是不被承认的(弄不好可能还得坐牢),正确的操作方式应该是,带着你的修改意见,去到地方政府,找相关工作人员帮你重新办理,办理完之后再发放给你。

    这个例子当中,数据中心我们视为父组件,身份证就是子组件通过父组件接收到的props,你不能直接修改自己的身份证,也就是你不能直接this.props.name = '葬爱',只能通知父组件去修改这个props,即调用父组件的提供的changeName方法来操作:this.props.changeName('葬爱'),这个过程事实上是修改了父组件的state,从而使得子元素接收到的数据也发生了改变。

    所以数据由始至终都是从父元素流向子元素,我们称为数据单向流动。

    我们回想一下我们之前构建过的所有react应用,数据都是由最顶层父组件(页面组件)一层层向下传递的。

    这也是深层次的组件之间通讯困难的原因:数据的传递是单向的,子组件的数据只能就近获取,但是真正的数据源却离得太远,没有捷径可以直接通知数据源更新状态。

    redux的出现改变了react的这种窘迫处境,它提供了整个应用的唯一数据源store,这个数据源是随处可以访问的,不需要靠父子相传,并且还提供了(间接)更新这个数据源的方法,并且是随处可使用的!

    天呐,这也太好了吧。

    子组件A:老子再也不用千里迢迢等你传个props了!
    子组件B:老子再也不用盼天盼地等你传个handler了!

    然而,事情并没有那么简单。

    前方高能!

    store

    store 是redux提供的唯一数据源,它存储了整个应用的state,并且提供了获取state的方法,即store.getState()

    就没了???修改数据的方法还没说呢???

    store是只读的。

    redux没有提供直接修改数据的方法,改变state的唯一方法就是触发(dispatchaction

    action

    action 是一个用于描述已发生事件的普通对象。

    简单来说,就是“你干了一件什么事情”。

    但是单单讲了你干的事情,我们并不知道你干的这件事产生了什么牛逼效果,于是有了一个专门负责描述某个行动对应产生某种效果的机构,叫做 reducer

    reducer

    reducer 只是一个接收state和action,并返回新的state的函数。

    它就像是一部法典,根据你所做的事情,提供对应的后果,这个后果直接对数据源起作用。

    什么鬼比喻。。。

    并且,这部法典应该尽可能覆盖所有的犯罪事件类型!

    你还来。。。

    ok,现在我们知道怎么修改数据源了,首先必须先定义好我们即将做的事情,也就是定义一个action,跟着,我们需要相对应地补充我们做的这件事要怎么影响数据源,于是我们根据这个action补充了一个reducer,最后我们触发这个action:store.dispatch(action),数据源就会根据reducer定义好的规则来更新自己了。

    就是这样!

    是不是很简单?

    是呀是呀!

    但好像还是没什么概念,连个action长什么样都不知道。

    没事,我们接着讲。

    场景代入

    我们假设整个应用只有两个组件,一个身份证组件,一个弹窗组件,那么应用的state树应该是这样子的:

    // store.getState()
    
    {
      card: {
        name: 'Jack',
        picture: 'a.jpg'
      },
      dialog: {
        status: false
      }
    }
    

    action本质上是一个普通的js对象,因为它只是一个用来描述事件的对象,先来两个现成的action看看:

    {
      type: 'CHANGE_NAME',
      name: '葬爱'
    }
    
    {
      type: 'CHANGE_PICTURE',
      picture: 'b.jpg'
    }
    

    发现了吗?action都会带一个type属性,这个属性是必选的,而其他的内容,比如name、picture等等,都是可选的,它们是由action携带,最后传递给reducer的内容,就好比我说我要改名字,这是事件,但是我没有说我要改成什么名字,这个操作就不完整,所以我还需要补充说,我要改名叫“葬爱”,所以我还需要提供一个name给你,这才能完整实现一个动作。这些附属的参数,我们称为 payload(载荷)。

    我们说过payload是可选的,也就是说,有些动作的触发是不需要其他信息的,比如“激活弹窗”、“关闭弹窗”等等,这类动作只需要一个type就可以传达意思了:

    {
      type: 'SHOW_DIALOG'
    }
    
    {
      type: 'CLOSE_DIALOG'
    }
    

    于是,一个完整的触发动作是这样的:

    // 修改名字
    dispatch({
      type: 'CHANGE_NAME',
      name: '葬爱'
    })
    // 激活弹窗
    dispatch({
      type: 'SHOW_DIALOG'
    })
    

    我们已经知道如何触发一个动作,现在我们来了解如何接收并处理这个动作,也就是补充一个reducer。

    可以认为,reducer就是根据传入的各种action不同,相对应对state进行处理,最后返回一个新state的函数。

    那么可以推断出这个函数需要的参数至少是:当前的state,以及一个action。

    事实也正是如此,下面是一个真实的reducer:

    function reducer(state, action) {
      switch (action.type) {
        case 'CHANGE_NAME':
        return {
          card: {
            name: action.name, // 使用action携带的新name
            picture: state.card.picture  // 不需要修改,使用旧state的值
          },
          dialog: state.dialog  // 不需要修改,使用旧state的值
        }
    
        case 'SHOW_DIALOG':
        return {
          card: state.card,  // 不需要修改,使用旧state的值
          dialog: {
            status: true
          }
        }
    
        case 'CLOSE_DIALOG':
        return {
          card: state.card,  // 不需要修改,使用旧state的值
          dialog: {
            status: false
          }
        }
    
        default:
        return state  // 没有匹配的action type,返回原来的state
      }
    }
    

    如上,reducer接收一个修改前的state和一个action,然后通过判断actionType的方式来进行不同操作(没有匹配的actionType则默认返回原state),而这个操作的最终目的就是 拼装 一个新的state,并最终return,这样就达到更新state的目的了!

    看到这里你可能会有疑问,为什么不直接拿state,然后修改它state.card.name = action.name,最后return state不就好了吗?

    这是一个不好的实践,因为state是一个对象,直接修改state是会对其他引用了state的地方产生影响的,这种影响我们称为 副作用 ,而redux规定reducer必须是 纯函数 ,纯函数是没有副作用的。

    reducer 一定要保持纯净,只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。 ——redux官方文档

    关于纯函数的概念,可以查找相关介绍,这里我们不细讲,只需要记住,纯函数的特征是,只要两次输入的值是一样的,那这两次输出值就一定是一样,不可能出现差异。

    我们回到上面的示例代码来,仔细观察这个state,我们发现它由两部分构成,分别是carddialog,但是这两者之间并没有关联,那么我们可不可以把它们拆分出来,分别管理呢?

    我们可以试试看:

    function cardReducer(state, action) {
      switch (action.type) {
        case 'CHANGE_NAME':
        return {
          name: action.name, // 使用action携带的新name
          picture: state.card.picture  // 不需要修改,使用旧state的值
        }
    
        default:
        return state  // 没有匹配的action type,返回原来的state
      }
    }
    
    function dialogReducer(state, action) {
      switch (action.type) {
        case 'SHOW_DIALOG':
        return {
          status: true
        }
    
        case 'CLOSE_DIALOG':
        return {
          status: false
        }
    
        default:
        return state  // 没有匹配的action type,返回原来的state
      }
    }
    
    function reducer(state, action) {
      return {
        card: cardReducer(state.card, action),
        dialog: dialogReducer(state.dialog, action)
      }
    }
    
    export default reducer
    

    这个由两个reducer组成的大reducer所实现的功能,跟之前只有一个reducer实现的功能是没有差别的!我们成功地将原本比较笼统的reducer拆分成了多个专注于不同功能的reducer,如果你愿意的话,还可以继续拆下去,但目前来看没有这个必要。

    这是个很重要的思想!强调一遍,拆分reducer是一个很重要的思想,这是我们之后编写reducer的最基本方式。

    我们可以看得出来,reducer的拆分规则是跟state的结构紧密相关的,所以很多时候拆分reducer并不是什么困难的事情。

    而且过程还很爽有木有。。。

    好吧,可能只是我的个人感受。

    combineReducers

    值得一提的是,redux对reducer的合并提供了一些便捷的方法,我们可以这么写:

    function card(state, action) {
      switch (action.type) {
        case 'CHANGE_NAME':
        return {
          name: action.name, // 使用action携带的新name
          picture: state.card.picture  // 不需要修改,使用旧state的值
        }
    
        default:
        return state  // 没有匹配的action type,返回原来的state
      }
    }
    
    function dialog(state, action) {
      switch (action.type) {
        case 'SHOW_DIALOG':
        return {
          status: true
        }
    
        case 'CLOSE_DIALOG':
        return {
          status: false
        }
    
        default:
        return state  // 没有匹配的action type,返回原来的state
      }
    }
    
    export default combineReducers({
      card,
      dialog
    })
    

    需要特别注意,使用combineReducers来合并reducer,需要子reducer的名字跟对应要接收的state的key一致,所以你看到上面我们把原来的子reducer名字分别从cardReducer和dialogReducer,改为了card和dialog。

    其实,好像也没多方便啦。。。

    不过可能逼格高一些就是了。

    使用combineReducers的目的是为了减少模板代码,但是在这里其实并没有显得多必要,反而,这种方式可能会限制子reducer的命名,显得不灵活,所以不建议使用。

    以上,就是redux的全部思想了,真的,除了获取state和改变state之外,我们没有更多的需求了。

    接下来我们就介绍如何在react项目中集成redux。

    集成

    假设我们原有项目(react+react-router)的入口文件App.jsx如下:

    import React, { Component, PropTypes } from 'react'
    import ReactDom from 'react-dom'
    import { Router, Route, IndexRoute, hashHistory } from 'react-router'
    
    import Index from './containers/index'
    import About from './containers/about'
    
    ReactDom.render(
      <Router history={hashHistory}>
        <Route path="/" component={Index}/>
      </Router>,
      document.getElementById('root')
    )
    

    在react中使用redux需要装两个包:

    npm install redux react-redux --save
    

    然后我们在现有项目中引入:

    import { createStore, applyMiddleware } from 'redux'
    import { Provider, connect } from 'react-redux'
    

    createStore是用来创建store的,applyMiddleware是用来整合接入middleware的,这个我们后面再介绍,Provider是用来实现store的全局访问的,而connect则是用来针对某个展示组件,创建包裹这个组件的容器组件的,有点绕,这个我们也留在后面再介绍。

    import React, { Component, PropTypes } from 'react'
    import ReactDom from 'react-dom'
    import { Router, Route, IndexRoute, hashHistory } from 'react-router'
    
    import Index from './containers/index'
    
    // 引入redux
    import { createStore, applyMiddleware } from 'redux'
    import { Provider, connect } from 'react-redux'
    
    // 引入reducer
    import reducer from './reducers'
    
    // 创建一个初始化的state
    var initState = {
      card: {
        name: 'Jack',
        picture: 'a.jpg'
      },
      dialog: {
        status: false
      }
    }
    
    // 创建store
    const store = createStore(reducer, initState)
    
    ReactDom.render(
      <Provider store={store}>
        <Router history={hashHistory}>
          <Route path="/" component={Index}/>
        </Router>
      </Provider>,
      document.getElementById('root')
    )
    

    这样,我们就集成了redux!

    请仔细思考以上带注释的部分,结合之前介绍的内容,重新整理一遍思路,加深理解。

    整个结构搭建好了之后,就可以来实际操作了!

    connect

    我们把视线转移到页面上,这个例子当中有一个页面Index,我们来看:

    import React from 'react'
    
    import Card from '../../components/Card'
    import Dialog from '../../components/Dialog'
    
    const Index = React.createClass({
      render () {
        return <div className="g-index">
          <Card />
          <Dialog />
        </div>
      }
    })
    

    我们需要将这个页面需要的state以及修改state的handler注入给它;这个state现在由store管理了,而这个handler也应该对应变成dispatch(action)!

    那么,我们怎么才能关联到store呢?这就要依靠上面提到的 connect了。

    connect需要知道你这个组件需要获取哪些state,以及你需要dispatch哪些action,我们通过下面的方式来表达:

    import React from 'react'
    
    import Card from '../../components/Card'
    import Dialog from '../../components/Dialog'
    
    const Index = React.createClass({
      render () {
        return <div className="g-index">
          <Card />
          <Dialog />
        </div>
      }
    })
    
    function mapStateToProps(state) {
      return state
    }
    
    function mapDispatchToProps(dispatch) {
      return {
        changeName () {
          dispatch({
            type: 'CHANGE_NAME',
            name: '葬爱'
          })
        },
        showDialog () {
          dispatch({
            type: 'SHOW_DIALOG'
          })
        }
      }
    }
    
    export default connect(mapStateToProps, mapDispatchToProps)(Index)
    

    mapStateToProps中传入的state就是整个应用的state,mapDispatchToProps中传入的dispatch就是store的dispatch!

    通过这样的定义和映射,我们的Index最终获得了所需要的数据以及方法,这些都是在Index中直接通过props可以访问到的!

    由于Index是页面,所以我们在mapStateToProps中直接return了整个state,代表整个state都可以通过props访问到:

    const Index = React.createClass({
      render () {
        return <div className="g-index">
          <Card />
          <Dialog />
          <button onClick={this.props.changeName}>change name</button>
          <button onClick={this.props.showDialog}>show dialog</button>
        </div>
      }
    })
    

    我们再来看看两个子组件:

    // Card.jsx
    import React from 'react'
    import { connect } from 'react-redux'
    
    const Card = (props) => {
      return <div className="m-card">
        <div>
          姓名:{props.name}
        </div>
        <div>
          照片:{props.picture}
        </div>
      </div>
    }
    
    function mapStateToProps(state) {
      var info = state.card
    
      return {
        name: info.name,
        picture: info.picture
      }
    }
    
    export default connect(mapStateToProps)(Card)
    
    // Dialog.jsx
    import React from 'react'
    import { connect } from 'react-redux'
    
    const Dialog = (props) => {
      if (!props.status) {
        return null
      }
    
      return <div className="m-dialog">
        <div>
          dialog
        </div>
        <button onClick={props.hideDialog}>close</button>
      </div>
    }
    
    function mapStateToProps(state) {
      var info = state.dialog
    
      return {
        status: info.status
      }
    }
    
    function mapDispatchToProps(dispatch) {
      return {
        hideDialog () {
          dispatch({
            type: 'CLOSE_DIALOG'
          })
        }
      }
    }
    
    export default connect(mapStateToProps, mapDispatchToProps)(Dialog)
    

    同样的,需要哪些state,就在mapStateToProps里return,需要哪些handler就在mapDispatchToProps里return,是不是很简单?

    这样我们就实现:在首页点击“change name”按钮,Card组件就会修改名字,点击“show dialog”按钮,弹窗(Dialog)就会出现;在弹窗里点击“close”按钮,弹窗就会关闭。

    这里可以发现,现在我们已经不在乎组件嵌套有多深了,因为不管在哪里,都能实现全局的通讯!

    之前我们遗留的问题终于得到解决了!

    开心得就地打滚起来。

    但是回头一想,你可能会问,虽然这样是实现了,但是感觉很麻烦,为什么不让每个组件直接接收store,然后在组件内直接通过store.getState()来获取需要的state,再直接通过store.dispatch(action)的方式来改变state?

    明明之前就是这么介绍的啊,难道你前面讲的都是在骗我???

    我想你可能是这样想的:

    import React from 'react'
    
    import Card from '../../components/Card'
    import Dialog from '../../components/Dialog'
    
    const Index = React.createClass({
      render () {
        var store = this.props.store
        var state = store.getState()
    
        return <div className="g-index">
          <div>
            {state.card.name}
          </div>
          <button onClick={this.changeName}>change name</button>
        </div>
      },
      changeName () {
        this.props.store.dispatch({type: 'CHANGE_NAME', name: '葬爱'})
      }
    })
    
    export default Index
    

    这样好像简单了很多!

    事实上,上面这段代码是会报错的,因为store并不在props上;但是,我们确实可以直接获取到store,正确的获取方式是这样的:

    import React from 'react'
    
    import Card from '../../components/Card'
    import Dialog from '../../components/Dialog'
    
    const Index = React.createClass({
      render () {
        var store = this.context.store
        var state = store.getState()
    
        return <div className="g-index">
          <div>
            {state.card.name}
          </div>
          <button onClick={this.changeName}>change name</button>
        </div>
      },
      changeName () {
        this.context.store.dispatch({type: 'CHANGE_NAME', name: '葬爱'})
      }
    })
    
    Index.contextTypes = {
      store: React.PropTypes.object
    }
    
    export default Index
    

    我们说过,Provider是用来实现store的全局访问的,它的原理就是使用react的context,context是可以实现跨组件之间传递的,而且不需要像props一样显式地表达,是完全透明的存在!

    现在你可以看到上面的例子是可以正常运行的,没有报错。

    但是,当你点击“change name”按钮的时候,页面上的state.card.name并没有相对应改变!

    纳尼???

    我们知道,在react中,state和props的改变会促使react重新渲染(rerender)当前组件;但是,使用context传递的属性却不会!也就是说,即便你的store发生了变化,当前组件也不会知道,所以不会重新渲染页面!

    要实现及时的渲染,我们需要订阅store的变化!store提供了一个底层api:subscribe

    但是,使用subscribe的方式,会改变我们整个程序的工作模式,从原本的数据主动流动,变成了订阅/发布!我们需要去主动地监听store的变化,订阅每一个在组件里使用到的state的变化,然后再更新组件,这让我们的工作变得复杂,并且容易出问题!

    一个最简单的例子,假设我在subscribe的listener里执行了dispatch,猜猜会是什么情况?

    没错,死循环。

    因此,为了避免这样的事情发生,简化我们的工作,redux(准确来说是react-redux)提供了connect的方式,来避免变成被动的订阅/发布模式,维持react原本的自动更新方式。

    所以还是老老实实用connect吧,人家官方文档也说了:

    这个方法做了性能优化来避免很多不必要的重复渲染。(这样你就不必为了性能而手动实现 React 性能优化建议 中的 shouldComponentUpdate 方法。)

    不管你信不信,反正我不信,实质是浅比较,对于比较复杂的对象,依然无能为力。

    好吧,这些都是题外话,我们接着讲。

    middleware

    引用官方文档的话:

    middleware提供的是位于 action 被发起之后,到达 reducer 之前的扩展点。

    也就是说,一个action刚发起,到生效之前,这一个过程中,你可以相对应做一些内容扩展,比如你希望在某一次action前后打印一些内容,那么,你可以这么做:

    function mapDispatchToProps(dispatch) {
      return {
        changeName () {
          console.log('即将改名……')
          dispatch({
            type: 'CHANGE_NAME',
            name: '葬爱'
          })
          console.log('完成改名!')
        }
      }
    }
    

    这确实可以实现,但是很low,而且,如果你想对所有action都生效,那你绝对不会这么去做,因为一个个都这么写你会写到吐血。使用 middleware 可以轻松搞定!

    官方推荐了一个用于实现相同功能的middleware——redux-logger,我们可以直接使用:

    // App.jsx
    import createLogger from 'redux-logger'
    const store = createStore(reducer, initState, applyMiddleware(createLogger()))
    

    (以上省略若干代码。。。)

    就是这么简单!

    其实middleware是一个完全可选的部分,因为一般情况下,我们并不需要对一个action的过程做什么处理,我们的工作主要是编写action以及相对于的reducer,这就够了。

    那为什么还要特地介绍它?因为接下来我们将会涉及到异步action的相关实现。就目前来看,我们所编写的action都是同步的,但是web开发中,我们常常会需要用到异步操作,比如从服务端拉取数据,那这时候的action应该怎么写?

    你可能已经稍微有点思路了,根据现有的方式,其实实现起来也不难。但是过程会比较麻烦,你可以试试看;使用middleware的话可以大大简化这部分工作,不过这都是后话了,我们下回再介绍。

    因为不知不觉又超字数了。。。

    总结

    好啦,基本的概念先讲到这里,通过上面的内容,我们已经可以在react中集成redux,并且尝试着编写一些简单的逻辑,使得整个应用运作起来!

    而且我们还解决了之前颇为诟病的嵌套组件之间的通讯问题!

    那么,到了这一步之后,我们就可以真正投入业务当中使用了吗?

    答案当然是——no!

    尼玛。。。

    学了这么多你跟我说还不能用,我要掀桌子了!

    事实上,我们还有一些东西没有规范好,比如action的type,如果应用做大起来,type的数量会非常庞大,而每个type又都必须是唯一的,如何解决这个命名冲突问题?当state变得庞大复杂的时候,如何高效地更新需要变动的部分?如何编写异步action?

    这些问题,我们会在之后的内容当中一一讲解。

    参考文献:
    react官方文档:https://facebook.github.io/react/
    redux中文官方文档:http://cn.redux.js.org/

    相关文章

      网友评论

        本文标题:redux在react中的应用(基础篇)

        本文链接:https://www.haomeiwen.com/subject/ucouwttx.html