Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

现代前端科技解析 —— Redux 及其中间件 #64

Open
jin5354 opened this issue Sep 13, 2017 · 0 comments
Open

现代前端科技解析 —— Redux 及其中间件 #64

jin5354 opened this issue Sep 13, 2017 · 0 comments

Comments

@jin5354
Copy link
Owner

jin5354 commented Sep 13, 2017

title: 现代前端科技解析 —— Redux 及其中间件
categories:

  • Code
    tags:
  • Modern web development tech analysis
  • 现代前端科技解析
  • Redux
    toc: true
    date: 2017-9-13 15:45:11

上篇文章中阐释了我对 Redux 架构及其复杂性的看法,提到了 Redux 本质是一个非常简单易懂的状态管理架构,本文将解析 Redux 的源码,并从零实现一个带有中间件系统的 Redux。

注:
原始链接: https://www.404forest.com/2017/09/13/modern-web-development-tech-analysis-redux-with-its-middleware/
文章备份: #64
redux 源码: https://github.com/vuejs/vue/tree/dev/src/core/observer
本文实现的 redux(附注释和 100% 测试):https://github.com/jin5354/leaf-store

1. 设计一个 Redux

首先,我们抽出一个典型的 Redux 用法:

// 初始状态,用户自己写
const initialState = {
  counter: 0,
}

// reducer,用户自己写
// 观察可知:该函数接收旧 state 和 action,返回新 state。若参数均为空,则会返回初始状态。
const reducer = (state = initialState, action) => {
  switch(action.type) {
    case('ADD_COUNTER'): {
      return Object.assign({}, state, {
        counter: state.counter + 1
      })
    }
    default: {
      return state
    }
  }
}

// createStore,Redux 实现,接收 reducer 作为参数
const store = createStore(reducer)

// dispatch,Redux 实现,接收 action 作为参数
store.dispatch({
  type: 'ADD_COUNTER'
})

// getState,Redux 实现
console.log(store.getState().counter)

上面是一个 Redux 的极简用例。我们看到 Redux 主要的几个功能点如下:

  • createStore 根据 reducer 创建 store
  • dispatch 派发 action,执行 reducer 进行数据修改与更新
  • getState 获取当前 state

2. 实现 createStore、dispatch 和 getState

调用 createStore 应该传入一个 reducer,返回一个 store 对象,其包含两个方法:dispatchgetState

function createStore(reducer) {
  let state //当前状态的 state

  const getState = () => { //获取当前 state
    return state
  }

  const dispatch = (action) => {...} //派发 action

  return {
    getState,
    dispatch
  }
}

调用 dispatch 进行数据修改时会传入 action,我们需要执行 reducer 拿到新状态。

function createStore(reducer) {
  let state //当前状态的 state

  const getState = () => { // 获取当前 state
    return state
  }

  const dispatch = (action) => { // 派发 action
    state = reducer(state, action) // 用新 state 替换旧 state
  }

  return {
    getState,
    dispatch
  }
}

现在 getStatedispatch 就可以正常工作了。不过,在 createStore 后,我们调用 getState 返回的是 undefined。我们需要初始化 state 为初始 state。发一个空的 disptach,获得默认 state。

function createStore(reducer) {
  let state //当前状态的 state

  const getState = () => { // 获取当前 state
    return state
  }

  const dispatch = (action) => { // 派发 action
    state = reducer(state, action) // 用新 state 替换旧 state
  }

  // 发一个空的 disptach,获得默认 state, 要求在写 reducer 时在 switch 中加一个 default: return state 分支
  dispatch({})

  return {
    getState,
    dispatch
  }
}

现在我们就得到了一个不过 20 行的极简版的 Redux,其已经能满足第一节中的使用需求了。

在线示例

3. 功能增强

Redux 的源码包括以下几个文件:

Redux-1

其中 createStore.js 实现的即是 createStore 函数,其核心即为上一节的 20 行代码。我们参照 Redux,为我们的极简版本加功能。

3.1 subscribe

在 Redux 中,我们可以使用 store.subscribe(callback) 来注册监听函数,监听函数将在每次 dispatch 之后执行。我们来添加一个 subscribe 函数。这里明显要使用发布-订阅模式,维护一个 listeners 数组,其中存储全部的监听函数。执行 subscribe 返回一个 unsubscribe 函数,执行 unsubscribe 即可解绑。

function createStore(reducer) {
  let state
  const listeners = [] // 存储监听函数

  const getState = () => { ... }

  const subscribe = (listener) => {
    listeners.push(listener)

    return function unsubscribe() { //返回一个 unsubscribe 函数,执行即可解绑。
      const index = listeners.indexOf(listener)  //这样删除有一点点问题……
      listeners.splice(index, 1)
    }
  }

  const dispatch = (action) => {
    state = reducer(state, action)
    listeners.forEach(listener => { // 每次 dispatch 之后执行全部 listen
      listener()
    })
  }

  dispatch({})

  return {
    getState,
    dispatch,
    subscribe
  }
}

