面试官叫我手写 Redux - 1

1,567 阅读6分钟

学生:方,面试官叫我手写 Redux

方:电话里让你口述 Redux?

学生:是线下面试,给我电脑问我能不能写。我写不出来,老尴尬了

方:这岗位工资给多少钱啊?敢出这么硬的题

学生:20k~30k

方:那还算合理

学生:唉,看来我是值不了这么多钱了

方:别灰心,其实很简单,我跟你讲一遍你就会写了

学生:真的?你讲讲呗

方:你如果想要理解一个库,最好就是先自己写一个类似的库,然后把自己的代码跟它的代码做对比。

学生:我自己写也不会啊……

方:你应该先问自己,Redux 要解决的问题是什么

学生:我看看 Redux 官网(10秒钟后)官网说 Redux is a predictable state container for JavaScript apps.

方:没错,Redux 说自己是一个可预测的状态容器。

学生:不懂……

方:没关系,我们先从「状态」开始,我会从零开始构建一个库,并逐渐告诉你如何优化:

App 有三个儿子,以及一个 AppState,现在想把 appState 分享给所有「大儿子」里的 User 组件,我们很容易想到使用 Context,对吧

想要在大儿子里的 User 组件里显示 appState.user.name

学生:嗯,需要先用 Provider 把整个 App 包起来就行

方:加三行代码即可,就像这样:

然后到大儿子的子组件 User 里,使用 Consumer:

学生:是不是还可以用 useContext ?

方:嗯,useContext 会简洁一点:

学生:我喜欢 useContext

方:现在,我们似乎就已经解决了 appState 共享问题了,对吧?那怎么更新 appState 呢?

学生:我看你把 setAppState 也放到 contextValue 里了,用它就行吧?

方:没错,我们来试一试,我在二儿子里新增了一个 UserModifier 组件,里面有个 input 可以修改 appState.user.name

学生:你这个代码应该不能修改 user.name

方:为什么呢?

学生:因为你传给 setAppState 的还是原来那个对象,虽然你改了里面的 name,但是对象的引用没变

方:确实,你说的是对的,那我就创建个新对象吧:

运行成功:

学生:等下,56 ~ 58 的代码有问题啊:

const onChange = (e) => {
  const { appState, setAppState } = contextValue;
  appState.user.name = e.target.value;
  setAppState({ ...appState });
};

你其实还是修改了之前的 appState,然后为了骗过 setAppState,故意使用了 {... appState} 来创建新对象

方:这样不行吗?你看代码还是能用的。

学生:我也说不上有什么问题,但是这样是不推荐的。

方:那我们得想个办法阻止这种写法,你看这样行不行:我们把创建新 state 的过程封装成一个函数 createNewState(),使用者只用传入参数就能得到新 state:

学生:对哦,这样开发者就算想捣乱也必须去改 createNewState 的源码才行,新手应该没有这么无聊。

方:为了防止新人手贱,我们把 createNewState 单独放到一个文件里:

学生:这个函数我看着有点眼熟啊,这不就是 reducer 么?

方:没错,我们的 createNewState 跟 reducer 就两个区别,

  1. 我没有接受 initialState
  2. 我没有把 actionType 和 actionData 合成 action 对象

另外,这里创建新对象的代码非常繁琐,如果是我来优化的话可能会引入 Immer.js,但由于 Redux 没有做优化,所以我们这里就先不动它。

我们先把 action 对象搞出来:

于是,所有调用 createNewState 的地方也要改成 {type, payload} 的形式:

学生:原来 reducer 和 action 的来历是这样的啊,是为了统一规范创建新 state 的流程啊

方:对

学生:那 dispatch 呢?

方:别急,我们把上面的代码简化一下:

学生:你只是把两行代码合并成一行了

方:你看第 59 行的 setAppState(createNewState(appState, .. 这段代码

学生:这代码怎么了

方:这段代码将来会被重复无数遍

学生:什么意思?

方:现在修改 user.name 要写

setAppState(createNewState(appState, {
  type: "updateUser",
  payload: {
    name: e.target.value
  }
}));

下一次你修改 user.age 是不是要写

setAppState(createNewState(appState, {
  type: "updateUser",
  payload: {
    age: e.target.value
  }
}));

再下一次你修改 group.name 是不是还要写

setAppState(createNewState(appState, {
  type: "updateGroup",
  payload: {
    name: e.target.value
  }
}));

学生:是诶……

方:那为什么不把 setAppState(createNewState(appState, ... 封装一下呢,就像这样:

学生:对哦,这样简化清爽多了。原来 dispatch 是为了简化和统一 setState 的流程啊

方:对啥对啊,这段代码有个明显的问题

学生:什么问题?

方:updateState 无法读取 context,所以它也不能访问 appState 和 setAppState!

学生:确实!那怎么办?

方: 有两个办法:

  • 一是不要把 appState 和 setAppState 放到 Context 里;
  • 二是把 updateState 放在组件里,因为组件里可以读取 context;

我先讲第二个方法。第一个方法改动有点大,明天再讲。

学生:第二个办法是把 updateState 放在组件里,但怎么放

方:语言很难描述,还是直接看代码吧。首先,我们要准备一个空组件,专门用来把 appState 和 setAppState 传给 updateState():

然后,这个 Wrapper 组件会把 updateState 传给 UserModifier,并渲染 UserModifier:

于是,UserModifier 组件就能从 props 里拿到「能访问 Context 的 updateState」了:

学生:那我应该使用 Wrapper 组件而不是 UserModifier 组件咯?

方:嗯,要把二儿子里的 UserModifier 组件改成 Wrapper 组件。

学生:让我想想,你为了让 updateState 能访问 Context,故意造了个空组件 Wrapper,然后让 Wrapper 渲染 UserModifier

方:没错

学生:原来还能这样。但有个问题,如此一来,我岂不是要给每个组件都套一个 Wrapper 才能拿到 「能访问 Context 的 updateState」?

方:没错

学生:你肯定有什么办法消除重复吧?

方:当然,我们可以写一个 createWrapper 函数:

然后直接把 UserModifier 改写成 createWrapepr(原来的 UserModifier) 的形式:

这样一来,UserModifier 就是原来的 Wrapper 了,直接使用 UserModifier 组件即可:

最后,这个 createWrapper 可以单独提取到另一个文件里。

学生: 这个 createWrapper 好像 connect 函数啊

方:没错,它就是 connect,我们来重构一下,把它改名为 connect:

学生:这个 updateState 是不是也可以改名为 dispatch

方:可以改:

学生:啊,越来越像 Redux 了。dispatch 这里是不是还能接受 state

方:当然,只需要在 connect 注入 dispatch 的时候,把 appState 也注入即可

这样一来,User 组件也不再需要自己从 Conext 里拿 user 了,直接 connect 一下就能注入 state 了:

学生:connect 确实方便,这就是传说中的高阶组件吗?

方:是,这些术语其实没什么复杂的,你从 0 开始理解就会觉得很简单

学生:那,我看 Redux 提供的 connect 是接受两次参数:

connect(mapState, mapDispatch)(Counter)

这个怎么实现?

方:这些都是小技巧而已,明天再讲吧,今天就到这里先。目前的代码我发给你,你可以运行看看有什么问题没有。

学生:好的,我先自己尝试一下。

后续 juejin.cn/post/694268…