手把手教你如何写出 Redux(源码解析)

前言


在学习 React 的过程中的小伙伴一定对 Redux 不陌生,并且对 Redux 的一系列流程感到困惑。
其实要了解 Redux 首先要知道三点:

  1. Redux 和 React 没有任何关系, 它也可以用在 Vue 等任何的框架中;
  2. Redux 是单向数据流;
  3. Redux 仅仅是一个状态管理工具。

源码解析


既然 Redux 是一个状态管理工具,那么我们就先从简单的计数器开始实现一个最简单的状态管理器。

1. 状态管理器

先设置一个 state 用于保存状态:

let state = {
    number: 1 
}

如果需要使用状态的时候需要调用 state.number, 如果需要重新赋值则调用 state.number = 2,但是这样会出现一个问题。使用 number 的地方感知不到 number 的变化。

我们使用发布订阅模式来通知使用过 number 的订阅者。

let state = {
    number: 1 
}

let listeners = [];

/* 
    订阅 
*/
function subscribe(listener) {
    listeners.push(listener)
}
/* 
    赋值 number
*/
function setNumber(number) {
    state.number = number
    /* 广播所有订阅者 */
    for(let listener of listeners) {
      listener()
    } 
}

这样一个通过订阅发布模式的计数器就完成了,我们尝试一下。

/* TRY IT */
/* 
    订阅
*/
subscribe(() => {
    console.log('Number haven been changed.')
})
/* 
    改变 number
*/
setNumber(2)

现在可以看到当执行了 setNumber 之后,订阅的方法被触发并且控制台输出更新后的值。

2. 提取公共代码

虽然代码奏效,但是新的问题又出现了,这个状态管理器只能处理 number。因此咱们把公共的方法提取出来试一下。

export default function createStore(preloadedState) {
    let state = preloadedState
    let listeners = []

    function subscribe(listener) {
        /* 订阅 */
        listeners.push(listener)
    }

    function getState() {
        /* 获取当前 State */
        return state
    }

    function changeState(newState) {
        /* 更新 State */
        state = newState
        /* 广播所有订阅者 */
        for (listener of listeners) {
            listener()
        }
    }

    /* 将封装的方法返回 */
    return {
        subscribe,
        getState,
        changeState
    }
}

给初始 State 增加不同的值再试试

/* TRY IT */

/* state初始值 */
let initState = {
    counter: {
        number: 1
    },
    article: {
        title: '',
        author: ''

    }
}

/* 创建 Store */
let store = createStore(initState)

/* 订阅 */
store.subscribe(() => {
    let state = store.getState()
    console.log(`State haven been changed. ${state.counter.number}`)
})
store.subscribe(() => {
    let state = store.getState()
    console.log(`State haven been changed. ${state.article.title} By ${state.article.author}`)
})

/* 更改State */
store.changeState({
    ...store.getState(),
    counter: {
        number: 2
    },
    article: {
        title: 'How to use Redux',
        author: 'Cheuk'
    }
})

store.changeState({
    ...store.getState(),
    counter: {
        number: 3
    },
    article: {
        title: 'Simple Demo',
        author: 'Lee'
    }
})

运行之后可以在控制台看到输出

State haven been changed. 2
State haven been changed. How to use Redux By Cheuk
State haven been changed. 3>
State haven been changed. Simple Demo By Lee

看来成功了!我们现在已经完成一个简单的状态管理器。通过 createStore 创建 Store,其中提供了三个方法:

  1. subscribe 用于改变订阅状态;
  2. changeState 用于改变 State 状态;
  3. getState 用于获取当前状态。

3. 实现有计划的状态管理器

咱们的状态管理器看起来不错,但是目前还是存在一个问题。基于上一节的状态管理器实现一个自增自减的计数器。

/* TRY IT */

/* state初始值 */
let initState = {
    count: 0
}

/* 创建 Store */
let store = createStore(initState)

/* 订阅 */
store.subscribe(() => {
    let state = store.getState()
    console.log(`Count: ${state.count}`)
})

