在造火箭之前,我们先分析下 redux 和 react-redux 源码。
准备工作
- redux 原理
- react-redux 原理
开始上手
- mini 版本的 Redux 实现
亮点学习
- 闭包(applyMiddleware)
- 设计模式:发布订阅(subscribe、dispatch)
- 极致的性能优化(代码看不懂有很大原因是加了很多 useMemo)(connect)
redux 原理
在看源码之前先贴上参考资料
createStore
它是创建整个状态树的关键,为什么说推荐在整个 app 中只创建一个状态树呢,多个状态树很难管理,而且大多数时候没有这个必要。一个状态树足以控制组件对数据变更做出反应。
createStore 内包含
dispatch触发 reducersubscribe订阅 dispatchgetState获取 state 状态树replaceReducer用来动态更改 reducer 函数
// preloadedState 是预置的 state,会在 redux 初始化的时候自动的被 reducer 的返回值覆盖
function createStore(reducer, preloadedState, enhancer) {
// 只有 reducer 和 enhancer 的情况
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState;
preloadedState = undefined;
}
if (typeof enhancer !== 'undefined') {
return enhancer(createStore)(reducer, preloadedState);
}
// 当前的 reducer 函数,新创建一个变量是为了在 replaceReducer 调用时新的 reducer 函数复制给它
let currentReducer = reducer;
// 当前的 state
let currentState = preloadedState;
// 在 subscribe 中订阅的事件
let currentListeners = [];
let nextListeners = currentListeners;
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice();
}
}
// 获取当前 state
function getState() {
return currentState;
}
// 订阅 dispatch
function subscribe(listener) {
// 防止调用多次 unsubscribe 函数
let isSubscribed = true;
ensureCanMutateNextListeners(); // 暂时忽略
nextListeners.push(listener); // 注册监听函数
return function unsubscribe() {
if (!isSubscribed) {
return;
}
isSubscribed = false;
ensureCanMutateNextListeners();
const index = nextListeners.indexOf(listener);
// unsubscribe 时把注册的函数从数组中剔除,防止后续 diapatch 时再次触发
nextListeners.splice(index, 1);
currentListeners = null;
};
}
function dispatch(action) {
// 让 reducer 函数计算新的 state,复制给 currentState
currentState = currentReducer(currentState, action);
// 调用注册的函数
const listeners = (currentListeners = nextListeners);
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
return action;
}
// 更改当前的 reducer 函数
function replaceReducer(nextReducer) {
currentReducer = nextReducer;
dispatch({ type: ActionTypes.REPLACE });
return store;
}
// 初始化 state,可以看上面的 dispatch 函数,会导致 reducer 函数的最新结果赋值给 currentState
dispatch({ type: ActionTypes.INIT });
const store = {
dispatch: dispatch,
subscribe,
getState,
replaceReducer,
};
return store;
}
combineReducers
combineReducers 是把用户定义的多个 reducer 函数合并到一起,合并之后的新函数传递参数的方式和之前 reducer 函数相同。
/**
* 把定义的多个 reducer 函数组合为一个 reducer 函数
* 实现比较简单,reducers 对象遍历一遍,让每个 reducer 函数执行一次,比较前后的 state,并返回最新的 state
* reducers = {counter: counterReducer}
*/
function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers);
const finalReducers = {};
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i];
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key];
}
}
const finalReducerKeys = Object.keys(finalReducers);
// 返回一个新的 reducer 函数
return function combination(state = {}, action) {
let hasChanged = false;
const nextState = {};
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i];
const reducer = finalReducers[key];
const previousStateForKey = state[key];
const nextStateForKey = reducer(previousStateForKey, action);
nextState[key] = nextStateForKey;
hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
}
hasChanged =
hasChanged || finalReducerKeys.length !== Object.keys(state).length;
return hasChanged ? nextState : state;
};
}
compose
compose 就是把函数都组合起来 d = compose(a, b, c) => d(x) === a(b(c(x)))
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
举个例子:
type composeFunc = <T>(args: T) => T
export default function compose(...funcs: composeFunc[]) {
if (funcs.length === 0) {
return (arg: any) => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
const f1: composeFunc = args => {
console.log('f1', args)
return args
}
const f2: composeFunc = args => {
console.log('f2', args)
return args
}
const f3: composeFunc = args => {
console.log('f3', args)
return args
}
const f4: composeFunc = args => {
console.log('f4', args)
return args
}
const c = compose(
f1,
f2,
f3,
f4
)
const res = c('hello')
console.log('res', res)
打印的结果
f4 hello
f3 hello
f2 hello
f1 hello
res hello
从左到右依次是 f1、f2、f3、f4 最后执行的结果是 f4、f3、f2、f1,符合 d = compose(a, b, c) => d(x) === a(b(c(x)))
applyMiddleware
applyMiddleware 是应用 redux middleware 的地方。这个函数设计的非常精妙!!!
function applyMiddleware(...middlewares) {
// const enhancer = compose(applyMiddleware(...middlewares))
// enhancer(createStore)(reducer, preloadedState) reateStore 的返回值
return createStore => (...args) => {
const store = createStore(...args) // args => reducer, preloadedState
// 这里定义一个假函数,传递给 middlewareAPI api 了,但是非常牛13 的是,middlewareAPI 通过闭包,可以获取到最新的 dispatch
// 也就是可以获取到 compose 后的 dispatch
let dispatch = () => { throw new Erro('不能在 middleware 构造之前调用 dispatch') }
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args),
}
//chain 和 dispatch 的生成可以参照下面写 middleware ,来反向推
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch) // 组合 middleware
return {
...store,
// 用新的 dispatch 替代 createStore 生成的 dispatch,让中间件发挥威力!!!
dispatch,
}
}
}
我们来逐行分析:
- 它直接返回一个函数,函数的参数是
createStore,这和它被调用的地方对应enhancer(createStore)(reducer, preloadedState) - 先定义一个
dispatch,这个dispatch在 middlewarwe 构造完之前不能调用 - 生成
middlewareAPI会被传到 middleware 中,它包含getState获取 state 状态树,以及dispatch,dispatch用函数包裹了一层,这样可以利用闭包,让 middleware 内部使用构造好的disaptch(这句很绕,其实就是使用最新的dispatch,dispatch后面赋值为compose(x)(y)的返回值,是一个新的dispatch),通过下面测试可以看到dispatch是使用最新赋值,dispatch由于是在函数内惰性获取值得到,所以一直都是最新的。 const chain = middlewares.map(middleware => middleware(middlewareAPI))一个简单的 middleware 内部包含三个函数, map 把middlewareAPI传了进去,让内部的函数可以使用到getState、dispatch,返回一个函数数组。dispatch = compose(...chain)(store.dispatch)!!!这句非常难看懂,compose(...chain)把所有的 middleware 第二层函数前后进行包裹(上面 compose 有详细效果),其返回是一个函数tmp1,tmp1(store.dispatch)返回一个函数,这个函数可以接收 action 作为参数,要注意 chain 内的每一个函数,其参数是dispatch,而且其可以返回一个函数 t,函数 t 的参数是 action({type: xxx, payload: yyy})。
一个简单的 middleware 包含 3 层函数,(storeCtx) => (nextDispatch) => action => {}。
function loggerMiddleware(storeContext) {
return function(nextDispatch) { // nextDispatch => next ,和 Koa 中的 middleware 很像
// 上面的 store.dispatch
return function(action) {
// dispatch 之前
console.log('before', storeContext.getState())
nextDispatch(action)
// dispatch 之后
console.log('after', storeContext.getState())
}
}
}
可前往 lxfriday.xyz/react-sourc… 打开开发者工具,点击增加、减少查看实际变化。
内部实现可参考 redux middleware 详解
洋葱模型的执行流程。
可以看到 compose(m1, m2) => m1(m2(dispatch))的效果,正好实现从 m1 内的 before 先开始,再到 m2 内的 after 最后执行。
下面我们来看看 thunk middleware 的实现逻辑 react-thunk,总共也没几行。
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
createThunkMiddleware 的使用方式是:先createThunkMiddleware(...args) 把一些预置参数给 extraArgument ,返回的就是一个 middleware 函数,if 判断如果 action 是函数,则直接用 return 截断洋葱模型的执行流,不再继续往洋葱内部执行,可以看下面的图,图片来自知乎 @胡满川。
我写了一个假的网络请求 getWeather.js
import { weatherGetData } from './reducers/weather'
function getWeather(dispatch, getState, extraArgs) {
console.log('state', getState())
console.log('extraArgs', extraArgs)
new Promise((res, rej) => {
setTimeout(() => {
res({
data: {
location: 'wuhan',
condition: 'cloudy',
},
})
}, 2000)
}).then(({ data }) => {
dispatch({
type: weatherGetData,
payload: data,
})
})
}
export default getWeather
reducer
const namespace = 'weather'
export const weatherGetData = `${namespace}/getData`
const INITIAL_STATE = {
location: '',
condition: '',
}
export default function(state = INITIAL_STATE, action) {
switch (action.type) {
case weatherGetData:
return {
...state,
...action.payload,
}
default:
return state
}
}
然后配置文件
import thunk from 'redux-thunk'
import { createStore, applyMiddleware, compose } from '../lib/redux'
import rootReducer from '../reducers'
// 添加额外的信息
const enhancers = compose(applyMiddleware(thunk.withExtraArgument({ info: 'extra args when applyMiddleware' })))
export default function configStore() {
const store = createStore(rootReducer, enhancers)
return store
}
使用的地方 ReactRedux.js
点击 测试 thunk,会把 withExtraArgument 传递的参数打印出来,这个参数是所有的 thunk action 函数都可以获取到的,数据可以正常显示。
react-redux 原理
主要讲两个常用的功能:Provider 和 connect。
Provider
Provider 内部实现和 React.createContext().Provider 一致,大概也就是这个思路。
const ReactReduxContext = React.createContext(null)
export function Provider({ store, children }) {
return <ReactReduxContext.Provider value={store}>{children}</ReactReduxContext.Provider>
}
connect
接下来才是重点,connect!!!它的源码可以把人看吐。它里面包含了非常多性能优化,大量使用 useMemo。现在这个版本的内部实现使用的 functional component,内部使用 useReducer实现视图刷新。
看看用法 @connect(state => ({xxx}), (dispatch) => ({xxx, dispatch}), ...)(Comp),connect 大家用很多,参数就不说了,connect的返回值是一个 t 函数, t 函数的参数是一个 Component 而且 t 函数的返回值也是一个 Component。源码是在返回的 functional Component 内用 useContext 获取从 Provider 中提取的值。具体实现可以看下面的 模拟实现。
造 redux
我在 react-source-analyse 项目里面实现了 mini 版本的 redux 和 react-redux,效果前往 MiniRedux
mini 版本的实现不包含复杂性能优化和错误提醒,尽可能直观的体现核心功能。
贴上源码
// redux.js
/**
* redux 模拟实现
*/
// 初始化 redux 中的数据
const REDUX_INIT = '@@redux/INIT'
/**
* 状态树创建的地方,返回 getState、subscribe、dispatch
*
* @param {*} reducer
* @param {*} enhancer
*/
export function createStore(reducer, enhancer) {
if (typeof enhancer === 'function') {
return enhancer(createStore)(reducer)
}
let currentState
let currentReducer = reducer
let listeners = []
function getState() {
return currentState
}
function dispatch(action) {
// 闭包,reducer 返回的 state 会是新的 state
currentState = currentReducer(currentState, action)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
// 这个很重要,初始情况下,把 reducer 中的数据挂到 currentState 中
dispatch({ type: REDUX_INIT })
function subscribe(listener) {
// 把传进来的订阅函数推入 listeners 数组
listeners.push(listener)
// 返回的取消订阅的函数
return function unsubscribe() {
const index = listeners.indexOf(listener)
// 删除订阅器
listeners.splice(index, 1)
}
}
return {
getState, // 获取 state
dispatch, // 触发数据变更
subscribe, // 刷新 connect 组件的时候用到 dispatch 之后触发视图强制渲染
}
}
/**
* 组合 reducer 的函数,处理有多个 reducer 的时候如何把它们组合成一个大 reducer,
* 其实就是把 action 分发到每个 reducer,匹配对应的一个 switch,然后返回一个新的 state
* 所有的 reducer 函数都符合 `function (state, action)` 签名
*/
export function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers)
return function combination(state = {}, action) {
const nextState = {} // 这个地方做了简化的处理,实际上为了性能优化(useMemo),能不返回新 state 就一定不要返回它
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i] // reducer 函数名
const reducer = reducers[key] // 当前的 reducer 函数
const previousStateForKey = state[key] // 调用 reducer 之前的 state
// 这句的调用有前提,reducer 函数内 switch case 语句 必须有 default 条件,要返回传入的 state
// 这样就表示 reducer 调用没有更改 state
const nextStateForKey = reducer(previousStateForKey, action)
nextState[key] = nextStateForKey
}
// 如果发生了变更,大 state 也会返回新生成的 state
return nextState
}
}
export function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
export function applyMiddleware(...middleware) {
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = () => {
throw new Error('暂时不能调用')
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args),
}
const chains = middleware.map(m => m(middlewareAPI))
dispatch = compose(...chains)(store.dispatch)
return {
...store,
dispatch,
}
}
}
造 react-redux
mini 版本的 react-redux 实现,掘金对 jsx 的代码高亮一直没显示正常过,可以去 github 看有高亮的代码。connect 中有对 dispatch 的订阅(subscribe),当 disptch 的时候会把通过 subscribe 注册的监听函数全部执行一遍,这样 connect 内部就可以知道 state 变化了,再使用 useReducer 刷新。
// react-redux.js
/**
* 模拟实现 react-redux
*/
import React, { createContext, useContext, useReducer, useEffect } from 'react'
// 贯穿 App 的 context
const ReactReduxContext = createContext(null)
/**
* 它会被当成一个 React 组件
*/
export function Provider({ store, children }) {
return <ReactReduxContext.Provider value={store}>{children}</ReactReduxContext.Provider>
}
const initStateUpdates = () => [null, 0]
function storeStateUpdatesReducer(state, action) {
console.log('storeStateUpdatesReducer', state, action)
const [, updateCount] = state
return [action.payload, updateCount + 1]
}
/**
* 这是一个 HOC
* connect 源码的实现非常复杂,Dan 写过一个简单实例
* @link https://gist.github.com/gaearon/1d19088790e70ac32ea636c025ba424e
*/
export function connect(mapStateToProps, mapDispatchToProps) {
// 用法 @connect(state => xxx, (dispatch) => xxx)(Comp)
return function(WrappedComponent) {
// 组件
function ConnectFunc(props) {
const store = useContext(ReactReduxContext)
const [, forceComponentUpdateDispatch] = useReducer(storeStateUpdatesReducer, null, initStateUpdates)
const state = store.getState()
// 订阅 dispatch 行为
useEffect(() => {
const unsubscribe = store.subscribe(() => {
forceComponentUpdateDispatch({
type: '@@redux/STORE_UPDATED',
payload: {
latestStoreState: state,
},
})
})
return () => {
// 每次组件重新渲染的时候取消上一次的订阅,否则订阅数会一直增加
unsubscribe()
}
})
return <WrappedComponent {...props} {...mapStateToProps(state, props)} {...mapDispatchToProps(store.dispatch, props)} />
}
return ConnectFunc
}
}
上面 connect 函数的实现有点复杂,它的用法是 @connect(state => xxx, (dispatch) => xxx)(Comp),所以第一次执行会返回一个函数 func1, func1 执行返回一个 React 组件,在 functional component 内部就可以使用 Hooks 函数获取 Context 以及使用 useReducer。
Redux 作者 Dan 也写了一个 connect 的实现思路 connect.js
// connect() is a function that injects Redux-related props into your component.
// You can inject data and callbacks that change that data by dispatching actions.
function connect(mapStateToProps, mapDispatchToProps) {
// It lets us inject component as the last step so people can use it as a decorator.
// Generally you don't need to worry about it.
return function (WrappedComponent) {
// It returns a component
return class extends React.Component {
render() {
return (
// that renders your component
<WrappedComponent
{/* with its props */}
{...this.props}
{/* and additional props calculated from Redux store */}
{...mapStateToProps(store.getState(), this.props)}
{...mapDispatchToProps(store.dispatch, this.props)}
/>
)
}
componentDidMount() {
// it remembers to subscribe to the store so it doesn't miss updates
this.unsubscribe = store.subscribe(this.handleChange.bind(this))
}
componentWillUnmount() {
// and unsubscribe later
this.unsubscribe()
}
handleChange() {
// and whenever the store state changes, it re-renders.
this.forceUpdate()
}
}
}
}
// This is not the real implementation but a mental model.
// It skips the question of where we get the "store" from (answer: <Provider> puts it in React context)
// and it skips any performance optimizations (real connect() makes sure we don't re-render in vain).
// The purpose of connect() is that you don't have to think about
// subscribing to the store or perf optimizations yourself, and
// instead you can specify how to get props based on Redux store state:
const ConnectedCounter = connect(
// Given Redux state, return props
state => ({
value: state.counter,
}),
// Given Redux dispatch, return callback props
dispatch => ({
onIncrement() {
dispatch({ type: 'INCREMENT' })
}
})
)(Counter)
- 系列文章地址 github.com/lxfriday/gi…
参考文章
- kenberkeley/redux-simple-tutorial
- 用最基础的方法讲解 Redux 实现原理
- react-redux一点就透,我这么笨都懂了!
- redux middleware 详解
- 图解Redux中middleware的洋葱模型
欢迎大家关注我的掘金和公众号,算法、TypeScript、React 及其生态源码定期讲解。