学生:方,面试官叫我手写 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 就两个区别,
另外,这里创建新对象的代码非常繁琐,如果是我来优化的话可能会引入 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!
学生:确实!那怎么办?
方: 有两个办法:
我先讲第二个方法。第一个方法改动有点大,明天再讲。
学生:第二个办法是把 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)
这个怎么实现?
方:这些都是小技巧而已,明天再讲吧,今天就到这里先。目前的代码我发给你,你可以运行看看有什么问题没有。
学生:好的,我先自己尝试一下。
本文配套视频课程已出:redux课程已出
饥人谷一直致力于培养有灵魂的编程者,打造专业有爱的国内前端技术圈子。如造梦师一般帮助近千名不甘寂寞的追梦人把编程梦变为现实,他们以饥人谷为起点,足迹遍布包括facebook、阿里巴巴、百度、网易、京东、今日头条、大众美团、饿了么、ofo在内的国内外大小企业。 了解培训课程:加微信 xiedaimala03,官网:https://jirengu.com