/* 自增 */
store.changeState({
    ...store.getState(),
    count: store.getState().count + 1
    
})
/* 自减 */
store.changeState({
    ...store.getState(),
    count: store.getState().count - 1
})
/* WTF?! */
store.changeState({
    ...store.getState(),
    count: 'Bad String'
})

问题很明显,State 中的 count 被改成了字符串。由于计数器没有任何约束, State 中的值可能会在任何地方被改成任何值,这样程序就很难维护。因此我们要给现在的状态管理器添加约束。

两步来解决问题:

  1. 初始化 store 的时候,让他知道我们的修改计划是什么,制定一个 state 的修改计划;
  2. 修改 store.changeState 方法,让他在修改 state 的时候,按照我们的计划来修改。

我们来声明一个 plan 方法用于接收 state 和修改计划(action.type),最后返回改变后的 state 。

/* Our Plan */
/* action 中必须要有 type 属性 */
function plan (state, action){
    switch(action.type) {
        case 'INCREMENT':
            return {
                ...state,
                count: state.count + 1
            }
        case 'DECREMENT':
            return {
                ...state,
                count: state.count - 1
            }
        default: 
            return state
    }
}

接着修改一下之前的 createStore

/* createStore 的时候将 plan 方法作为参数传入 */
function createStore(plan, preloadedState) {
    let state = preloadedState
    let listeners = []

    function subscribe(listener) {
        listeners.push(listener)
    }

    function getState() {
        return state
    }

    function changeState(action) {
        /* 根据 plan 修改 state */
        state = plan(state, action)
        for (listener of listeners) {
            listener()
        }
    }

    return {
        subscribe,
        getState,
        changeState
    }
}

现在我们再试试新的 createStore 实现的自增自减:

/* TRY IT */

/* state初始值 */
let initState = {
    count: 0
}

/* 创建 Store */
let store = createStore(plan, initState)

/* 订阅 */
store.subscribe(() => {
    let state = store.getState()
    console.log(`Count: ${state.count}`)
})

/* 自增 */
store.changeState({
    type: 'INCREMENT'
})
/* 自减 */
store.changeState({
    type: 'DECREMENT'
})
/* 传入无效的值和 type 不会影响我们的 State */
store.changeState({
    count: 'not work'
})
store.changeState({
    type: 'BAD_TYPE'
})

到了这一步我们的状态管理器已经可以根据自定义的计划来工作了。
接下来把代码中的 plan 改成 reducer,把 changeState 改成 dispatch 就和 Redux 中的变量名一样了。

4. 合并 reducer

目前我们的状态管理器就更加完善了,我们可以通过 reducer 接受旧的 state 做一系列处理再返回新的 state。但是在实际项目中可能会有大量的 state,如果每个 state 的 reducer 都写在一个方法中做 type 判断那实在是难以维护。

因此我们可以按照组件的维度来拆分 reducer 函数,然后再通过一个合并函数将每个 reducer 组合起来。

比如现在有两个 state:

let initState = {
    counter: {
        number: 1
    },
    article: {
        title: 'How to write a Redux',
        author: 'Cheuk'
    }
}

需要两个 reducer:

/* counterReducer */
function countReducer (state, action){
    switch(action.type) {
        case 'INCREMENT':
            return {
                ...state,
                count: state.count + 1
            }
        case 'DECREMENT':
            return {
                ...state,
                count: state.count - 1
            }
        default: 
            return state
    }
}
/* articleReducer */
function articleReducer (state, action){
    switch(action.type) {
        case 'SET_NAME':
            return {
                ...state,
                name: action.name
            }
        case 'SET_TITLE':
            return {
                ...state,
                title: action.title
            }
        default: 
            return state
    }
}

我们再通过 combineReducers 方法,返回一个合并的 reducer,传入 state 和 action, 可以返回新的 state:

let reducer = combineReducers({
    counter: countReducer,
    article: articleReducer
})

实现 combineReducers 方法:

