在React中,组件和组件之间通过props
传递数据的规范,极大地增强了组件之间的耦合性,而context
类似全局变量一样,里面的数据能被随意接触就能被随意修改,每个组件都能够改context
里面的内容会导致程序的运行不可预料。
Redux是一个独立专门用于做状态管理的JS库,帮助开发出行为稳定可预测的、易于测试的应用程序,通过react-redux,可集中式管理React应用中多个组件共享的状态。
1 前置知识,context和高阶组件
在实现自己的React-redux之前,需要先了解下以下2个东西:React的context和高阶组件。
1.1 React中的Context
在开发react项目时,我们会写很多的组件,同时组件之间嵌套组成一棵组件树,组件之间需要共享数据,如果通过props的方式传递数据的话,就需要将共享数据保存在这些组件的公共父节点组件的state,再通过props往下传递;如果还需要修改这些共享数据的话,需层层传递修改数据的回调函数,这简直就是地狱,完全无法维护。 为此,React的Context
提供了一种在组件之间共享数据的方式,而不必显式地通过组件树的逐层传递props
,其设计目的是为了共享那些对于一个组件树而言是“全局”的数据。 我们看一个具体示例,比如我们有这样一棵组件树,看如何通过Context
实现组件树的数据共享:
import React,{Component} from 'react';
import ReactDOM from 'react-dom/client';
import PropTypes from 'prop-types';
class Index extends Component {// 如果你要给组件设置 context,那么 childContextTypes 是必写的// 它的作用其实 propsType 验证组件 props 参数的作用类似。不过它是验证 getChildContext 返回的对象。static childContextTypes = {themeColor: PropTypes.string}constructor () {super()this.state = { themeColor: 'red' }}// 设置 context 的过程,它返回的对象就是 contextgetChildContext () {return { themeColor: this.state.themeColor }}render () {return (<div><Header /><Content /></div>)}
}
class Header extends Component {render () {return (<div><h2>Header</h2><Title /></div>)}
}
class Content extends Component {render () {return (<div><h2>Content</h2></div>)}
}
class Title extends Component {// static contextTypes = {themeColor: PropTypes.string}render () {return (// 获取context中的数据<h1 style={{ color: this.context.themeColor }}>Title</h1>)}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Index />
);
Index
把state.themeColor
放到某个地方,这个地方是每个Index
的子组件都可以访问到的。当某个子组件需要的时候就直接去那个地方拿就好了,而不需要一层层地通过props
来获取。不管组件树的层次有多深,任何一个组件都可以直接到这个公共的地方提取themeColor
状态。 Context
就是这么一个东西,某个组件只要往自己的Context
里面放了某些状态,这个组件之下的所有子组件都直接访问这个状态而不需要通过中间组件的传递。一个组件的Context
只有 它的子组件 能够访问,它的父组件是不能访问到的。
1.2 高阶组件
高阶组件在概念上很简单,但却非常常用、实用的东西,被大量 React.js 相关的第三方库频繁地使用,如果你能够灵活地使用高阶组件,可以让你代码更加优雅,复用性、灵活性更强。
高阶组件就是一个函数,传给它一个组件,它返回一个新的组件,这个新的组件会使用你传给它的组件作为子组件。
一个简单的高阶组件:
import React, { Component } from 'react'
export default (WrappedComponent) => {class NewComponent extends Component {// 可以做很多自定义逻辑render () {return <WrappedComponent />}}return NewComponent
}
它就是简单的构建了一个新的组件类NewComponent
,然后把传进入去的WrappedComponent
渲染出来,这个例子看着好像没啥用,但是我们可以在render()
之前做很多自定义逻辑,比如从远程获取数据,通过props传给子组件。
下面再给出个实用点的例子:
src/wrapWithRemoteData.js
import React, { Component } from 'react'
// 替代ajax
const getRemoteDate = key => {if (key==='user') return '张三';else if (key==='content') return 'Redux 是 JavaScript 状态容器,提供可预测化的状态管理。';
}
export default (WrappedComponent, key) => {class NewComponent extends Component {constructor () {super()this.state = { data: null }}componentWillMount () {let data = getRemoteDate(key)this.setState({ data })}render () {return <WrappedComponent data={this.state.data} />}}return NewComponent
}
src/InputWithUser.js
import React,{Component} from "react";
import wrapWithRemoteData from './wrapWithRemoteData'
class InputWithUserName extends Component {render () {return <>user: <input value={this.props.data} /></>}
}
export default wrapWithRemoteData(InputWithUserName, 'user')
src/textareaWithContent.js
import React,{Component} from "react";
import wrapWithRemoteData from './wrapWithRemoteData'
class TextareaWithContent extends Component {render () {return <textarea value={this.props.data} />}
}
export default wrapWithRemoteData(TextareaWithContent, 'content')
可以看到,wrapWithRemoteData
作为一个高阶组件,接收2个参数:
1.WrappedComponent: 被包装的子组件;
2.key: 数据key,根据该值获取远程数据,并通过props
传给WrappedComponent
;
InputWithUser
生成一个新的输入框,其内容在高级组件通过key='user'
远程数据访问获取; textareaWithContent
生成一个新的文本域,其内容在高级组件通过key='content'
远程数据访问获取。
到这里,高阶组件的作用其实不言而喻,其实就是为了组件之间的代码复用。组件可能有着某些相同的逻辑,把这些逻辑抽离出来,放到高阶组件中进行复用。高阶组件内部的包装组件和被包装组件之间通过 props 传递数据。
2 使用createStore管理共享数据
了解完Context
和高阶组件的概念后,我们使用上一篇文章【xxx】中的createStore
结合React组件来实现自己的react-redux
。 设计这样一棵组件树:
其中,Index
组件中使用Context
维护全局组件颜色的状态变量themeColor
,ThemeSwitch
中两个按钮控制所有组件的颜色。
使用create-react-app
脚手架创建一个新的react项目my-redux
,在src/下添加以下文件:
src/index.js
import React, { Component } from 'react'
import ReactDOM from 'react-dom/client';
import Header from './Header'
import Content from './Content'
import PropTypes from 'prop-types'
import './index.css'
function createStore(reducer) {let state = nullconst listeners = []const subscribe = (listener) => listeners.push(listener)const getState = () => stateconst dispatch = (action) => {state = reducer(state, action)listeners.forEach((listener) => listener())}dispatch({}) // 初始化 statereturn { getState, dispatch, subscribe }
}
// 负责主题颜色的 reducer
const themeReducer = (state, action) => {if (!state) return {themeColor: 'red' // 状态名themeColor,初始值red}switch (action.type) {case 'CHANGE_COLOR': // 只允许一种操作:修改themeColorreturn { ...state, themeColor: action.themeColor }default:return state}
}
// 创建store
const store = createStore(themeReducer)
class Index extends Component {// 给顶层父组件组件设置 context,childContextTypes 是必写的static childContextTypes = {store: PropTypes.object}// 把 store 设置到 context 中的过程,它返回的对象就是 contextgetChildContext () {return { store }}render () {return (<div><Header /><Content /></div>)}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Index />
);
src/header.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
class Header extends Component {static contextTypes = {store: PropTypes.object}constructor () {super()this.state = { themeColor: '' }}// 首次渲染 & 注册渲染回调函数componentWillMount () {const { store } = this.contextthis._updateThemeColor()store.subscribe(() => this._updateThemeColor())}_updateThemeColor () {// 获取context中的storeconst { store } = this.contextconst state = store.getState()this.setState({ themeColor: state.themeColor })}render () {return (<h1 style={{ color: this.state.themeColor }}>Header</h1>)}
}
export default Header
src/content.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ThemeSwitch from './ThemeSwitch'
class Content extends Component {static contextTypes = {store: PropTypes.object}constructor () {super()this.state = { themeColor: '' }}// 首次渲染 & 注册渲染回调函数componentWillMount () {const { store } = this.contextthis._updateThemeColor()store.subscribe(() => this._updateThemeColor())}_updateThemeColor () {// 获取context中的storeconst { store } = this.contextconst state = store.getState()this.setState({ themeColor: state.themeColor })}render () {return (<div><p style={{ color: this.state.themeColor }}>Content</p><ThemeSwitch /></div>)}
}
export default Content
src/themeSwitch.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
class ThemeSwitch extends Component {static contextTypes = {store: PropTypes.object}constructor () {super()this.state = { themeColor: '' }}// 首次渲染 & 注册渲染回调函数componentWillMount () {const { store } = this.contextthis._updateThemeColor()store.subscribe(() => this._updateThemeColor())}_updateThemeColor () {// 获取context中的storeconst { store } = this.contextconst state = store.getState()this.setState({ themeColor: state.themeColor })}// dispatch action 去改变颜色handleSwitchColor (color) {const { store } = this.contextstore.dispatch({type: 'CHANGE_COLOR',themeColor: color})}// button组件绑定点击回调方法render () {return (<div><buttonstyle={{ color: this.state.themeColor }}onClick={this.handleSwitchColor.bind(this, 'red')}>Red</button><buttonstyle={{ color: this.state.themeColor }}onClick={this.handleSwitchColor.bind(this, 'blue')}>Blue</button></div>)}
}
export default ThemeSwitch
启动服务后,点击两种按钮就可以触发dispatch
回调,修改store
中的state
,更改整体颜色:
1.在顶层父组件Index
中,创建了一个store
用来存取全局共享数据;
2.在顶层父组件Index
中,创建了一个themeReducer
用来初始化全局颜色状态数据,并定义只允许CHANGE_COLOR
这个action
来操作数据;
3.通过Context
将该状态共享给Index
所有的子组件;
4.子组件中在componentWillMount
生命周期里获取状态数据,渲染组件颜色,并通过subscribe
注册状态变化时重新渲染的回调方法。
3 使用高阶组件封装重复代码
3.1 代码复用性的问题
上面一节,我们将上篇文章中实现的createStore
版本的Redux
应用到了实际的组件树中,但是还存在两个问题:
1.有大量重复的逻辑:它们基本的逻辑都是,取出Context
,取出里面的store
,然后用里面的状态设置自己的状态,这些代码逻辑其实都是相同的。
2.对Context
依赖性过强:这些组件都要依赖Context
来取数据,使得这个组件复用性基本为零。想一下,如果别人需要用到里面的ThemeSwitch
组件,但是他们的组件树并没有Context
也没有store
,他们没法用这个组件了。
对于第一个问题,根据上面高阶组件章节,可以把一些可复用的逻辑放在高阶组件当中,高阶组件包装的新组件和原来组件之间通过props
传递信息,减少代码的重复程度。 第二个问题,可以通过纯函数的思想解决。到底什么样的组件才叫复用性强的组件?如果一个组件对外界的依赖过于强,那么这个组件的移植性会很差,就像这些严重依赖Context
的组件一样。一个组件的渲染只依赖于外界传进去的props
和自己的state
,而并不依赖于其他的外界的任何数据,也就是说像纯函数一样,给它什么,它就吐出(渲染)什么出来,这种组件的复用性是最强的。
看下如何通过高阶组件、纯函数的思想解决这两个问题:
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export connect = (WrappedComponent) => {class Connect extends Component {static contextTypes = {store: PropTypes.object}// TODO: 如何从 store 取数据?render () {return <WrappedComponent />}}return Connect
}
connect
函数接受一个组件WrappedComponent
作为参数,把这个组件包含在一个新的组件Connect
里面,Connect
会去context
里面取出store
。现在要把store
里面的数据取出来通过props
传给WrappedComponent
,这样做就解决前面提出的两个问题:
1.Context
中的store
注入到WrappedComponent
中的逻辑由connect
高阶组件统一完成;
2.WrappedComponent
作为纯函数本身不依赖于其他的外界的任何数据,该组件的渲染只依赖于外界传进去的props
和自己的state
。
3.2 mapStateToProps和mapDispatchToProps
解决了代码复用性的问题后,又出现了两个新的问题:
1.每个WrappedComponent
需要的state
是不同,不应该整个state
传给WrappedComponent
,而是需要做个转换,按需分配;
2.同理,不应该把dispatch
的完整功能传给WrappedComponent
,而是传递与这个WrappedComponent
相关的操作;
用户可以自定义两个转换函数,对state
和dispatch
做转换,如:
// 只传递 state.themeColor, state.themeName
const mapStateToProps = (state) => {return {themeColor: state.themeColor,themeName: state.themeName}
}
// 只允许 CHANGE_COLOR 这个action
const mapDispatchToProps = (dispatch) => {return {onSwitchColor: (color) => {dispatch({ type: 'CHANGE_COLOR', themeColor: color })}}
}
3.3 connect
综合3.1和3.2提出的问题,这一节提出我们的解决方案,实现这样一个方法connect
:
入参: 接收 mapStateToProps
,mapDispatchToProps
出参: 生成一个高阶组件,这个高阶组件内部获取Context的store,将store
中的state
、dispatch
分别应用入参的mapStateToProps
,mapDispatchToProps
转换函数转换后,作为props
传递给WrappedComponent
具体用法如下:
const higherOrderComponent = connect(mapStateToProps, mapDispatchToProps); // 返回一个高阶组件
const NewComponent = higherOrderComponent(WrappedComponent); // 高阶组件包裹子组件
将上面两条语句合并成一条
const NewComponent = connect(mapStateToProps, mapDispatchToProps)(WrappedComponent)
connect、WrappedComponent的关系如下图所示:
下面给出具体实现:
src/react-redux.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {class Connect extends Component {// 子组件获取context中数据时用于校验类型static contextTypes = {store: PropTypes.object}constructor () {super()this.state = {// 存放转换后的state、转换后的dispatch、组件本身接收的propsallProps: {}}}componentWillMount () {// 获取context中的storeconst { store } = this.contextthis._updateProps()// 注册状态数据变化时的回调store.subscribe(() => this._updateProps())}_updateProps () {const { store } = this.context// 转换statelet stateProps = mapStateToProps? mapStateToProps(store.getState(), this.props): {} // 防止 mapStateToProps 没有传入// 转换dispatchlet dispatchProps = mapDispatchToProps? mapDispatchToProps(store.dispatch, this.props): {} // 防止 mapDispatchToProps 没有传入// 存放转换后的state、转换后的dispatch、组件本身接收的propsthis.setState({allProps: {...stateProps,...dispatchProps,...this.props}})}render () {// 将转换后的state、转换后的dispatch、组件本身接收的props 作为props传给子组件return <WrappedComponent {...this.state.allProps} />}}return Connect
}
以ThemeSwitch为例,看下如何使用connect进行改造:
src/ThemeSwitch.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from './react-redux'
class ThemeSwitch extends Component {// 类型检查static propTypes = {themeColor: PropTypes.string,onSwitchColor: PropTypes.func}// 按钮点击操作回调handleSwitchColor (color) {if (this.props.onSwitchColor) {// 调用转换后的 onSwitchColorthis.props.onSwitchColor(color)}}render () {return (<div><buttonstyle={{ color: this.props.themeColor }}onClick={this.handleSwitchColor.bind(this, 'red')}>Red</button><buttonstyle={{ color: this.props.themeColor }}onClick={this.handleSwitchColor.bind(this, 'blue')}>Blue</button></div>)}
}
// 将state转换后通过props注入被包裹的组件
const mapStateToProps = (state) => {return {themeColor: state.themeColor}
}
// 将props转换后通过props注入被包裹的组件
const mapDispatchToProps = (dispatch) => {return {onSwitchColor: (color) => {dispatch({ type: 'CHANGE_COLOR', themeColor: color })}}
}
ThemeSwitch = connect(mapStateToProps, mapDispatchToProps)(ThemeSwitch)
export default ThemeSwitch
src/Header.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from './react-redux'
class Header extends Component {static propTypes = {themeColor: PropTypes.string}render () {return (<h1 style={{ color: this.props.themeColor }}>Header</h1>)}
}
const mapStateToProps = (state) => {return {themeColor: state.themeColor}
}
Header = connect(mapStateToProps)(Header)
export default Header
src/content.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ThemeSwitch from './ThemeSwitch'
import { connect } from './react-redux'
class Content extends Component {static propTypes = {themeColor: PropTypes.string}render () {return (<div><p style={{ color: this.props.themeColor }}>Content</p><ThemeSwitch /></div>)}
}
const mapStateToProps = (state) => {return {themeColor: state.themeColor}
}
Content = connect(mapStateToProps)(Content)
export default Content
src/index.js
import React, { Component } from 'react'
import ReactDOM from 'react-dom/client';
import Header from './Header'
import Content from './Content'
import PropTypes from 'prop-types'
import './index.css'
function createStore (reducer) {let state = nullconst listeners = []const subscribe = (listener) => listeners.push(listener)const getState = () => stateconst dispatch = (action) => {state = reducer(state, action)listeners.forEach((listener) => listener())}dispatch({}) // 初始化 statereturn { getState, dispatch, subscribe }
}
const themeReducer = (state, action) => {if (!state) return {themeColor: 'red'}switch (action.type) {case 'CHANGE_COLOR':return { ...state, themeColor: action.themeColor }default:return state}
}
const store = createStore(themeReducer)
class Index extends Component {static childContextTypes = {store: PropTypes.object}getChildContext () {return { store }}render () {return (<div><Header /><Content /></div>)}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Index />
);
通过这一节的改造,与第2节中的基础实现相比,ThemeSwitch
、Header
、Content
中的
1.获取context
中的store
;
2.通过subscribe
注册共享状态数据变化时回调;
3.直接调用dispatch
修改共享状态数据;
这3类操作都被优化掉了,我们的组件变得更加干净,取而代之的是,通过props
将这些状态、操作回调方法传给对应组件,这3类操作如下所示:
...
// 首次渲染 & 注册渲染回调函数
componentWillMount () {const { store } = this.contextthis._updateThemeColor()store.subscribe(() => this._updateThemeColor())
}
_updateThemeColor () {// 获取context中的storeconst { store } = this.contextconst state = store.getState()this.setState({ themeColor: state.themeColor })
}
...
...
// dispatch action 去改变颜色
handleSwitchColor (color) {const { store } = this.contextstore.dispatch({type: 'CHANGE_COLOR',themeColor: color})
}
...
4 Provider
通过上一节改造,ThemeSwitch
、Header
、Content
已经变得非常干净了,还剩Index
公共父组件中依然包含Contex
、createStore
的代码逻辑,这一节讲解通过Provider
优化Index
父组件实现。
由于Index
组件是所有组件的公共父组件,所以将Contex
设置在Index
中,这样他的所有子组件才能共享Context
中的数据。我们可以额外构建一个组件来做这种脏活,然后让这个组件成为组件树的根节点,那么它的子组件都可以获取到Context
了。
src/react-redux.js中新增如下代码
export class Provider extends Component {static propTypes = {store: PropTypes.object,children: PropTypes.any}static childContextTypes = {store: PropTypes.object}getChildContext () {return {store: this.props.store}}render () {return (<div>{this.props.children}</div>)}
}
Provider
做的事情也很简单,它就是一个容器组件,会把嵌套的内容原封不动作为自己的子组件渲染出来。它还会把外界传给它的props.store
放到Context
,这样子组件connect
的时候都可以获取到。
可以用它来重构我们的 src/index.js:
class Index extends Component {// 删除 Index 里面所有关于 context 的代码// static childContextTypes = {// store: PropTypes.object// }// getChildContext () {// return { store }// }render () {return (<div><Header /><Content /></div>)}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Provider store={store}><Index /></Provider>
);
这样我们就把所有关于Context
的代码从Index组件里面删除了。
5 总结
这几节的成果就是 react-redux
这个文件里面的两个内容:connect
函数和 Provider
容器组件。理解了
1.为什么要 connect,
2.为什么要 mapStateToProps 和 mapDispatchToProps,
3.什么是 Provider,
这就是 react-redux 的基本内容,当然它是一个简易 react-redux,很多地方需要完善。
最后
最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。
有需要的小伙伴,可以点击下方卡片领取,无偿分享