3

面试官叫我手写 Redux - 1

 3 years ago
source link: https://zhuanlan.zhihu.com/p/358551518
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

面试官叫我手写 Redux - 1

杭州饥人谷教育科技有限公司 CTO

后续:方应杭:面试官叫我手写 Redux - 2

学生:方,面试官叫我手写 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,但是对象的引用没变

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

运行成功:

v2-2c231981e69b6e1e8ddba991a83b715b_b.jpg

学生:等下,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)

这个怎么实现?

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK