咱们在使用react
进行开发时,一般会搭配react-redux
进行状态管理,react-redux
实际上是基于redux
封装的,使开发者更方便的使用redux
管理数据,因此要明确redux
彻底可使用。咱们要学习react-redux
首先要先学习redux
。javascript
redux简单实现demohtml
咱们先来看一下redux
的基本使用,下面的代码经过createStore
来建立一个store
,建立成功后会返回三个API(subscribe
、dispatch
、getState
)。咱们经过subscribe
来订阅store中数据的变化,当有变化时会执行回调函数,经过getState
获取最新数据输出,最后咱们经过dispatch
传入action
来触发数据改变。react
// src/store/index.js
import { createStore } from 'redux'
// 定义reducer
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
// 建立store,返回API { subscribe, dispatch, getState }
let store = createStore(counter)
// 订阅store变化试,派发通知
store.subscribe(() => console.log(store.getState()))
// 经过dispatch触发action,作到store中数据变化
store.dispatch({ type: 'INCREMENT' }) // 1
store.dispatch({ type: 'INCREMENT' }) // 2
store.dispatch({ type: 'DECREMENT' }) // 1
复制代码
咱们引入这个文件,在控制台中能够看到依次输出一、二、1。能够看出来redux
用法很简单,其实它只是规定了改变数据的方法,当咱们遵循这个规则时,咱们的数据源就是惟一的,数据也变得可控起来。接下来咱们本身来实现一个简易版的redux
来知足基本使用。git
经过上面的例子,咱们首先要实现createStore
,该函数会返回三个经常使用的API,而且能够操做state。下面是函数的骨架。github
// src/mock/redux.js
function createStore(reducer) {
let currentState; // 始终保持最新的state
const listeners = []; // 用于存储订阅者
// 订阅store
function subscribe(fn) {}
// 获取最新state
function getState() {}
// 改变数据的惟一方法(约定)
function dispatch() {}
return { subscribe, getState, dispatch };
}
export default createStore;
复制代码
下面咱们逐一实现这三个API。redux
getState
实现就超简单了,由于内部变量currentState
始终保持最新,咱们只要将这个变量返回就行了,一行代码搞定数组
// 获取最新state
function getState() {
return currentState;
}
复制代码
咱们定义了内部变量listeners
,因此只要将传入的订阅者存储到listeners
中就能够。注意:订阅者必定是函数,这样state
变化时,去执行listeners
中的函数就能够了。咱们还要返回一个函数用于取消订阅。闭包
// 订阅store
function subscribe(fn) {
if (typeof fn !== "function") {
throw new Error("期待订阅者是个函数类型");
}
listeners.push(fn);
// 用于取消订阅
return function describe() {
const idx = listeners.indexOf(fn);
listeners.splice(idx, 1);
};
}
复制代码
dispatch
接受一个action对象,该action对象会传入到reducer
中,reducer
是咱们在建立store
传入的。reducer
约定会经过action
的type来返回新的state,那其实dispatch
的原理也就很简单了。咱们只要把传入的action传入到reducer
函数中,返回新的state赋值给currentState
就能够了。看代码:app
// 改变数据的惟一方法(约定)
function dispatch(action) {
currentState = reducer(currentState, action);
// 别忘了,数据改变后,要通知全部的订阅者。
listeners.forEach(fn => fn());
}
复制代码
是否是超Easy?抛去redux
的概念,其实咱们就是经过闭包的概念,来操做内部的数据,从而实现状态管理。
- import { createStore } from 'redux'
+ import createStore from "../mock/redux";
复制代码
咱们将src/store/index.js
文件中createStore
替换成咱们的,再次执行看下,效果是一致的。demo源码
咱们定义好store
,而后经过react-redux
提供的Provider
向下注入依赖store
。
import store from "./store/index";
import { Provider } from "react-redux";
// 忽略无关代码
ReactDOM.render(
<Provider store={store}> <APP /> </Provider>,
rootElment
);
复制代码
咱们在须要依赖state的组件文件中使用react-redux
提供的connect
对组件进行高阶包裹。其中咱们向传connect函数传入俩个参数,分别是mapStateToProps
和mapDispatchToProps
,做用跟名字相同,react-redux
会把俩个函数执行,将返回值都以props
的形式传入到组件中。
import { connect } from "react-redux";
// 忽略无关代码
function mapStateToProps(state) {
return {
count: state
};
}
function mapDispatchToProps(dispatch) {
return {
increment() {
dispatch({
type: "INCREMENT"
});
},
decrement() {
dispatch({
type: "DECREMENT"
});
}
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(App); // App组件接受到的props中 包括 count、increment、decrement
复制代码
咱们只要在App组件从props
中解构出值`进行使用。
function App(props) {
const { count, increment, decrement } = props;
return (
<div className="App"> <p>当前count: {count}</p> <button onClick={increment}>增长1</button> <button onClick={decrement}>减小1</button> </div>
);
}
复制代码
乍一看代码量不少,但解决了组件嵌套的问题,当嵌套组件须要依赖state时候,咱们只须要用connect
进行包裹,传入mapStateToProps
就能够。并且不须要咱们手动订阅store
的变化,从而触发组件的渲染。那它是如何工做的呢?咱们接下来分析一波,并动手实现一个简易的react-redux
。
首先咱们忘记react-redux
的存在,尝试直接在react
组件中使用redux
,咱们须要在组件渲染前获取到所需的state
。而且订阅store
,当其state
变化后,咱们要从新渲染该组件从而获取到最新的state。代码以下:
class App extends React.Component {
componentDidMount() {
// 订阅
this.describe = store.subscribe(() => {
// 强制渲染
this.forceUpdate();
});
}
componentWillUnmount() {
// 取消订阅
this.describe();
}
increment = () => {
store.dispatch({
type: "INCREMENT"
});
};
decrement = () => {
store.dispatch({
type: "DECREMENT"
});
};
render() {
// 获取当前状态并赋值
const count = store.getState();
return (
<div className="App"> <p>当前count: {count}</p> <button onClick={this.increment}>增长1</button> <button onClick={this.decrement}>减小1</button> </div>
);
}
}
复制代码
咱们能够发现,获取所需state和订阅store从新渲染组件是每个须要依赖redux
组件都须要的,因此咱们应该抽离出公共部分。
在类组件咱们想要复用逻辑只能经过HOC
高阶组件来实现,connect
函数其实就是生成高阶组件。下面咱们先写个最基本的connect函数:
/** * 经过传入mapStateToProps/mapDispatchToProps生成高阶组件 * 并把所需state经过props传入组件 * @param {function} mapStateToProps * @param {function} mapDispatchToProps */
function connect(mapStateToProps, mapDispatchToProps) {
return function wrapWithConnect(WrapperComponent) {
return function ConnectFunction(props) {
// 获取到所需state,触发dispatch的函数
const stateProps = mapStateToProps(store.getState());
const dispatchProps = mapDispatchToProps(store.dispatch);
// 执行强制渲染
const [, forceRender] = useReducer(s => s + 1, 0);
// 订阅store变化
useEffect(() => {
const describe = store.subscribe(forceRender);
return describe;
}, []);
return <WrapperComponent {...props} {...stateProps} {...dispatchProps} />; }; }; } 复制代码
注:由于函数组件没有this.forceUpdate
方法,因此经过useReducer
自增实现一样的效果。
上述代码把获取所需state和订阅store从新渲染组件俩部分都抽离了出来,使咱们能够在须要使用store
中数据时,直接经过connect(mapStateToProps)(Comp)
对组件进行包裹便可。
但如今还有俩个问题须要优化。1.是咱们如今的store是直接引入的,没法支持动态的store ,2.是目前为止,咱们store变化就会从新渲染,当咱们所依赖的值没有改变时,咱们无需从新渲染。
咱们先解决上面的说的第一个问题,想支持动态的store,咱们就须要实现react-redux
中的Provider
组件,看名字你们应该知道它是基于react context
实现的,没错,要实现动态store,咱们须要使Provider
向下注入依赖,而后在connect
包裹组件的时候,经过context
来获取最新store。
import storeContext from "./storeContext";
// storeContext就是经过React.createContext()生成context
const Provider = ({ store, children }) => {
return (
<storeContext.Provider value={store}>{children}</storeContext.Provider> ); }; export default Provider; 复制代码
Provider
组件就这么简单,接下来咱们须要修改connect
函数
import storeContext from "./storeContext";
// 忽略无关代码...
const store = useContext(storeContext);
// 获取到所需state,触发dispatch的函数
const stateProps = mapStateToProps(store.getState());
const dispatchProps = mapDispatchToProps(store.dispatch);
// 订阅store变化
useEffect(() => {
const describe = store.subscribe(forceRender);
return describe;
}, [store]);
// 忽略无关代码...
复制代码
经过react
提供的useContext()
来获取到当前store
,useEffect
第二个参数依赖store,当store自己变化时,也会从新订阅。这样咱们第一个问题算是解决了。用法与react-redux
也大致相同。
再解决第二个问题:咱们如今订阅store中state变化,仍是很暴力的(直接强制从新渲染)。要解决这个问题也很简单,咱们只要订阅的回调函数中,加入新老值的比较,当不相同时,咱们才执行forceRender
。
// src/react-redux/connect.js
import shallowEqual from "shallowequal";
function connect(mapStateToProps, mapDispatchToProps) {
return function wrapWithConnect(WrapperComponent) {
return function ConnectFunction(props) {
const store = useContext(storeContext);
const lastStateProps = useRef({}); // 保存最新的state
const lastDispatchProps = useRef({});
// 执行强制渲染
const [, forceRender] = useReducer(s => s + 1, 0);
// 订阅store变化
useEffect(() => {
lastStateProps.current = mapStateToProps(store.getState());
lastDispatchProps.current = mapDispatchToProps(store.dispatch);
}, [store]);
// 订阅store变化
useEffect(() => {
forceRender();
function checkForUpdates() {
const newStateProps = mapStateToProps(store.getState());
// 执行浅比较
if (!shallowEqual(lastStateProps.current, newStateProps)) {
console.log('render')
// 赋值最新的state
lastStateProps.current = newStateProps;
forceRender();
}
}
const describe = store.subscribe(checkForUpdates);
return describe;
}, [store]);
return (
<WrapperComponent {...props} {...lastStateProps.current} {...lastDispatchProps.current} /> ); }; }; } 复制代码
咱们引入shallowequal
对新老state进行浅比较,当不相等时,才进行forceRender
。
- import { Provider } from "react-redux";
+ import { connect, Provider } from "./react-redux";
复制代码
如今,咱们将App组件中的Provider
、connect
替换掉,代码是能够正常的使用。完整demo
上面实现了connect
用于共享逻辑,虽然函数组件也能够经过它进行包裹使用,但React Hook
的出现让咱们对于逻辑复用有了更好的办法,那就是本身写一个Hook
。useSelector
是react-redux
官方已经实现了的。具体的使用以下:
const count = useSelector(state => state.count)
复制代码
经过传入一个选取函数
返回所须要的state,其实这里的选取函数
至关因而mapStateToProps
。咱们来动手实现如下。
import storeContext from "./storeContext";
export default function useSelector(seletorFn) {
const store = useContext(storeContext);
return seletorFn(store.getState());
}
复制代码
如今咱们能够执行useSeletor
获取到所须要的state,接下来咱们要作的就是订阅store从新渲染,其实就是咱们实现connect中函数组件的代码,咱们直接copy过来改一下
import storeContext from "./storeContext";
import shallowEqual from "shallowequal";
export default function useSelector(selectorFn) {
const store = useContext(storeContext);
const lastStateProps = useRef();
const lastSelectorFn = useRef();
// 执行强制渲染
const [, forceRender] = useReducer(s => s + 1, 0);
// 赋值state
useEffect(() => {
lastSelectorFn.current = selectorFn;
lastStateProps.current = selectorFn(store.getState());
});
// 订阅store变化
useEffect(() => {
function checkForUpdates() {
const newStateProps = lastSelectorFn.current(store.getState());
if (!shallowEqual(lastStateProps.current, newStateProps)) {
console.log("render");
lastStateProps.current = newStateProps;
forceRender();
}
}
const describe = store.subscribe(checkForUpdates);
forceRender();
return describe;
}, [store]);
return lastStateProps.current;
}
复制代码
注意:这里须要使用lastSelectorFn
Ref存储选择器
,不然useEffect依赖selectorFn
会形成死循环。
实现useDispatch
就超简单了,就是直接返回store.dispatch
就好
import { useContext } from "react";
import storeContext from "./storeContext";
export default function useDispatch(seletorFn) {
const store = useContext(storeContext);
return store.dispatch;
}
复制代码
本文中实现的redux
、react-redux
都只是实现了一小部分API,而且没有处理异常状况。但与源码的核心大致相同。但愿阅读完的小伙伴有所收获,若是不过瘾还能够去阅读下源码哦。