zoukankan      html  css  js  c++  java
  • 如何在非 React 项目中使用 Redux

    目录

    • 1、前言
    • 2、单纯使用 Redux 的问题
      • 2.1、问题 1:代码冗余
      • 2.2、问题2:不必要的渲染
    • 3、React-redux 都干了什么
    • 4、构建自己项目中的 “Provider” 和 “connect”
      • 4.1、包装渲染函数
      • 4.2、避免没有必要的渲染
    • 5、总结
    • 6、练习

    1、前言

    最近在知乎上看到这么一个问题: 请教 redux 与 eventEmitter? - 知乎

    最近一个小项目中(没有使用 react),因为事件、状态变化稍多,想用 redux 管理,可是并没有发现很方便。..

    说起 Redux,我们一般都说 React。似乎 Redux 和 React 已经是天经地义理所当然地应该捆绑在一起。而实际上,Redux 官方给自己的定位却是:

    Redux is a predictable state container for JavaScript apps.

    Redux 绝口不提 React,它给自己的定义是 “给 JavaScript 应用程序提供可预测的状态容器”。也就是说,你可以在任何需要进行应用状态管理的 JavaScript 应用程序中使用 Redux。

    但是一旦脱离了 React 的环境,Redux 似乎就脱缰了,用起来桀骜不驯,难以上手。本文就带你分析一下问题的原因,并且提供一种在非 React 项目中使用 Redux 的思路和方案。这不仅仅对在非 React 的项目中使用 Redux 很有帮助,而且对理解 React-redux 也大有裨益。

    本文假设读者已经熟练掌握 React、Redux、React-redux 的使用以及 ES6 的基本语法。

    2、单纯使用 Redux 的问题

    我们用一个非常简单的例子来讲解一下在非 React 项目中使用 Redux 会遇到什么问题。假设页面上有三个部分,header、body、footer,分别由不同模块进行渲染和控制:

    <div id='header'></div>
    <div id='body'></div>
    <div id='footer'></div>
    

    这个三个部分的元素因为有可能会共享和发生数据变化,我们把它存放在 Redux 的 store 里面,简单地构建一个 store:

    const appReducer = (state, action) => {
      switch (action.type) {
        case 'UPDATE_HEADER':
          return Object.assign(state, { header: action.header })
        case 'UPDATE_BODY':
          return Object.assign(state, { body: action.body })
        case 'UPDATE_FOOTER':
          return Object.assign(state, { footer: action.footer })
        default:
          return state
      }
    }
    
    const store = Redux.createStore(appReducer, {
      header: 'Header',
      body: 'Body',
      footer: 'Footer'
    })
    

    很简单,上面定义了一个 reducer,可以通过三个不同的 action:UPDATE_HEADERUPDATE_BODYUPDATE_FOOTER 来分别进行对页面数据进行修改。

    有了 store 以后,页面其实还是空白的,因为没有把 store 里面的数据取出来渲染到页面。接下来构建三个渲染函数,这里使用了 jQuery:

    /* 渲染 Header */
    const renderHeader = () => {
      console.log('render header')
      $('#header').html(store.getState().header)
    }
    renderHeader()
    
    /* 渲染 Body */
    const renderBody = () => {
      console.log('render body')
      $('#body').html(store.getState().body)
    }
    renderBody()
    
    /* 渲染 Footer */
    const renderFooter = () => {
      console.log('render footer')
      $('#footer').html(store.getState().footer)
    }
    renderFooter()
    

    现在页面就可以看到三个 div 元素里面的内容分别为:HeaderBodyFooter。我们打算 1s 以后通过 store.dispatch 更新页面的数据,模拟 app 数据发生了变化的情况:

    /* 数据发生变化 */
    setTimeout(() => {
      store.dispatch({ type: 'UPDATE_HEADER', header: 'New Header' })
      store.dispatch({ type: 'UPDATE_BODY', body: 'New Body' })
      store.dispatch({ type: 'UPDATE_FOOTER', footer: 'New Footer' })
    }, 1000)
    

    然而 1s 以后页面没有发生变化,这是为什么呢?那是因为数据变化的时候并没有重新渲染页面(调用 render 方法),所以需要通过 store.subscribe 订阅数据发生变化的事件,然后重新渲染不同的部分:

    store.subscribe(renderHeder)
    store.subscribe(renderBody)
    store.subscribe(renderFooter)
    

    好了,现在终于把 jQuery 和 Redux 结合起来了。成功了用 Redux 管理了这个简单例子里面可能会发生改变的状态。但这里有几个问题:

    2.1、问题 1:代码冗余

    编写完一个渲染的函数以后,需要手动进行第一次渲染初始化;然后手动通过 store.subscribe 监听 store 的数据变化,在数据变化的时候进行重新调用渲染函数。这都是重复的代码和没有必要的工作,而且还可能提供了忘了subscribe 的可能。

    2.2、问题2:不必要的渲染

    上面的例子中,程序进行一次初始化渲染,然后数据更新的渲染。3 个渲染函数里面都有一个 log。两次渲染最佳的情况应该只有 6 个 log。

    但是你可以看到出现了 12 个log,那是因为后续修改 UPDATE_XXX ,除了会导致该数据进行渲染,还会导致其余两个数据重新渲染(即使它们其实并没有变化)。store.subscribe 一股脑的调用了全部监听函数,但其实数据没有变化就没有必要重新渲染。

    以上的两个缺点在功能较为复杂的时候会越来越凸显。

    3、React-redux 都干了什么

    可以看到,单纯地使用 Redux 和 jQuery 目测没有给我们带来什么好处和便利。是不是就可以否了 Redux 在非 React 项目中的用处呢?

    回头想一下,为什么 Redux 和 React 结合的时候并没有出现上面所提到的问题?你会发现,其实 React 和 Redux 并没有像上面这样如此暴力地结合在一起。在 React 和 Redux 这两个库中间其实隔着第三个库:React-redux。

    在 React + Redux 项目当中,我们不需要自己手动进行 subscribe,也不需要手动进行过多的性能优化,恰恰就是因为这些脏活累活都由 React-redux 来做了,对外只提供了一个 Provider 和 connect 的方法,隐藏了关于 store 操作的很多细节。

    所以,在把 Redux 和普通项目结合起来的时候,也可以参考 React-redux,构建一个工具库来隐藏细节、简化工作。

    这就是接下来需要做的事情。但在构建这个简单的库之前,我们需要了解一下 React-redux 干了什么工作。 React-redux 给我们提供了什么功能?在 React-redux 项目中我们一般这样使用:

    import { connect, Provider } from 'react-redux'
    
    /* Header 组件 */
    class Header extends Component {
      render () {
        return (<div>{this.props.header}</div>)
      }
    }
    
    const mapStateToProps = (state) => {
      return { header: state.header }
    }
    Header = connect(mapStateToProps)(Header)
    
    /* App 组件 */
    class App extends Component {
      render () {
        return (
          <Provider store={store}>
            <Header />
          </Provider>
        )
      }
    }
    

    我们把 store 传给了 Provider,然后其他组件就可以使用 connect 进行取数据的操作。connect 的时候传入了 mapStateToPropsmapStateToProps 作用很关键,它起到了提取数据的作用,可以把这个组件需要的数据按需从 store 中提取出来。

    实际上,在 React-redux 的内部:Provider 接受 store 作为参数,并且通过 context 把 store 传给所有的子组件;子组件通过 connect 包裹了一层高阶组件,高阶组件会通过 context 结合 mapStateToProps 和 store 然后把里面数据传给被包裹的组件。

    如果你看不懂上面这段话,可以参考 动手实现 React-redux。说白了就是 connect 函数其实是在 Provider 的基础上构建的,没有 Provider 那么 connect也没有效果。

    React 的组件负责渲染工作,相当于我们例子当中的 render 函数。类似 React-redux 围绕组件,我们围绕着渲染函数,可以给它们提供不同于、但是功能类似的 Provider 和 connect

    4、构建自己项目中的 Provider 和 connect

    4.1、包装渲染函数

    参考 React-redux,下面假想出一种类似的 provider 和 connect 可以应用在上面的 jQuery 例子当中:

    /* 通过 provider 生成这个 store 对应的 connect 函数 */
    const connect = provider(store)
    
    /* 普通的 render 方法 */
    let renderHeader = (props) => {
      console.log('render header')
      $('#header').html(props.header)
    }
    
    /* 用 connect 取数据传给 render 方法 */
    const mapStateToProps = (state) => {
      return { header: state.header }
    }
    renderHeader = connect(mapStateToProps)(renderHeader)
    

    你会看到,其实我们就是把组件换成了 render 方法而已。用起来和 React-redux 一样。那么如何构建 provider 和 connect 方法呢?这里先搭个骨架:

    const provider = (store) => {
      return (mapStateToProps) => { // connect 函数
        return (render) => {
          /* TODO */
        }
      }
    }
    

    provider 接受 store 作为参数,返回一个 connect 函数;connect 函数接受 mapStateToProps 作为参数返回一个新的函数;这个返回的函数类似于 React-redux 那样接受一个组件(渲染函数)作为参数,它的内容就是要接下来要实现的代码。当然也可以用多个箭头的表示方法:

    const provider = (store) => (mapStateToProps) => (render) => {
      /* TODO */
    }
    

    storemapStateToPropsrender 都有了,剩下就是把 store 里面的数据取出来传给 mapStateToProps 来获得 props;然后再把 props 传给 render 函数。

    const provider = (store) => (mapStateToProps) => (render) => {
      /* 返回新的渲染函数,就像 React-redux 的 connect 返回新组件 */
      const renderWrapper = () => {
        const props = mapStateToProps(store.getState())
        render(props)
      }
      return renderWrapper
    }
    

    这时候通过本节一开始假想的代码已经可以正常渲染了,同样的方式改写其他部分的代码:

    /* body */
    let renderBody = (props) => {
      console.log('render body')
      $('#body').html(props.body)
    }
    mapStateToProps = (state) => {
      return { body: state.body }
    }
    renderBody = connect(mapStateToProps)(renderBody)
    
    /* footer */
    let renderFooter = (props) => {
      console.log('render footer')
      $('#footer').html(props.footer)
    }
    mapStateToProps = (state) => {
      return { footer: state.footer }
    }
    renderFooter = connect(mapStateToProps)(renderFooter)
    

    虽然页面已经可以渲染了。但是这时候调用 store.dispatch 是不会导致重新渲染的,我们可以顺带在 connect 里面进行 subscribe:

    const provider = (store) => (mapStateToProps) => (render) => {
      /* 返回新的渲染函数,就像 React-redux 返回新组件 */
      const renderWrapper = () => {
        const props = mapStateToProps(store.getState())
        render(props)
      }
      /* 监听数据变化重新渲染 */
      store.subscribe(renderWrapper)
      return renderWrapper
    }
    

    赞。现在 store.dispatch 可以导致页面重新渲染了,已经原来的功能一样了。但是,看看控制台还是打印了 12 个 log,还是没有解决无关数据变化导致的重新渲染问题。

    4.2、避免没有必要的渲染

    在上面的代码中,每次 store.dispatch 都会导致 renderWrapper 函数执行, 它会把 store.getState() 传给 mapStateToProps 来计算新的 props 然后传给 render

    实际上可以在这里做手脚:缓存上次的计算的 props,然后用新的 props 和旧的 props 进行对比,如果两者相同,就不调用 render

    const provider = (store) => (mapStateToProps) => (render) => {
      /* 缓存 props */
      let props
      const renderWrapper = () => {
        const newProps = mapStateToProps(store.getState())
        /* 如果新的结果和原来的一样,就不要重新渲染了 */
        if (shallowEqual(props, newProps)) return
        props = newProps
        render(props)
      }
      /* 监听数据变化重新渲染 */
      store.subscribe(renderWrapper)
      return renderWrapper
    }
    

    这里的关键点在于 shallowEqual。因为 mapStateToProps 每次都会返回不一样的对象,所以并不能直接用 === 来判断数据是否发生了变化。这里可以判断两个对象的第一层的数据是否全相同,如果相同的话就不需要重新渲染了。例如:

    const a = { name: 'jerry' }
    const b = { name: 'jerry' }
    
    a === b // false
    shallowEqual(a, b) // true
    

    这时候看看控制台,只有 6 个 log 了。成功地达到了性能优化的目的。这里 shallowEqual 的实现留给读者自己做练习。

    到这里,已经完成了类似于 React-redux 的一个 Binding,可以愉快地使用在非 React 项目当中使用了。完整的代码可以看这个 gist 。

    5、总结

    通过本文可以知道,在非 React 项目结合 Redux 不能简单粗暴地将两个使用起来。要根据项目需要构建这个场景下需要的工具库来简化关于 store 的操作,当然可以直接参照 React-redux 的实现来进行对应的绑定。

    也可以总结出,其实 React-redux 的 connect 帮助我们隐藏了很多关于store 的操作,包括 store 的数据变化的监听重新渲染、数据对比和性能优化等。

    6、练习

    对本文所讲内容有兴趣的朋友可以做一下本文配套的练习:

    1. 实现一个 shallowEqual
    2. 给 provider 加入 mapDispatchToProps
  • 相关阅读:
    element ui 表单清空
    element ui 覆盖样式 方法
    element ui 修改表单值 提交无效
    element ui 抽屉里的表单输入框无法修改值
    element ui 抽屉首次显示 闪烁
    css 左侧高度 跟随右侧内容高度 自适应
    PICNUF框架
    elementui 抽屉组件标题 出现黑色边框
    vue 子组件跨多层调用父组件中方法
    vue 编辑table 数据 未点击提交,table里的数据就发生了改变(深拷贝处理)
  • 原文地址:https://www.cnblogs.com/huzidaha/p/7089565.html
Copyright © 2011-2022 走看看