在class组件的世界里,如果后代组件共享某些状态,比如主题色、语言键,则需要将这些状态提升到根组件,以props的方式从根组件向后代组件一层一层传递,这样则需要在每层写props.someData,这样使用起来非常麻烦。那么,有没有更好的方式,让后代组件们共享状态?
当然是有的,react提供了context解决方案,某个组件往context放入一些用于共享的状态,该组件的所有后代组件都可以直接从context取出这些共享状态,跳出一层一层传递的宿命。怎么做?以下给出示例代码
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
class App extends PureComponent {
// 1. 定义静态变量childContextTypes声明context中状态对应的类型
static childContextTypes = {
lang: PropTypes.string,
};
constructor(props) {
super(props);
this.state = {
lang: 'zh_CN',
};
}
// 2. 通过getChildContext返回值设置context
getChildContext() {
return {
lang: this.state.lang,
};
}
render() {
return (
);
}
}
class SideMenu extends PureComponent {
// 1. 定义静态变量contextTypes声明context中状态对应的类型
static contextTypes = {
lang: PropTypes.string,
}
constructor(props) {
super(props);
}
// 2. 通过this.context取出context
render() {
return (
123
)
}
}
class Content extends PureComponent {
// 1. 定义静态变量contextTypes声明context中状态对应的类型
static contextTypes = {
lang: PropTypes.string,
}
constructor(props) {
super(props);
}
// 2. 通过this.context取出context
render() {
return (
123
)
}
}
总结起来,就两点:
redux可以比作数据中心,所有数据(即state)存于store中。如果想查询state,必须调用store.getState方法;如果想要更改state,必须调用store.dispatch方法;如果想要在更改state后执行其他额外逻辑,需要使用store.subscribe订阅。由store.subscribe订阅,由store.dispatch发布,构成订阅/发布模式。
本篇实现一个简易版的createStore,执行createStore()会返回store对象,createStore源码实现如下:
const createStore = (reducer, initialState) => {
// 初始化state
let state = initialState;
// 保存监听函数
const listeners = [];
// 返回store当前保存的state
const getState = () => state;
// 通过subscribe传入监听函数
const subscribe = (listener) => {
listeners.push(listener);
}
// 执行dispatch,通过reducer计算新的状态state
// 并执行所有监听函数
const dispatch = (action) => {
state = reducer(state, action);
for(const listener of listeners) {
listener();
}
}
!state && dispatch({});
return {
getState,
dispatch,
subscribe,
}
}
初始化store(createStore)需要reducer,而reducer可以理解为更新state的方法,是一个纯函数。store.dispatch(action)一个动作后,首先,执行reducer方法,根据action.type找到对应处理逻辑,更新state;然后,执行所有由store.subscribe订阅的监听函数。
为了验证我们实现的redux功能,设计了一个简单的reducer:
const reducer = (state, action) => {
if(!state) {
return {
menu: {
text: 'menu',
color: 'red',
},
}
}
switch(action.type) {
case 'UPDATE_MENU_TEXT':
return {
...state,
menu: {
...state.menu,
text: action.text,
}
};
case 'UPDATE_MENU_COLOR':
return {
...state,
menu: {
...state.menu,
color: action.color,
}
};
default:
return state;
}
}
万事具备,创建store,作为一些简单测试,Demo源码如下:
const store = createStore(reducer);
store.subscribe(() => console.log('demo') );
const action = {
type: 'UPDATE_MENU_COLOR',
color: 'blue'
};
store.dispatch(action);
// 打印当前状态
console.dir(store.getState());
打印结果如下:
第1节讲到context可以让react组件很方便的共享状态,第2节讲到redux统一管理状态,那是不是可以用redux统一管理react组件的共享状态呢?是可以的,react-redux就实现了context和redux的完美粘合。
首先是父组件部分,将父组件类“定义静态变量childContextTypes”和“通过getChildContext()方法的返回值设置context”移到Provider中,并且context的值从Provider的props获取。
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
export class Provider extends PureComponent {
// Provider的props仅含store和children
static propTypes = {
store: PropTypes.object,
children: PropTypes.any,
}
// 1. 定义静态变量childContextTypes声明context中状态对应的类型
static childContextTypes = {
store: PropTypes.object,
};
// 2. 通过getChildContext返回值设置context
getChildContext() {
return {
store: this.props.store
}
}
render() {
return (
{this.props.children}
)
}
}
然后是后代组件部分,将后代组“定义静态变量contextTypes”和“通过this.context取出context”移到Connect组件中。
为什么这里就需要用到高阶组件,而不是像Provider组件一样,仅Connect组件就可以?因为Provider仅提供数据,逻辑简单:1. 用this.props.store设置context,2. 不改变this.props.chidren。但Connect组件不行,这里需要从context取出store,且不能简单将store传递后代组件(后代组件不能从自己的props取store,这会污染后代组件),而是需要从store中取出state和dispatch,根据state和dispatch映射出对应数据(即mapStateToProps和mapDispatchToProps)作为props传给后代组件。怎样映射的方式需要另外方式传进来,即高阶函数connect。
是怎么做到store.dispatch(action)一个动作后,被connect包裹的后代组件自行渲染?这是因为Connect组件中调用store.subscribe()方法订阅了() => this._updateProps(),this._updateProps中调用了this.setState,来触发更新。
export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
class Connect extends PureComponent {
static contextTypes = {
store: PropTypes.object,
};
constructor(props) {
super(props);
this.state = {
allProps: {}
};
}
componentWillMount() {
const { store } = this.context;
this._updateProps();
store.subscribe(() => this._updateProps());
}
_updateProps() {
const { store } = this.context;
const stateProps = mapStateToProps(store.getState(), this.props);
const dispatchProps = mapDispatchToProps(store.dispatch, this.props);
this.setState({
allProps: {
...stateProps, // 从store的state取状态数据
...dispatchProps, // 需要更新store的state的方法,从这里传入dispatch
...this.props // 透传给WrappedComponent
}
});
}
render() {
return (
)
}
}
return Connect;
}
Provider作为根组件用来包含App组件,并将store传给Provider。
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from './react-redux';
import createStore, { reducer } from './redux';
import App from './App';
const store = createStore(reducer);
const domNode = document.getElementById('root');
const root = createRoot(domNode);
root.render(
);
用connect包裹Content组件,通过mapStateToProps和mapDispatchToProps从store中取出相应的数据。
import React from 'react';
import { createRoot } from 'react-dom/client';
import { connect } from './react-redux';
class Content extends PureComponent {
// 2. 通过this.context取出context
render() {
return (
this.props.onChangeLang('zh_CN')}
>
123
)
}
}
// state是从store取出来的,props是传给高阶组件Connect的props
const mapStateToProps = (state, props) => {
return {
lang: state.lang
};
}
// dispatch是从store取出来的,props是传给高阶组件Connect的props
const mapDispatchToProps = (dispatch, props) => {
return {
onChangeLang: (lang) => {
dispatch({ type: 'UPDATE_LANG', lang })
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Content);
react-redux是redux在React的一次成功应用,第2小节redux实现比较简单,主要是对第1小节的理解,理解第1小节后,理解第3小节就容易多了。
看最新react官方文档,已将PureComponent和Componet列为过时的API,估计后续不推荐再使用class组件,因此react-redux实现方式可能会发生变更,或新出现一种redux和函数组件结合的方式,或基于useContext、useReducer、createContext形成一种新的实现方式。
注:以上,如有不合理之处,还请帮忙指出,大家一起交流学习~