【100 Line Code】第一幕:Redux

206 阅读7分钟

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 实例上会携带两个方法:

  1. getState:用来获取当前的状态。
  2. dispatch:用来触发 Action 更新状态。
const store = createStore(state = { num: 1 }, reducer)

store.getState() // { num: 1 }
store.dispatch(ADD_NUM)
store.getState() // { num: 2 }

👨🏻‍🍳 制作 Minux

实现一个完整的 Minux “生态” 需要实现三个部件:

  1. Minux 的核心方法,即 createStore 方法。
  2. Minux 与 React 的捆绑工具,即 react-minux。
  3. 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。😉