目前 Reduxsubscribe 就是这样实现的,不过这里有个小问题:使用 indexOf(listener) 来查找数组中 listener 的位置时,如果 listener 有多个重复的,那么只会返回第一个——也就是说如果你多次 subscribe 了一个函数,那么无论你执行哪一个 unsubscribe,删掉的都是第一个 listener。看看例子:

在线示例

我顺便提了个 PR,维护者认为这是极小概率事件,就先不处理了。要修复这个缺陷也好办,为每个 listener 加一个 unique ID 即可区分。

3.2 combineReducers

大型项目往往是多人开发的,多个人同时修改一个 reducer 极易造成冲突,我们希望 reducer 可以是模块化的,每个人维护一个模块,最终可以通过一个方法组装成 root reducer

const initialStateA = {
  counterA: 0,
}

const reducerA = (state = initialStateA, action) => { ... }

const initialStateB = {
  counterB: 10,
}

const reducerB = (state = initialStateB, action) => { ... }

//通过 combineReducers 方法将各个 reduce 组合起来
const store = createStore(combineReducers({
  reducerA,
  reducerB
}))

console.log(store.getState().reducerA.counterA) // 输出:0
console.log(store.getState().reducerB.counterB) // 输出:10

观察可知,该方法应接收一个包含多个 reducer 的对象(数组也可以,对象可以通过 key 来重命名 reducer),返回一个组装后的 reducer
reducer 将子 reducer 的 state 以 key 为属性挂在根 reducer 的 state 上,每次接受 action 时,按 key 来获取每个子 reducer 的状态,并将产生的新状态进行替换。

/**
 * [combineReducers 组合多个 reducer]
 * @param  {[object]} reducers
 * @return {[type]}
 */
function combineReducers(reducers) {
  //拿到所有子 reducer 的 key
  const reducerKeys = Object.keys(reducers)

  //返回一个组装后的 reduce
  return function combination(state = {}, action) {
    let newState = {}
    let hasChanged = false // 如果各个子 reducer 的 state 均未变化就直接返回原 state
    reducerKeys.forEach(key => {
      // 获取子 reducer 的状态,再用子 reducer 产生的新状态替换掉
      let oldKeyState = state[key]
      let newKeyState = reducers[key](state[key], action)
      newState[key] = newKeyState // 挂在根 state 上
      hasChanged = hasChanged || newKeyState !== oldKeyState
    })
    return hasChanged ? newState : state
  }
}

4.实现中间件系统

如果你使用过 Redux,你一定知道若要在 Redux 流程内处理异步操作,必须借助中间件 Redux-thunk。Redux 自身无法处理异步操作。dispatch 派发的 action 默认只能是一个 plain Object

使用 Redux-thunk 中间件后,dispatch 方法就可以接收一个函数作为参数,在函数中我们就可以实现更多的功能。Redux 是如何设计的中间件系统,使得第三方中间件可以扩展原生 dispatch?

4.1 以 monkeypatch 举例

假设我们要为 dispatch 加一个 log 功能。能够把 dispatch 后的 state 打印出来。而使用时还是直接调用 store.dispatch。我们可以直接对 dispatch 进行魔改。

// 先把原生的 dispatch 存起来
let next = store.dispatch

// 魔改 dispatch
store.dispatch = action => {
  //可以做点预处理
  //执行原生 dispatch
  next(action)
  //还可以再做点后处理
  console.log('dispatch 完毕,state 为:', store.getState())
}

简单画个图:

Redux-2

如果要再加一个功能咋办?如法炮制:

// 先把原生的 dispatch 存起来
let next = store.dispatch

// 魔改 dispatch
store.dispatch = action => {
  //可以做点预处理
  //执行原生 dispatch
  next(action)
  //还可以再做点后处理
  console.log('dispatch 完毕,state 为:', store.getState())
}

// 这里存的是魔改 1 层 dispatch 了
let next2 = store.dispatch

// 在 1 层的基础上再魔改 dispatch
store.dispatch = action => {
  //预处理2
  //执行魔改 1 层 dispatch
  next2(action)
  //后处理2...
}

这样做就实现了多个中间件的串联,每次我们调用 store.dispatch 时,实际上是这样执行的:

Redux-3

Redux 中中间件的执行原理就是这样,但 Redux 中调整了中间件写法。我们的这个简陋版中间件系统要求开发者和用户这么使用:

// 第三方中间件开发者要这么开发:
function someMiddleware(store) {
  let next = store.dispatch

  //魔改 dispatch
  store.dispatch = action => {
    //预处理
    next(action)
    //后处理
  }
}

// redux 用户要这么使用中间件:
const store = createStore(reducer)

someMiddleware1(store)
someMiddleware2(store)
...

4.2 优化 monkeypatch

上文的写法有以下几个缺点:

缺点1:用户用起来不方便,添加中间件行为明显集成在 createStore 中更方便:

const store = createStore(reducer, applyMiddleware(someMiddleware1, someMiddleware2))

