V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
FrankFang128
V2EX  ›  职场话题

面试官叫我手写 Redux(对话教程)

  •  
  •   FrankFang128 · 251 天前 · 2559 次点击
    这是一个创建于 251 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原文链接: https://juejin.cn/post/6941973271210360845/

    学生:方,面试官叫我手写 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)
    

    这个怎么实现?

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

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

    待续……

    第 1 条附言  ·  250 天前
    第 2 条附言  ·  249 天前
    8 条回复    2021-03-22 14:59:54 +08:00
    jatai
        1
    jatai   251 天前 via Android   ❤️ 8
    面试官: 你手写一个 redux 吧
    我: redux 那么垃圾又麻烦,项目里我都是用 rxjs 来实现状态管理。你是不是不会 rxjs?
    Lxxyx
        2
    Lxxyx   251 天前
    面试官: 你手写一个 redux 吧
    我:不写
    FrankFang128
        3
    FrankFang128   251 天前
    @jatai 那你手写一个 rxjs 怎么样
    23333
    renmu123
        4
    renmu123   251 天前 via Android
    我不会,你能手写一个给我开开眼界不
    MrTreasure
        5
    MrTreasure   251 天前
    redux 掌握思路基本还是能手写的。新建一个状态建议使用 immerjs 。写越大的项目,越觉得 redux 增加心智负担。看看前人写的 redux 那坨代码根本维护不动
    FrankFang128
        6
    FrankFang128   251 天前
    @MrTreasure 我平时也不用 redux,模板代码太多
    hantsy
        8
    hantsy   250 天前
    虽然 Angular 也有集成 Redux 的项目,还专门概念上 Clone 的项目,感觉完全没有必须。使用 Actions 那一套,是不错,可以让代码更清楚。
    关于   ·   帮助文档   ·   API   ·   FAQ   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   2056 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 04:58 · PVG 12:58 · LAX 20:58 · JFK 23:58
    ♥ Do have faith in what you're doing.