/* 
*   combineReducers.js
*/
function combineReducers (reducers){
    const reducerKeys = Object.keys(reducers)
    return function combination(state = {}, action) {
        // 初始化新的 state
        const nextState = {}
        for (let key of reducerKeys) {
            // 根据 key 得到对应模块的reducer
            const reducer = reducers[key]
            // 根据 key 得到对应模块的旧的 state
            const previousStateForKey = state[key]
            // 将 state 和 action 传入 reducer 返回新的 state
            const nextStateForKey = reducer(previousStateForKey, action)
            // 每个模块的 state 都通过 key 传入新的 state 中
            nextState[key] = nextStateForKey;
        }
        // 返回一个新的 state
        return nextState
    }
}

到这里我们已经可以将不同模块的 reducer 通过combineReducers 方法组合在一起,来测试一下:

/* TRY IT */
let reducer = combineReducers({
    counter: countReducer,
    article: articleReducer
})
/* state初始值 */
let initState = {
    counter: {
        count: 1
    },
    article: {
        title: 'old title',
        author: 'old author'

    }
}

/* 创建 Store */
let store = createStore(reducer, initState)

/* 订阅 */
store.subscribe(() => {
    let state = store.getState()
    console.log(`state.article: { author: ${state.article.author}, title: ${state.article.title} }`)
})
store.subscribe(() => {
    let state = store.getState()
    console.log(`state.counter: { count: ${state.counter.count} }`)
})

/* 自增 */
store.dispatch({
    type: 'INCREMENT'
})
/* 自减 */
store.dispatch({
    type: 'DECREMENT'
})
/* 更新 author */
store.dispatch({
    type: 'SET_AUTHOR',
    author: 'new author'
})
/* 更新 title */
store.dispatch({
    type: 'SET_TITLE',
    title: 'new title'
})

棒极了,现在我们已经可以在项目中把 reducer 按照组件模块拆分,这样业务中每个组件只需要维护自己的reducer,最后再合并到一起就可以了。

5. 整合 state

上一节中尽管我们现在拆分了 reducer,可是 state 还是写在了一起。那么我们需要把 state 也按照组件拆分到各个模块中。

比如上一节的 counter 在业务中我们希望:

