如何在非 React 项目中使用 Redux
- 本文作者:胡子大哈
- 原文链接:https://scriptoj.com/topic/178/如何在非-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_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 管理了这个简单例子里面可能会发生改变的状态。但这里有几个问题:
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 的时候传入了 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
。
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 */
}
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,还是没有解决无关数据变化导致的重新渲染问题。
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、练习
对本文所讲内容有兴趣的朋友可以做一下本文配套的练习:
如何在非 React 项目中使用 Redux的更多相关文章
- 如何优雅地在React项目中使用Redux
前言 或许你当前的项目还没有到应用Redux的程度,但提前了解一下也没有坏处,本文不会安利大家使用Redux 概念 首先我们会用到哪些框架和工具呢? React UI框架 Redux 状态管理工具,与 ...
- 优雅的在React项目中使用Redux
概念 首先我们会用到哪些框架和工具呢? React UI框架 Redux 状态管理工具,与React没有任何关系,其他UI框架也可以使用Redux react-redux React插件,作用:方便在 ...
- 在react项目中使用redux or mobx?
主要比较参数: 库体积,打包项目体积 开发体验 性能对比 在对比参数前首先分析一下redux和mobx的设计模式,redux和mobx都没有使用传统的mvc/mvvm形式,而且他们使用flux结构也略 ...
- redux在react项目中的应用
今天想跟大家分享一下redux在react项目中的简单使用 1 1.redux使用相关的安装 yarn add redux yarn add react-redux(连接react和redux) 2. ...
- react项目中引入了redux后js控制路由跳转方案
如果你的项目中并没有用到redux,那本文你可以忽略 问题引入 纯粹的单页面react应用中,通过this.props.history.push('/list')就可以进行路由跳转,但是加上了redu ...
- React项目中实现右键自定义菜单
最近在react项目中需要实现一个,右键自定义菜单功能.找了找发现纯react项目里没有什么工具可以实现这样的功能,所以在网上搜了搜相关资料.下面我会附上完整的组件代码. (注:以下代码非本人原创,具 ...
- 深入浅出TypeScript(5)- 在React项目中使用TypeScript
前言 在第二小节中,我们讨论了利用TypeScript创建Web项目的实现,在本下节,我们讨论一下如何结合React创建一个具备TypeScript类型的应用项目. 准备 Webpack配置在第二小节 ...
- react项目中实现元素的拖动和缩放实例
在react项目中实现此功能可借助 react-rnd 库,文档地址:https://github.com/bokuweb/react-rnd#Screenshot .下面是实例运用: import ...
- React项目中使用Mobx状态管理(二)
并上一节使用的是普通的数据状态管理,不过官方推荐使用装饰器模式,而在默认的react项目中是不支持装饰器的,需要手动启用. 官方参考 一.添加配置 官方提供了四种方法, 方法一.使用TypeScrip ...
随机推荐
- hdu2612 Find a way BFS
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=2612 思路: 裸的BFS,对于Y,M分别进行BFS,求出其分别到达各个点的最小时间: 然后对于@的点, ...
- RabbitMQ学习2---使用场景
RabbitMQ主页:https://www.rabbitmq.com/ AMQP AMQP协议是一个高级抽象层消息通信协议,RabbitMQ是AMQP协议的实现.它主要包括以下组件: 1.Serve ...
- Java学习笔记——Linux下安装配置MySQL
山重水复疑无路,柳暗花明又一村 --游山西村 系统:Ubuntu 16.04LTS 1\官网下载mysql-5.7.18-linux-glibc2.5-x86_64.tar.gz2\建立工作组:$su ...
- springcloud(五):熔断监控Hystrix Dashboard和Turbine
Hystrix-dashboard是一款针对Hystrix进行实时监控的工具,通过Hystrix Dashboard我们可以在直观地看到各Hystrix Command的请求响应时间, 请求成功率等数 ...
- nginx-tomcat-memcached架构文档说明(转)
800x600 Normal 0 7.8 磅 0 2 false false false EN-US ZH-CN X-NONE MicrosoftInternetExplorer4 st1\:*{be ...
- 【原创】iOS图片预览(支持缩放和移动)
1.传入图片 PreViewController.h: #import <UIKit/UIKit.h> @interface PreViewController : UIViewContr ...
- 项目管理之 Objective-C 编码规范
目录: 一.格式化代码 二.命名 命名要求 1. 类的命名: 规则: 大驼峰命名法,每个单词的首字母都采用大写字母.一般添加业务前缀.后缀一般是当前类的种类. ViewController:后缀:Vi ...
- C++ STL快速入门
在数月之前的机试中第一次体验到STL的威力,因为自己本来一直在用C语言做开发,很多数据结构都是自己造的,比如链表.队列等,第一次接触C++ STL后发现这些数据结构都已经给我提供好了,我直接拿去调用就 ...
- 网络编程4之UDP协议
一.定义 UDP 是User Datagram Protocol的简称, 中文名是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联) 参考模型中一种[无 ...
- 软件raid 5
软件raid 5的实现 RAID 5 是一种存储性能.数据安全和存储成本兼顾的存储解决方案. RAID 5可以理解为是RAID 0和RAID 1的折中方案.RAID 5可以为系统提供数据安全保障,但保 ...