7

面试官叫我手写 Redux - 2

 3 years ago
source link: https://zhuanlan.zhihu.com/p/359128208
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 - 2

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

方:目前,我们需要整理一下我们的代码,把 appContext.js、connect.js 和 createNewState.js 的代码全整合到 redux.js 里来:

v2-807752409bcc5c20a8d3043c343b231a_720w.jpg

如此一来,App.js 就只用到了两个 API 了:

import { appContext, connect } from "./redux";

学生:接下来做什么呢?

方:优化一下 appContext,来看看它是被如何使用的:

v2-c9b973d627e7450f22ba88c4d099d623_720w.jpg

可以看到,appContext 主要是用来初始化 appState 和 setAppState 的。目前我们使用 App 的 state 当做全局 state,其实是有很大的性能问题的

学生:什么问题?

方:我给你演示一下这个 bug,首先,我在三个儿子组件的第一行都添加一个 log

v2-abbbfa84aeb6f221253b4cc6138b0e42_720w.jpg

然后,你注意看控制台里的 log:

v2-360f92dcb51cf85dce7ddf37cf1eabf8_b.jpg

发现什么问题没有?

学生:有什么问题吗?

方:你没发现有多余的 render 吗?我在「二儿子」里的 UserModifier 调用 dispatch,理论上应该只需要执行「大儿子」函数和「二儿子」函数即可(因为这两个组件的子组件都读取了 user),为什么要执行「幺儿子」函数呢?这属于多余的 render 对吧?

学生:哦,对哦……

方:你知道这意味着什么吗?这意味着我们每次对 state 的微小改动,都会渲染这个 App

学生:因为你调用了 App 的 setAppState 函数对吗?

方:是的,只要调用这个函数(并传给它了一个新的 appState),就必定会触发 App 重新执行,也就必定会导致 App 的所有子组件重新执行,除非给子组件加缓存:

显然,大部分人不会给每个后代组件都加缓存。

学生:那怎么办?

方:问题出在我们「使用 App 的 state 当做全局 state」,可以考虑将 appState 独立出来,于是我在 redux.js 里声明了一个 store,并将其导出:

然后将 store 传给 context 的 value:

如此一来,就消除了对 App 的 state 的依赖了。

学生:那么所有用到 appState 的地方,就都要改成 store.state 了

方:没错,由于我们上节课让 User 和 UserModifier 组件都从 connect 那里获取 state 和 dispatch,所以我们只需要修改 connect 就行了:

学生:这样就可以了吗?

方:完全不行,运行页面你会发现「无法修改 user.name」。因为 store.state 的变化并不能触发任何一个组件更新,React 里想要触发组件更新一般需要调用 setState。

学生:这不有回到原点了吗?我们一开始就是用的 setState

方:我们一开始用的是 App 的 setState,这次我们可以使用 Wrapper 的 setState 呀

学生:可是 Wrapper 怎么知道 store.state 变化了呢?

方:Wrapper 可以订阅 store.state 的变化,我们只需要给 store 添加一个 subscribe 接口即可:

然后,在 setState 运行完毕后,遍历调用所有 fn 即可:

学生:你就用这么几行代码就实现了一个发布订阅?

方:没错,只不过实现得比较简陋而已。接下来我们需要在 Wrapper 里面监听 store.state 的变化:

学生:state 变化了就重新渲染 Wrapper?

方:对,但问题是如何重新渲染 Wrapper?函数组件并没有提供 forceUpdate 接口哦。这里我们要使用一个小技巧:

26 行的 update({}) 就相当于 forceUpdate()

学生:你利用了 {} !== {} 这一特性!

方:聪明,用 symbol 也能达到相同的效果。接下来看看我们的优化成果:

v2-60e0afd7625cfcfe15431a793cbbcafd_b.jpg

看到没,修改 user.name 时,三个儿子组件全都没有重新执行,只有 User 和 UserModifier 重新执行了。

学生:那也就是说,每次修改 state,只有被 connect 过的组件会重新渲染?

学生:诶?那我又有一个问题了,如果一个组件被 connect 过,但是它没有使用 user.name 也会在 user.name 被更新时重新渲染吗?

方:目前是

学生:能优化吗?

方:这还不简单吗?每个 Wrapper 对比一下自己依赖的值是否改变了(用 ref 来存储上一次的值),没变就不要 update({}) 呗……

学生:那每个 Wrapper 怎么知道自己依赖的值是 state.user 还是 state.group 呢?

让 connect 接受一个 selector 即可,如果依赖 user,就给 connect 传一个 state => ({user: state.user});如果依赖 group,就传 state => ({group: state.group})

学生:原来 redux 的 connect 的第一个参数 mapStateToProps是干这个的啊

方:嗯,目前的代码先给你看看 damp-wave-1ufwd - CodeSandbox ,具体怎么实现 selector 下节课再讲

学生:好的!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK