前端造轮子【5】- 简易 redux

549 阅读5分钟

在日常的项目开发中,状态管理库一定是必不可少的,今天,本文将带领大家深入了解 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)
    }
  }
}

结语

本文的探讨就到此为止,如果有误,望不吝指出。