We don't know what we can't create.
-- Feynman
ps:在这里查看和运行本帖涉及的完整 代码
🍚 食用前
本贴的目的是介绍如何使用仅仅 100 行代码,参照 Redux 源码构建一个【可以跑起来】的前端状态管理【玩具】。
本帖涉及 Redux、React-Redux 的源码,源码在剔除注释、TS类型之后大概也就几百行代码,所以你可以把本帖视作阅读相关源码前的一道开胃小菜。
本贴是面向使用过 Redux 进行前端开发的读者,如果你还没有接触过它,建议你阅读 官方文档 并尝试写一些简单的 Demo 之后再食用本贴。 🙂
Minux (米纳克斯)
本贴的目标就是介绍如何制造一个与 Redux 相仿的玩具,同样使用 Store、Reducer 和 Action 来管理前端应用状态,我把这个玩具叫做 Minux 。
简单回顾一下 Store、Reducer 和 Action :
Action
Action 是一个简单的对象,用来表示更新某个状态的动作(type),以及携带更新所需的数据:
const ADD_NUM = { type: 'ADD', data: 1 }
Reducer
Reducer 是一个函数,用来描述如何根据 Action 更新状态:
const reducer = (state, action) => {
switch(action.type) {
case: 'ADD':
return { ...state, num: state.num + action.data }
default:
return state
}
}
Store
Store 通过 createStore 方法创建,与 Redux 一样,Minux 的核心工作就是实现 createStore 方法。
通过 createStore 方法创建的 Store 实例上会携带两个方法:
- getState:用来获取当前的状态。
- dispatch:用来触发 Action 更新状态。
const store = createStore(state = { num: 1 }, reducer)
store.getState() // { num: 1 }
store.dispatch(ADD_NUM)
store.getState() // { num: 2 }
👨🏻🍳 制作 Minux
实现一个完整的 Minux “生态” 需要实现三个部件:
- Minux 的核心方法,即 createStore 方法。
- Minux 与 React 的捆绑工具,即 react-minux。
- Minux 中间件扩展方法,即 applyMiddleware 方法。
🧬 核心方法:实现 createStore
function createStore (state, reducer) {
let currentState = state
function getState () {
return currentState
}
// dispatch 调用 reducer 来更新 currentState
function dispatch (action) {
currentState = reducer(action)
}
const store = { getState, dispatch }
return store
}
这就是 Minux createStore 核心方法的全部代码😀。createStore 不过是将状态维护在闭包里,然后用我们在之前创造的 reducer 去更新它而已。
到此为止,我们就已经能够通过 Store、Reducer 和 Action 来管理我们的状态了:
const store = createStore({ num: 1 }, reducer)
store.dispatch(ADD_NUM)
store.getState() // { num: 2 }
在工业级别的 Redux 中,createStore 还包括许多函数重载、错误处理、订阅监听等等逻辑,但是处理 state 的核心逻辑与上面 Minux 的版本没什么本质的区别。
📎 捆绑到 React:实现 react-minux
和 react-redux 一样,我们需要实现 react-minux 才能把 Store、Reducer 和 Action 这套逻辑放到 React 上玩。
实现 react-minux 的核心,是通过 React Context API 将 stroe 实例注入到组件的 props 上。如果你没有了解过 React Context API ,可以翻阅 React 官方文档上的一些例子。简单来说,Context API 的作用就是维护一个全局变量,并直接把这些变量提供给组件,而不用从父组件上层层传递下来。
react-minux 会实现两个方法:
1. Provider,用于注入应用的全局 store
2. connect,把 store 提供给需要它的组件使用
为 createStore 添加订阅机制
为了实现对 React 的绑定,我们必须在 store 状态改变时通知 React ,为此,我们需要给之前的 createStore 方法创建的 store 实例上添加订阅机制。
Redux 和 React-Redux 自己实现了订阅机制来通知 React 组件更新,而在 Minux 为了节省代码量😅,我们使用一个第三方库 mitt 来实现发布订阅功能。
function createStore (state, reducer) {
...
let emitter = mitt()
function dispatch (action) {
currentState = reducer(currentState, action)
emitter.emit('NEW_STATES', currentState)
}
const store = { getState, dispatch, emitter }
...
}
实现 Provider
现在,我们就能够实现 react-minux 的 Provider 方法,将全局状态通过 Context API 注入到根组件中了。并且,每当我们调用 store.dispatch 方法的时候,我们都能够通过 store.emitter.on 方法获取到最新的状态注入到 Context 中来驱动 React 组件视图的更新。
const Context = React.createContext(null)
function Provider (props) {
const { store } = props
const [state, setState] = useState(store.getState())
useEffect(() => {
store.emitter.on('NEW_STATES', (state) => setState(state))
}, [])
return (
<Context.Provider value={state}>
{ props.children }
</Context.Provider>
)
}
实现 connect
实现 connect 轻而易举,不过是通过 React Context Consumer 把 state 和 dispatch 注入到组件而已。
// InnerComponent 是待绑定的 React 组件
function connect (InnerComponent, store) {
return () => (
<Context.Consumer>
{ value => <InnerComponent {...value} dispatch={store.dispatch} /> }
</Context.Consumer>
)
}
大功告成!🎉我们可以通过 Provider 和 connect 将我们使用 Minux 管理的 store 注入到我们的应用里了:
function Layout (props) {
const { num, dispatch } = props
return (
<div>
<div>{num}</div>
<button onClick={() => dispatch(ADD_NUM)}>Add Num</button>
</div>
)
}
const AddNum = connect(Layout)
function App () {
return (
<Provider store={store}>
<AddNum />
</Provider>
)
}
⚙️ 扩展 Minux: 实现 applyMiddleware
Redux 的中间件机制使我们能够灵活地使用各种中间件扩展 Redux 的能力,比如支持异步 action 的中间件 redux-thunk。
中间件通过 applyMiddleware 方法扩展,在 Minux 中我们也将实现该方法。
制造中间件
在实现 applyMiddleware 之前,我们先看看如何制造一个中间件。
const thunk = ({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState)
}
return next(action)
}
这是 redux-thunk 的主要代码 (redux-thunk 用来处理异步 anction),它返回了一个很难理解的“三层”高阶函数的怪兽👾,幸好它的函数体的处理逻辑十分简单。
制作一个中间件基本上就是像 redux-thunk 这样,返回一个“三层”高阶函数。我初次看到它的时候很难相信,如此简单的逻辑就足以为 Redux 处理异步 action 。但如果你明白了“三层”函数的参数意义,那么这个高阶函数可能就比较容易理解了。
第一层函数中的 dispatch 参数是最终的 dispatch 方法,这意味着:调用 dispatch 将会从头执行所有的中间件。
第二层函数中的 next 参数其实也是一个 dispatch 方法,只不过调用 next(action) 时,只会执行该中间件之后余下的中间件。
现在你大概能对 redux-thunk 是如何能够处理异步 action 有那么一点想法了是吧?如果 action 是一个(异步)函数,那么我们执行它,并在函数内执行各种(异步)方法后再次调用最终的 dispatch,dispatch 会再次经过 redux-thunk,只不过这次 action 是一个普通对象,我们调用 next 来执行余下的中间件方法并最终通过 reducer 更新状态。
实现 applyMiddleware
在了解如何制造一个中间件之后,我们来看看 applyMiddleware 是如何处理那个“三层”高阶函数,并把 dispatch,next 参数正确地给到每一个中间件(毕竟每个中间件的 next 都不相同,调用 next 只执行余下的中间件)
我认为这是 redux 中最精辟的一段代码逻辑,它使用 compose 将每一个中间件串联起来,一步步创建属于每个中间件的 next 参数,并将最终的 store.dispatch 参数注入到中间件。
// 将中间件挨个链接起来
function compose(funcs) {
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
function applyMiddleware (store, middlewares) {
const dispatch = (action) => {}
const middlewareAPI = {
getState: store.getState,
dispatch,
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
/**
* 通过 compose 方法,每一个中间件都将强化 store.dispatch,并最终返回一个每次调用经历所有中间件的 dispatch 方法。
* 而在每个中间件内部的 next 方法,都将成为来自它之前所有中间强化过的 dispatch 方法。
*/
store.dispatch = compose(...chain)(store.dispatch)
}
👋🏻 最后
我们实现了 Minux,一个仿照 Redux 的前端状态管理玩具。Minux 是一个非常简陋的实现,简陋到比 Redux 的源码要容易理解地多。
Minux 缺少足够的参数处理、健壮的类型判断逻辑甚至是 React 组件 props 的继承处理,但 Minux 的核心与 Redux 是相同的:通过 Action、Reducer、Store 管理状态;通过 React-Minux 捆绑到 React 上使用;通过 applyMiddleware 方法扩展中间件。
最后,你可以在 这里 查看本帖的所有代码以及使用 Minux 构建的 Demo。😉