前言
这篇文章会带大家梳理一下React、React-Redux的原理,以及核心API的最简实现。适合已经使用过redux,但对原理不大了解的读者。
简介
在开始之前先简单介绍一下这两个库,以及他们的区别
Redux
Redux
是一个Javascript状态管理库。他的做的事情非常简单: 使用一个对象去描述应用的状态state
,并只能通过action
配合reducer
去修改state
。同时提供了一个订阅服务,在状态被修改时会通知所有订阅者。
-
state tree
存放应用状态的状态树。
-
action
可以将它理解为改变状态的一个指令,通常是一个这样的对象{type: 'ADD',payload: 1}
,type
字段告诉reducer
该怎么去改变store
中的状态,payload
则是携带的数据。 -
reducer
一个返回新状态的纯函数,它接收一个action
,通过判断其中type
字段对全局状态进行不同的操作,返回一个全新的状态树。
Redux
核心部分仅负责状态维护和订阅通知, 是与框架完全无关的。
React-Redux
React Redux
是 React
的官方 Redux UI
绑定库,帮助用户在组件中拿到Redux
中存放的状态以及更新状态的操作。
实现Redux
基本使用
我们先通过这个DEMO
看一下如何使用Redux
import { createStore } from 'redux'
const preloadedState = {
count: 0,
};
// reducer
const reducer = (state, action) => {
switch (action.type) {
case "INCREASE":
return { ...state, count: state.count + 1 };
case "DECREASE":
return { ...state, count: state.count - 1 };
case "SET_COUNT":
return { ...state, count: action.payload };
default:
return state;
}
};
// action
const actions = {
increment: () => ({ type: "INCREASE" }),
decrement: () => ({ type: "DECREASE" }),
setCount: (count) => ({ type: "SET_COUNT", payload: count }),
};
const store = createStore(reducer, preloadedState);
const state = store.getState() // 获取状态树
store.subscribe(() => console.log('store变啦',store.getState())); // 订阅状态变化
store.dispatch(actions.setCount(233)); // 触发状态变化
store.dispatch(actions.increment()); // 触发状态变化
可以看到Redux
包含了以下方法
-
createStore 接收一个默认状态树和reducer函数并返回一个store,store中包含以下方法
-
store.getState 获取状态
-
store.dispatch 通过派发action去修改状态
-
store.subscribe 订阅状态的变化
createStore
getState
getState
非常简单,我们返回函数中定义的state
即可。
function createStore(reducer, preloadedState) {
let state = preloadedState; // 定义初始的状态树
const getState = () => state; // 返回状态树
return { getState };
}
dispach
这是一个修改状态的函数,通过前面的介绍我们知道state
不应该被直接修改,而是每次需要更新时就通过action
和reducer
来获得一个新的state tree。
const newState = reducer(oldState,action)
所以dispatch
需要做的就是接收一个action,通过reducer生成新的state
并替换旧的 state
function createStore(reducer, preloadedState) {
let state = preloadedState; // 定义初始的状态树
const getState = () => state; // 返回状态树
const dispatch = (action) => {
// 获得新的状态树
state = reducer(state, action);
// 这里还需要通知所有订阅者
};
return { getState, dispatch };
}
subscribe
我们已经实现的状态的创建、获取、修改,但都只是对闭包内变量的操作,外界并不能感知到。subscribe
需要提供一个通知外接订阅者状态更新了的这么一个功能。
这里我们可以通过实现一个简易的发布订阅模式
来达到这个效果。在函数内部再维护一个listeners
数组,里面存放订阅者的回调函数。在每次状态变化(dispatch被调用)时,就执行所有回调。
function createStore(reducer, preloadedState) {
let state = preloadedState; // 定义初始的状态树
const getState = () => state; // 返回状态树
const listeners = [];
// 订阅
const subscribe = (fn) => {
listeners.push(fn);
// 取消订阅
return () => {
const index = listeners.find((item) => item === fn);
listeners.splice(index, 1);
};
};
const dispatch = (action) => {
// 获得新的状态树
state = reducer(state, action);
// 通知所有订阅者
listeners.forEach((fn) => fn());
};
return { getState , dispatch, subscribe };
}
至此我们已经实现了一个乞丐版本的redux
,我们来测试一下
const preloadedState = {/** ... */};
const reducer = (state, action) => {/** ... */};
const actions = {/** ... */};
const store = createStore(reducer, preloadedState);
console.log('初始状态' , store.getState())
const unsubscribe = store.subscribe( // 订阅
() => console.log('我知道state变啦',store.getState())
);
store.dispatch(actions.setCount(233));
store.dispatch(actions.increment());
unsubscribe(); // 取消订阅
store.dispatch(actions.increment()); // 这次更新不会通知到我
combineReducers
随着应用变得越来越复杂,我们就可以考虑将 reducer 函数
拆分成多个单独的函数,每个函数负责独立管理state
的一部分。
Redux
就有几个扩展api用来增强reducer
和dispatch
。combineReducers
可以将多个reducer
合并成一个最终的reducer
函数。 applyMiddleware
可以让我们自己包装 store 的 dispatch
来增强功能,比如实现异步action。
这里只介绍combineReducers
,
该方法接收一个对象,通过为传入对象的reducer
命名不同的 key 来控制返回 state key 的命名。
// 合并后的reducer
const rootReducer = combineReducers({potato: potatoReducer, tomato: tomatoReducer})
// 相应的,state的结构就得是
const rootState = { potato: {}, tomato: {} }
const store = createStore(rootReducer, rootState)
实现思路就是通过传入对象的key
去关联reducer
和state中的部分状态
,当接收到action
时,需要执行所有reducer
获得新的状态,然后将这些状态重新组装成一个新的状态树。
export default function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers)
// 返回合并后的reducer函数
return function combinedReducer(state = {}, action) {
// 新的state
const nextState = {}
// 遍历执行所有的reducers
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i] // 代表state的key
const reducer = reducers[key] // key对应的reducer
// 当前key对应state的 旧值
const prevKeyState = state[key]
// 执行reducer,获得当前key对应state的新值
const nextKeyState = reducer(prevKeyState, action)
// 组装最终的state
nextState[key] = nextKeyState
}
return nextState
}
}
实现React-Redux
基本使用
和前面一样,我们先看个DEMO
import store from './store'
import { Provider,connect } from 'react-redux'
function App() {
return (
<Provider store={store}>
<Header />
<Main />
<Footer />
</Provider>
)
}
// 映射state到props
const mapState = (state) => ({ count: state.count });
// 映射dispatch到props
const mapDispatch = (dispatch) => ({
increment: () => dispatch({ type: "increment" }),
});
const Header = connect(mapState)((props) => {
const { count } = props;
return <header> header: {count}</header>;
});
const Main = connect(mapState,mapDispatch)((props) => {
const { count, increment } = props;
return (
<main>
Main: count:{count} <button onClick={increment}>increase </button>
</main>
);
});
function Footer(props) {
return <footer>footer 没用到状态</footer>;
}
可以看到 react-redux
提供了一个Provider组件,用于将redux
中创建的store
透传给所有的子组件。
以及一个connect
函数,它可以让被包裹的组件直接通过props
访问store
中那些被‘map’的状态和dispatch
函数。这里的map可以理解为映射关系。
mapStateToProps
函数可以让你控制store
中哪些状态会被映射到组件的props中。mapDispatchToProps
让你可以自行封装dispatch
函数并映射到props
中。
默认情况下,如果都不传的话组件会拿到整个store
和dispatch
函数。
connect(mapStateToProps, mapDispatchToProps)(MyComponent)
Context
使用React
的createContext
方法直接创建一个context即可
import React from 'react'
const ReduxContext = React.createContext(null)
export default ReduxContext
Provider
Provider
接收store
并通过 ReduxContext
传给所有子组件
import React from 'react'
import ReduxContext from "./Context";
const Provider = (props: any) => {
const { store, children, ...rest } = props;
// 把store透传给所有子组件
return (
<ReduxContext.Provider value={store} {...rest}>
{children}
</ReduxContext.Provider>
);
};
export default Provider;
connect
先通过connect
的调用方式去推断一下connect
函数的结构。
connect(mapStateToProps, mapDispatchToProps)(MyComponent)
首先它接收mapStateToProps
,mapDispatchToProps
这两个函数,并且又返回了一个函数,该函数接收一个组件,并且能让组件中的props
获得参数。
function connect(mapStateToProps, mapDispatchToProps) {
return function wrapWithConnect(component) {
// 这里需要向组件的props中注入参数
};
}
那么怎么才能在wrapWithConnect
函数中拿到store
,并传给组件的props
呢? store
是通过context
透传的,而只有在组件中才能消费context
,所以很明显需要通过高阶组件(HOC)来实现这个功能。
function connect(mapStateToProps?: any, mapDispatchToProps?: any) {
return function wrapWithConnect(component) {
// 包裹组件,主要为了获取组件上下文
const HOC = (props) => {
const { dispatch, getState, subscribe } = useContext(AppContext);
const state = getState();
function childPropsSelector(state) {
// 注入到props中的状态
// 可能是整个store, 也可能是被mapState 返回的几个特定状态
const stateProps = mapStateToProps ? mapStateToProps(state) : state;
// 注入到props中改变状态的函数
// 可以是原本的dispatch,也可以是mapDispatch 返回的特定更新状态的函数
const dispatchProps = mapDispatchToProps
? mapDispatchToProps(dispatch)
: dispatch;
return { ...stateProps, ...dispatchProps, ...props };
}
// 获得最终子组件的的props
const actualChildProps = childPropsSelector(state);
return React.createElement(component, actualChildProps, props.children);
};
return HOC;
};
}
通过一层高阶组件的包裹,我们可以从context中拿到store
的信息,并经过一顿处理传给被包裹的组件。
接下来我们需要实现组件的更新,在react中更新组件的方式是通过setState
,每次传入一个新的值就能触发更新。
通过store
提供的的subscribe
方法,我们可以知道状态在什么时候被dispatch
,被改变了。
// 强制更新组件
const [, forceUpdate] = useState({});
useEffect(() => {
const unsubscribe = subscribe(() => {
forceUpdate({});
});
return unsubscribe;
}, []);
这里还需要做一个小优化。现在的代码即使dispatch
后的状态没有改变,组件中也会收到通知并触发组件的render
。
// 记录上一次渲染时的state
const preStateRef = useRef({});
preStateRef.current = state
// 订阅store
useEffect(() => {
const unsubscribe = subscribe(() => {
// 对比新的state和旧的state
const state = getState();
if (!isShadowEqual(preStateRef.current, state)) {
forceUpdate({});
}
});
return unsubscribe;
}, []);
通过一个ref去记录旧的state,并在每次render
之前对新旧state
做一次浅层次的对比,就可以减少不必要的重渲染。当然真正的场景下不会只有这么简单的判断,具体可以查看源码react-redux 源码
最终的实现代码如下:
import React, { useContext, useEffect, useRef, useState } from "react";
import ReduxContext from "./Context";
function connect(mapStateToProps, mapDispatchToProps) {
return function wrapWithConnect(component) {
const HOC = (props) => {
const { dispatch, getState, subscribe } = useContext(ReduxContext);
const state = getState();
const [, forceUpdate] = useState({});
function childPropsSelector(state) {
const stateProps = mapStateToProps ? mapStateToProps(state) : state;
const dispatchProps = mapDispatchToProps
? mapDispatchToProps(dispatch)
: dispatch;
return { ...stateProps, ...dispatchProps, ...props };
}
// 最终子组件的props
const actualChildProps = childPropsSelector(state);
// 记录上一次渲染时的state
const preStateRef = useRef({});
preStateRef.current = state;
// 订阅store
useEffect(() => {
const unsubscribe = subscribe(() => {
// 对比新的state和旧的state
const state = getState();
if (!isShadowEqual(preStateRef.current, state)) {
forceUpdate({});
}
});
return unsubscribe;
}, []);
return React.createElement(component, actualChildProps, props.children);
};
return HOC;
};
}
function isShadowEqual(origin: any, next: any) {
if (Object.is(origin, next)) {
return true;
}
if (
origin &&
typeof origin === "object" &&
next &&
typeof next === "object"
) {
if (
[...Object.keys(origin), ...Object.keys(next)].every(
(k) =>
origin[k] === next[k] &&
origin.hasOwnProperty(k) &&
next.hasOwnProperty(k)
)
) {
return true;
}
}
return false;
}
export default connect;
Hooks API
除了使用connect
,React-Redux
还支持使用hook的方式去访问store
useStore
拿到store的对象useSelector
使用选择器函数从state
中提取数据useDipatch
拿到dispatch函数
import React from "react";
import { useSelector, useStore, useDispatch } from "react-redux";
const CounterComponent = () => {
const store = useStore()
const count = useSelector((state) => state.counter);
const dispatch = useDispatch()
return (
<div>
{count}
<button onClick={() => dispatch({type: 'inc'})}>increase</button>
</div>
);
};
访问store
需要拿到context
,而这里的调用都不需要我们自己传入context
,所以需要事先拿到到这个context
对象。
import React, { useEffect, useState } from 'react';
import context from './Context'
function createSelectorHook(context) {
const useSelector = (selector) => {}
return useSelector
}
export default createSelectorHook(context);
拿到context
之后,就可以通过React.useContext
访问到store
了,接下来处理方式就和connect
类似
import React, { useEffect, useState } from 'react';
import context from './Context'
function createSelectorHook(context) {
const useReduxContext = () => React.useContext(context)
const useSelector = (selector) => {
const { getState,subscribe } = useReduxContext()
const state = getState()
const selectedState = selector(state);
// 强制更新组件
const [, forceUpdate] = useState({});
// 订阅store
useEffect(() => {
const unsubscribe = subscribe(() => {
// 这里省略的state的浅层对比
forceUpdate({});
});
return unsubscribe;
}, []);
return selectedState;
}
return useSelector
}
export default createSelectorHook(context);
其他两个API也是类似的思路,这里不再赘述。hooks的方式更适在函数组件中使用,不需要事前给组件包一层,更加的方便。而connect
更适合类组件,也可以通过@connect 装饰器
简化这一层包裹的过程。
结语
本文通过一些的代码示例,手写实现了React
和React-Redux
的一些常用函数,实现参考了源码但是简化了很多优化和错误判断的步骤,重点在于梳理出实现功能的大致思路。
如有描述不正确的地方,欢迎大家指正!