/* counterReducer.js*/
//单个的state
let initState = {
    count: 1
/* counterReducer */
function countReducer (state, action){
    /*如果参数 state 没有初始值,那就给他初始值*/
    if (!state) {
      state = initState;
    }
    switch(action.type) {
        case 'INCREMENT':
            return {
                ...state,
                count: state.count + 1
            }
        case 'DECREMENT':
            return {
                ...state,
                count: state.count - 1
            }
        default: 
            return state
    }
}

在初始化的时候每个组件的 reducer 中我们都设置了各自组件的默认值,那么最后我们需要在 createStore 中整合所有的 state :

function createStore(reducer, preloadedState) {
    let state = preloadedState
    let listeners = []

    function subscribe(listener) {
        listeners.push(listener)
    }

    function getState() {
        return state
    }

    function dispatch(action) {
        state = reducer(state, action)
        for (listener of listeners) {
            listener()
        }
    }
    /**通过一个不匹配任何 reducer的 type,来全部的初始值*/
    /**redux 中是使用一个随机的字符串来保证不匹配任何 reducer, 这里使用了 ES6 的 Symbol 类型*/
    dispatch({ type: Symbol() });

    return {
        subscribe,
        getState,
        dispatch
    }
}

完美,现在我们的状态管理器已经完全可以根据组件拆分成不同模块了。
对比 Redux 我们的状态管理器还缺少中间件。

6.实现中间件

什么是中间件?

It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer. People use Redux middleware for logging, crash reporting, talking to an asynchronous API, routing, and more.

根据官方文档的解释就是,中间件是对 dispatch 功能的扩展。我们可以通过中间件做日志记录,崩溃报告,与异步API通信,路由等工作。

实现一个记录日志的中间件
我们在每次要修改 state 的时候记录下修改前的 state 和修改后的 state 以及action:

/* 创建 Store */
let store = createStore(reducer, initState)

/* Logger Middleware */
const next = store.dispatch

store.dispatch = action => {
    console.log("======日志======", new Date())
    console.log("修改前的state: ", store.getState())
    console.log("action: ", action)
    next(action)
    console.log("修改后的state: ", store.getState())
    console.log("================")
}

接着上一节的例子,我们添加了一个日志的中间件,执行之后就能看到日志输出了。
很棒!我们的日志中间件成功了,但是我们收到了新的需求,需要把错误日志也打印出来。好吧,只有加下班了。

实现一个记录异常日志的中间件
修改一下我们的日志中间件:

store.dispatch = action => {
    try{
        console.log("======日志======", new Date())
        console.log("修改前的state: ", store.getState())
        console.log("action: ", action)
        next(action)
        console.log("修改后的state: ", store.getState())
        console.log("================")
    } catch(err) {
        console.error("错误报告: ", err)
    }
}

完美!可是这时候我们发现如果一直有新的需求就需要不断地增加 dispatch,这可实在是不好维护,我们需要把中间件提取出来:

/* 提取 Logger Middleware */
store.dispatch 
let loggerMiddleware = action => {
    console.log("======日志======", new Date())
    console.log("修改前的state: ", store.getState())
    console.log("action: ", action)
    next(action)
    console.log("修改后的state: ", store.getState())
    console.log("================")
}
/* 提取 Exception Middleware */
let exceptionMiddleware  = action => {
    try{
        loggerMiddleware(action)
    } catch(err) {
        console.error("错误报告: ", err)
    }
}
store.dispatch = exceptionMiddleware

但是这样一来,每个中间件之间互相耦合,也不太好。我们用一种组合的方式将每个中间件结合起来:

/* 提取 Logger Middleware */
store.dispatch 
let loggerMiddleware = next => action => {
    console.log("======日志======", new Date())
    console.log("修改前的state: ", store.getState())
    console.log("action: ", action)
    next(action)
    console.log("修改后的state: ", store.getState())
    console.log("================")
}
/* 提取 Exception Middleware */
let exceptionMiddleware = next =>action => {
    try{
        next(action)
    } catch(err) {
        console.error("错误报告: ", err)
    }
}
/* 通过一层层地执行 */
store.dispatch = exceptionMiddleware(loggerMiddleware(next))

看起还行,但是还存在一个问题,因为中间件大多数是第三方插件,因此像loggerMiddelware 这样的中间需要从外部获取 store,因此我们需要把 store 也在作为参数传进来:

/* 提取 Logger Middleware */
store.dispatch 
let loggerMiddleware = store => next => action => {
    console.log("======日志======", new Date())
    console.log("修改前的state: ", store.getState())
    console.log("action: ", action)
    next(action)
    console.log("修改后的state: ", store.getState())
    console.log("================")
}
/* 提取 Exception Middleware */
let exceptionMiddleware = store => next =>action => {
    try{
        next(action)
    } catch(err) {
        console.error("错误报告: ", err)
    }
}
/* 传入store */
let exception = exceptionMiddleware(store)
let logger = loggerMiddleware(store)
/* 通过一层层地执行 */
store.dispatch = exception(logger(next))

现在我们已经可以完全将这两个中间件作为第三方的插件独立于项目之外,我们的状态管理器得到了扩展。

7.优化中间件

上面的例子使用起来其实还不够友好,既然已知三个需要用到的中间件,我们可以把其中实现的细节封装起来:

const newCreateStore = applyMiddleware(
    exceptionMiddleware,
    timeMiddleware,
    loggerMiddleware
)(createStore)

const store = newCreateStore(reducer, initState);

实现 Redux 中的 applyMiddleware

function applyMiddleware(...middlewares) {
    return (createStore) => (...args) => {
        let store = createStore(...args)
        /* 
            给每一个 middleware 传入store
        */
       let chain = middlewares.map(middleware => middleware(store))
       let dispatch = store.dispatch
       /* 依次嵌套调用 */
       chain.reverse().map(middleware => {
           dispatch = middleware(dispatch)
       })
       /* 重写 store 中的 dispatch */
       store.dispatch = dispatch
       return store
    }
}

但是这样就出现了两个 createStore 的方法,解决的方法是将我们的中间件作为 enhancers 传入 createStore 中处理:

function createStore(reducer, preloadedState, enhancers) {
    let state = preloadedState
    let listeners = []
    // 如果有 enhancers 就将 createStore 传入生成重写过 dispatch 的 store
    if(enhancers) {
        return enhancer(createStore)(reducer, preloadedState)
    }
  // do something
}

OK, 目前为止我们的状态管理器已经把 Redux 中的 applyMiddleware 也是现实了。
最终中间件使用方法已经和 Redux 一样了:

const enhancers = applyMiddleware(
    exceptionMiddleware,
    timeMiddleware,
    loggerMiddleware
)

const store = newCreateStore(reducer, initState, enhancer)

8.退订

修改一下 subscribe,增加退订的方法

function subscribe(listener) {
    listeners.push(listener)
    return function unsubscribe() {
      const index = listeners.indexOf(listener);
      listeners.splice(index, 1)
    }
}

使用:

const unsubscribe = store.subscribe(() => {
    let state = store.getState()
    console.log(state.counter.count)
})
/*退订*/
unsubscribe()

9. 实现compose 方法

我们的 applyMiddleware 中,把 [A, B, C] 转换成 A(B(C(next))),是这样实现的

       let dispatch = store.dispatch
       /* 依次嵌套调用 */
       chain.reverse().map(middleware => {
           dispatch = middleware(dispatch)
       })

在 Redux 中实际上还有一个 compose 方法可以将中间件组合:

export default function compose(...funcs) {
    if (funcs.length === 1) {
      return funcs[0]
    }
    return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

10. 按需加载reducer

reducer 做了拆分之后,就可以和 UI 组件一样做按需加载,用新的 reducer 替换旧的 reducer:

const createStore = function(reducer, initState) {
    function replaceReducer(nextReducer) {
      reducer = nextReducer
      /*刷新一遍 state 的值,新来的 reducer 把自己的默认状态放到 state 树上去*/
      dispatch({ type: Symbol() })
    }
    //其他代码
    return {
      ...replaceReducer
    }
}

使用的例子:

const reducer = combineReducers({
    counter: counterReducer
})
const store = createStore(reducer)

/*生成新的reducer*/
const nextReducer = combineReducers({
    counter: counterReducer,
    info: infoReducer
});
/*replaceReducer*/
store.replaceReducer(nextReducer)

11. 实现 bindActionCreators

bindActionCreators 的作用是通过闭包将 dispatch 和 createStore 方法隐藏,让其他调用 action 的地方感知不到内部的操作,简单的实现是这样的:

const reducer = combineReducers({
  counter: counterReducer,
  info: infoReducer
});
const store = createStore(reducer);

/*返回 action 的函数就叫 actionCreator*/
function increment() {
    return {
      type: "INCREMENT"
    }
}

function setName(name) {
    return {
      type: "SET_NAME",
      name: name
    }
}

const actions = {
    increment: function() {
      return store.dispatch(increment.apply(this, arguments))
    },
    setName: function() {
      return store.dispatch(setName.apply(this, arguments))
    }
}
/*其他引用action的地方,无需知道 dispatch,actionCreator等细节*/
actions.increment() /*自增*/
actions.setName("Cheuk") /*修改 info.name*/

接下来提取公共代码:

function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}

export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error()
  }

  const boundActionCreators = {}
  for (const key in actionCreators) {
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

小结

目前为止, 我们的状态管理器已经把Redux 中的 API 就已经都实现了一遍。Redux 的源码不多,经过自己手写了一遍之后再多读几次,就能对 Redux 的工作流程有更深刻的理解。在第一次读的时候可能一头雾水,当理解了每个API 的用途并且自己写几次,大概就能理解作者的意图了。互勉!

你可能感兴趣的:(手把手教你如何写出 Redux(源码解析))