Redux 时间旅行

668 阅读4分钟

关于 Redux 学习

很早就接触了 React,自然也就认识了 Redux,但因为一直做的是 Vue 相关,所以其实没有真正的理解。最近又重新在学习 React,在深刻理解 React 的基本思想和概念之后,我觉得自己已经掌握了 React 的基础。直到再一次遇到 Redux 这个硬骨头,还是遇到点困难,不得不说,Redux 的学习需要一点时间。

首先,不像 React 基本的 JSX、props、state 等概念,Redux 是状态管理,涉及的东西太多,概念太多,虽然每一个都不难,有的可能只有几行代码的实现,但是你可能很久之后才完全认识它们所有、并且认识到每一个都是不可或缺的。例如:

  • redux 本身的实现
  • redux 的设计原则
  • 深入理解 store、state、action、reducer、dispatch
  • 深入理解 immutable、函数式编程
  • 深入理解高阶函数
  • react-redux 的功能
  • middleware 的概念
  • applyMiddleware
  • compose
  • action creators
  • Selectors
  • Reselect
  • Normalizing
  • thunk 的概念
  • redux-thunk 的实现
  • Store enhancer
  • reducer enhancers
  • combineReducers
  • Higher Order Reducers
  • ……

Redux 如此丰富,这个列表可以一直列下去。别的不说,关于 redux 异步的实现就是一堆,thunk、saga 等等,哪一个不是要理解原理才能搞懂呢?redux 本身是不依赖任何东西的,其他所有的都是围绕 redux 创造出来的,为了增强 redux 的功能,为了高效、优雅的写法。

其次,redux 官网有点混乱,可能是因为东西多、想表达的东西多。我们总说“多看文档”,但是我觉得 Redux 的文档根本看不完。更不用提里面提到的链接有多少了,多么让人绝望!几个明显的问题:

  • Tutorials 里面 Essentials 和 Fundamentals 几乎是重复的
  • Redux ToolKit 的加重了 Redux 的复杂度
  • 没有一个完整的指引,学习的先后顺序

这些都会让 Redux 的学习变得困难。

时间旅行

但是在学习 Redux 的过程中,一直隐隐有个问题困扰着我。应该说是我最想搞懂的。这个问题,也是 Redux 最早要实现的功能,用以宣称所具有的巨大优势。那就是时间旅行。出现在 Dan 最初的演讲,Redux 的面世:Dan Abramov - Live React: Hot Reloading with Time Travel at react-europe 2015

在第一看的时候,我就想知道这个时间旅行到底是怎么做的。最近看文档,算是终于看到相关的实现了。

主要是文档 Implementing Undo History 介绍了 undo todo 的实现。

首先预备知识:reducer enhancer。

设计数据结构,要实现 undo、redo 的功能,要有如下的数据结构:

{
  past: Array<T>,
  present: T,
  future: Array<T>
}
  • past、future 表示存储的过去和未来的操作的所有数据(state)列表
  • present 表示现在的数据

实现 undo、redo 的 reducer:

const initialState = {
  past: [],
  present: null, // (?) How do we initialize the present?
  future: []
}

function undoable(state = initialState, action) {
  const { past, present, future } = state

  switch (action.type) {
    case 'UNDO':
      const previous = past[past.length - 1]
      const newPast = past.slice(0, past.length - 1)
      return {
        past: newPast,
        present: previous,
        future: [present, ...future]
      }
    case 'REDO':
      const next = future[0]
      const newFuture = future.slice(1)
      return {
        past: [...past, present],
        present: next,
        future: newFuture
      }
    default:
      // (?) How do we handle other actions?
      return state
  }
}

问题是如何在 undo、redo 时将数据存入 past 和 future?

这里就涉及 reducer enhancer,设计一个 undoable 函数,传入 todo reducer,返回一个 reducer,通过 HOF,既能对原来 todo 的 state 进行操作,也能对 undoable 的 state(包含 past、future,present 为 todo state)进行操作。

核心源码:

function undoable(reducer) {
  // Call the reducer with empty action to populate the initial state
  const initialState = {
    past: [],
    present: reducer(undefined, {}),
    future: []
  }

  // Return a reducer that handles undo and redo
  return function (state = initialState, action) {
    const { past, present, future } = state

    switch (action.type) {
      case 'UNDO':
        const previous = past[past.length - 1]
        const newPast = past.slice(0, past.length - 1)
        return {
          past: newPast,
          present: previous,
          future: [present, ...future]
        }
      case 'REDO':
        const next = future[0]
        const newFuture = future.slice(1)
        return {
          past: [...past, present],
          present: next,
          future: newFuture
        }
      default:
        // Delegate handling the action to the passed reducer
        const newPresent = reducer(present, action)
        if (present === newPresent) {
          return state
        }
        return {
          past: [...past, present],
          present: newPresent,
          future: []
        }
    }
  }
}

这个 undoable 有库实现,就是 redux-undo。

关于 Redux 学习总结

Redux 的实现与相关生态,依赖于 JavaScript 函数的灵活和强大,利用高阶函数,到处都是组合、抽象、封装,所以不仅是 middleware 高阶、dispatch 可以高阶(action 也要用函数返回一下、以及 thunk)、甚至 reducer 也可以高阶,只要一直高阶下去,就能实现任何功能。学习 React 和 Redux 的过程,感觉可以学到很多东西,感觉代码可以一步步变好变规范。对比学习 Vue 的过程是,学习它的 api、用法,然后学习它的实现原理(更好的使用它),但是你没办法 hack 到它的底层,因为它有自己的模式

参考