缺点2:中间件开发者权力过大,可以任意操纵 store。Redux 只允许中间件对 dispatch 功能进行扩展,需要对中间件进行可访问性限制。
缺点3:多中间件条件下,每个中间件中拿到的 store 只是环节中的中间产物,无法拿到最终 store。若要在中间件中派发新的 dispatch,我们期望
使用最终 storedispatch 进行派发,这样才能保证所有 dispatch 行为是一致的。

我们来看 Redux 是如何解决这三个问题的:

//为了使用 createStore(reducer, applyMiddleware(middlewares...)) 用法
//修改 createStore

function createStore(reducer, enhancer) {

  // 如果有中间件,在 enhancer(即 applyMiddleware(middlewares...)) 中进行 store 初始化与应用中间件工作
  if(typeof enhancer !== 'undefined') {
    return enhancer(createStore)(reducer)
  }

  let state
  let listeners = []

  const getState = () => {...}
  const subscribe = (listener) => {...}
  const dispatch = (action) => {...}

  dispatch({})

  return {
    getState,
    dispatch,
    subscribe
  }
}

这样,执行 createStore(reducer, applyMiddleware(middlewares...)) 即为执行 applyMiddleware(middlewares...)(createStore)(reducer)

function applyMiddleware(...middlewares) {
  return (createStore) => (reducer) => {
    // 使用 createStore 初始化 store
    const store = createStore(reducer)
    let dispatch = store.dispatch

    // 准备一个变量,仅包含 getState 和 dispatch 两个 api
    const storeWithLimitedAPI = {
      getState: store.getState(),
      dispatch: (...args) => dispatch(...args)
      // 这里为什么不直接 dispatch: dispatch 而是写了个闭包?
      // 因为 storeWithLimitedAPI 是要传递给中间件的,我们期望这里的 dispatch 指的是最终 store 的 dispatch
      // 在下文的代码中,dispatch 会被替换,如果使用 dispatch: dispatch,这里的 dispatch 不会更新,依旧指向原生 dispatch
      // 只有写做闭包,执行时的 dispatch 才会指向正确的、最终 store 的 dispatch
    }

    // 使用中间件将 store.dispatch 重写 这里可以直接用 forEach 土方法写
    middlewares.reverse().forEach(middleware => {
      // 这里为 middleware 传进了两个参数。storeWithLimitedAPI 可以让中间件拿到最终 store 的 getState 和 dispatch 方法
      // dispatch 参数则是中间环节的 dispatch,即上一个 middleware 处理过后的 dispatch
      // 例:对于魔改第三层的中间件,这里的 dispatch 即为魔改第二层处理后的 dispatch
      dispatch = middleware(storeWithLimitedAPI)(dispatch)
    })

    // 在 redux 源码中使用了 compose 的写法,compose 为语法糖,为了好看
    // let chain = middlewares.map(middleware => middleware(storeWithLimitedAPI))
    // dispatch = compose(...chain)(dispatch)

    // 向用户返回 store,可见应用了中间件后的 store 只有 dispatch 发生了变化,其他 API 都不变,保证安全
    return {
      ...store,
      dispatch
    }
  }
}

通过调整 createStore 的 API 修复了缺点1。控制参数传入,只给 middleware 传进 dispatch 和 storeWithLimitedAPI,限制了 middleware 的操作范围。storeWithLimitedAPI 中提供了最终 store 的 dispatch,使得从中间件内部派发新 disptatch 成为可能,这样缺点2和3也解决了。

applyMiddleware 中是这样调用 middleware 的:

dispatch = middleware(storeWithLimitedAPI)(dispatch)

这就要求中间件的形式是:一个函数,接收两个参数,返回新 dispatch。我们拿 redux-thunk 的源码来看:

function createThunkMiddleware(extraArgument) {

  // 对应  (storeWithLimitedAPI)   (dispatch)
  return ({ dispatch, getState }) => next => action => {

    // 在 redux-thunk 中间件中,如果发现 action 是一个函数
    // 则会执行该函数,并将最终 store 的 dispatch、getState 都作为参数传进去,任由该函数使用
    // 从而为 dispatch 扩展了处理函数的功能
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    // 如果发现 action 不是函数,那就原封不动的传递给下一个中间件,不做任何额外处理。
    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

中间件的执行原理可见下图。

Redux-4

4.3 compose

初看 redux 源码时,见到 compose 可能会觉得懵逼,实际上只是个语法糖。由于多中间件时 dispatch 会被反复替换,所以会写出这样的代码:

// 三个中间件, middlewareA, middlewareB, middlewareC

dispatch = middlewareC(middlewareB(middlewareA(dispatch)))

这样写不美观,中间件越多看起来越乱,所以引入 compose 函数用来整理:

// 用了 compose 可以这么写, 最终效果完全一样
dispatch = compose(middlewareA, middlewareB, middlewareC)(dispatch)

//compose 实现不难,只是使用了 reduce
function compose(...funcs) {

  if(funcs.length === 0) {
    return arg => arg
  }

  if(funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => {
    return function(...args) { //关键在这两行
      return a(b(...args))
    }
  })
}

5.参考资料

  1. React.js 小书
  2. Redux从设计到源码
  3. Async Actions
  4. Middleware
  5. redux-thunk/src/index.js
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant