在日常的项目开发中,状态管理库一定是必不可少的,今天,本文将带领大家深入了解 redux 内部的实现原理,相信在阅读完本文之后,你会对状态管理库有更加深入的认识。
我们先来看一个 redux 使用的例子:
// store.js
import { createStore } from 'redux'
function countReducer(state = 0, { type }) {
switch (type) {
case 'ADD':
return state + 1
case 'MINUS':
return state - 1
default:
return state
}
}
const store = createStore(countReducer)
export default store
// ReduxPage.js
import React, { useEffect, useReducer } from 'react'
import store from '../store/'
export default function ReduxPage() {
const [ignore, forceUpdate] = useReducer((x) => x + 1, 0)
const add = () => {
store.dispatch({ type: 'ADD' })
}
useEffect(() => {
const unsubscribe = store.subscribe(() => {
forceUpdate()
})
return () => {
unsubscribe()
}
}, [])
return (
<div>
<h3>ReduxPage</h3>
<p>{store.getState()}</p>
<button onClick={add}>add</button>
</div>
)
}
上面的代码实现了一个简单的累加器,这里我们可以看到:
- createStore 接收一个 reducer,返回一个 store
- store 提供了以下方法:
- getState:获取当前状态
- dispatch:派发更改状态的请求
- subscribe:监听状态函数
根据上面的信息,我们来思考一下如果想要实现一个简单的 createSotre,需要怎么做:
- currentState:用于存储当前的状态
- currentListeners:用于存储监听事件
- getState:直接返回 currentState 即可
- dispatch:它接收 action 作为参数,首先调用 reducer 更改状态,然后调用监听事件重新渲染
- subscribe:直接将监听事件入栈即可(这里需要注意的是,有监听一定要有取消监听,所以这里要返回一个取消监听的方法)
根据上面的思路,一个简单的 createStore 很容易就能实现了:
export default createState = (reducer) => {
let currentState
let currentListeners
const getState = () => {
return currentState
}
const dispatch = (action) => {
currentState = reducer(currentState, action)
currentListeners.forEach((listener) => {
listener()
})
}
const subscribe = (listener) => {
currentListeners.push(listener)
return () => {
const index = currentListeners.indexOf(listener)
currentListeners.splice(index, 1)
}
}
return {
getState,
dispatch,
subscribe,
}
}
现在我们已经实现了一个简易版的 createStore,但目前还存在一个小坑,我们来看一看:
可以看到,这里 count 一开始是没有初始值的。但我们并不知道用户将设置怎样的初始值,所以为了解决这个问题,这里可以手动调用一下 dispatch,并且将 type 设置成用户不可能设置的值,从而让逻辑一定走 default,即返回 initState:
dispatch({ type: 'di12jd9xhawerfy12l4hhdfsdahf/12390uisjdfwsf' })
可以看到,现在已经能拿到初始值了。
扩展
我们知道,为了可预测性,用于改变状态的 reducer 一定是一个纯函数,而在纯函数中是不能带有副作用的,但在实际的工作中我们往往需要在 dispatch 中向后端请求数据,通过获取的数据去更新状态,那应该怎么办呢?
redux 的方法是提供了中间件,比如 redux-thunk,我们先来看看它是怎么做的:
// store.js
import { createStore, applyMiddleware } from 'redux'
import thunk from "redux-thunk";
function countReducer(state = 0, { type }) {
switch (type) {
case 'ADD':
return state + 1
case 'MINUS':
return state - 1
default:
return state
}
}
const store = createStore(countReducer, applyMiddleware(thunk))
export default store
// ReduxPage.js
export default function ReduxPage() {
const [ignore, forceUpdate] = useReducer((x) => x + 1, 0)
const add = () => {
store.dispatch({ type: 'ADD' })
}
const asyncAdd = () => {
store.dispatch((dispatch) => {
setTimeout(() => {
dispatch({ type: 'ADD' })
}, 1000)
})
}
useEffect(() => {
const unsubscribe = store.subscribe(() => {
forceUpdate()
})
return () => {
unsubscribe()
}
}, [])
return (
<div>
<h3>ReduxPage</h3>
<p>{store.getState()}</p>
<button onClick={add}>add</button>
<button onClick={asyncAdd}>add async</button>
</div>
)
}
可以看到,这里有以下改变:
- 引入了一个 applyMiddleware,看起来是用来注册中间件的,这里注册了 thunk
- dispatch 可以接受一个回调函数,这个函数的第一个参数是 dispatch,所以可以在回调函数中进行数据请求,请求结束后再调用 dispatch 去修改状态
那么这里我们借用原生的 thunk,先来实现 applyMiddleware。
首先需要稍微修改一下 createStore,可以知道的是:
- 当没有传入 applyMiddleware 的时候,返回的是普通的 store
- 当有传入 applyMiddleware 的时候,将对 reducer 进行增强
那么我们只需要判断是否有第二个参数即可:
export default function createStore(reducer, applyMiddleware) {
if (applyMiddleware) {
/* applyMiddleware */
}
/* ... */
}
接下来的问题是,这里 applyMiddleware 应该怎么做呢?
从上面我们已经知道 dispatch 有了新的功能,那么这里显然应该是对 dispatch 进行增强,增强的具体参数则由 thunk 控制。
所以 applyMiddleware 应该是一个工厂,这里我们先创建和返回普通的 store:
export default (...middlewares) => {
return (createStore) => (reducer) => {
const store = createStore(reducer)
return {
...store,
}
}
}
那么 createStore 也可以稍微修改一下:
export default function createStore(reducer, enhancer) {
if (enhancer) {
enhancer(createStore)(reducer)
}
/* ... */
}
接下来就是对 dispatch 的增强了:
- 首先遍历需要注册的中间件(这里只有 thunk),生成增强函数链
- 另外前面我们知道,被增强的 dispatch 的回调中有用真实 dispatch 这个方法,所以这个方法需要传递给增强函数(实际上还有 getState)
代码如下:
const midApi = {
getState: store.getState,
dispatch: (action) => store.dispatch(action),
}
const middlewaresChain = middlewares.map((middleware) => middleware(midApi))
接下来就是将这些链中的方法组合起来,用于增强 dispatch:
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)))
}
const dispatch = compose(...middlewareChain)(store.dispatch)
最后将增强后的 dispatch 返回即可:
return {
...store,
dispatch,
}
效果如下:
接下来就来实现 thunk。
同样的,来分析一下 thunk 是如何对 dipatch 进行增强的,那么回到应用代码:
const asyncAdd = () => {
store.dispatch((dispatch) => {
setTimeout(() => {
dispatch({ type: 'ADD' })
}, 1000)
})
}
显然,在 thunk 的增强下,dispatch 可以接受一个函数作为参数,并且会将原本的 dispatch 作为参数传递给这个回调函数。
这里稍微理一下思路:
- 首先在遍历增强函数的时候会调用 thunk,这时候它接到的参数是 midApi,并且将返回用于增强 dispatch 的方法
- 接着调用增强 dispatch 的方法,参数是 dispatch,返回是增强的 dispatch
- 如何增强呢?
- 如果 dispatch 接受的参数 action 是函数,则调用回调函数,并且将真正的 dispatch 传递过去
- 否则直接调用 dispatch 即可
顺着思路,代码也就出来了(这里显然也是要用到工厂):
// thunk.js
export default ({ getState, dispatch }) => {
return (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState)
} else {
return next(action)
}
}
}
结语
本文的探讨就到此为止,如果有误,望不吝指出。