本文做者:胡子大哈javascript
原文连接:https://scriptoj.com/topic/178/如何在非-react-项目中使用-reduxhtml
转载请注明出处,保留原文连接和做者信息。java
一、前言react
二、单纯使用 Redux 的问题git
2.一、问题 1:代码冗余github
2.二、问题2:没必要要的渲染redux
三、React-redux 都干了什么缓存
四、构建本身项目中的 “Provider” 和 “connect”性能优化
4.一、包装渲染函数app
4.二、避免没有必要的渲染
五、总结
六、练习
最近在知乎上看到这么一个问题: 请教 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 的基本语法。
咱们用一个很是简单的例子来说解一下在非 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_HEADER
、UPDATE_BODY
、UPDATE_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
元素里面的内容分别为:Header
、Body
、Footer
。咱们打算 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 管理了这个简单例子里面可能会发生改变的状态。但这里有几个问题:
编写完一个渲染的函数之后,须要手动进行第一次渲染初始化;而后手动经过 store.subscribe
监听 store 的数据变化,在数据变化的时候进行从新调用渲染函数。这都是重复的代码和没有必要的工做,并且还可能提供了忘了subscribe
的可能。
上面的例子中,程序进行一次初始化渲染,而后数据更新的渲染。3 个渲染函数里面都有一个 log。两次渲染最佳的状况应该只有 6 个 log。
可是你能够看到出现了 12 个log,那是由于后续修改 UPDATE_XXX
,除了会致使该数据进行渲染,还会致使其他两个数据从新渲染(即便它们其实并无变化)。store.subscribe
一股脑的调用了所有监听函数,但其实数据没有变化就没有必要从新渲染。
以上的两个缺点在功能较为复杂的时候会愈来愈凸显。
能够看到,单纯地使用 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 的时候传入了 mapStateToProps
,mapStateToProps
做用很关键,它起到了提取数据的做用,能够把这个组件须要的数据按需从 store 中提取出来。
实际上,在 React-redux 的内部:Provider
接受 store 做为参数,而且经过 context 把 store 传给全部的子组件;子组件经过 connect
包裹了一层高阶组件,高阶组件会经过 context 结合 mapStateToProps
和 store
而后把里面数据传给被包裹的组件。
若是你看不懂上面这段话,能够参考 动手实现 React-redux。说白了就是 connect
函数实际上是在 Provider
的基础上构建的,没有 Provider
那么 connect
也没有效果。
React 的组件负责渲染工做,至关于咱们例子当中的 render 函数。相似 React-redux 围绕组件,咱们围绕着渲染函数,能够给它们提供不一样于、可是功能相似的 Provider
和 connect
。
Provider
和 connect
参考 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 */ }
store
、mapStateToProps
、render
都有了,剩下就是把 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,仍是没有解决无关数据变化致使的从新渲染问题。
在上面的代码中,每次 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 。
经过本文能够知道,在非 React 项目结合 Redux 不能简单粗暴地将两个使用起来。要根据项目须要构建这个场景下须要的工具库来简化关于 store 的操做,固然能够直接参照 React-redux 的实现来进行对应的绑定。
也能够总结出,其实 React-redux 的 connect 帮助咱们隐藏了不少关于store 的操做,包括 store 的数据变化的监听从新渲染、数据对比和性能优化等。
对本文所讲内容有兴趣的朋友能够作一下本文配套的练习: