use-shared-state 源码阅读
acfasj opened this issue · 6 comments
背景
看了知乎「如何优雅地处理使用 React Context 导致的不必要渲染问题?」 的一个回答, 对里面提到的
一句话,react context是给你注入服务的,不是让你注入数据的,如果要注入具有数据的服务那你就注入个类似EventEmitter的东西,例如rxjs observable
感觉有被打到, 所以想看看答主是如何实践这一点的 (其实是我看回答里的例子还get不到, 只好看代码了orz)
版本是 v1.2.0, commit 是 7711f3
用法
首先看 github 里的 README
1. Share state between components
这个还比较好理解, 直接看 CodeSandbox 的这个实例会直接一点
2. Notify event between components
这里我就看不太懂了, 大致能理解是观察者模式, 但是又分什么 ChangeNotifier
, ValueNotifier
是几个意思?
3. Create 'Controller' for controlling a component
没用过 flutter, 但是我可以理解为用 class 组织业务逻辑代码, 然后用 hooks 在组件里消费? 毕竟 hooks直接堆业务逻辑太难看了
看源码
好了, 带着上面的疑问开始看代码吧
首先拉代码下来, 果然只有 100 行的代码, 太开心了
先看index.ts:
import React, { useState, useRef, useCallback } from 'react';
import { ValueNotifier, useListen } from './listenable';
export * from './listenable';
export class SharedState<T> extends ValueNotifier<T> {}
export function useSharedState<T>(
}
导出了: SharedState
, useSharedState
, 以及 listenable
里的全部东西
useSharedState
先看input, 作者已经说得很清楚了:
- sharedState, 要共享的状态, 是 class
SharedState
是实例 - shouldUpdate, 类似于 shouldComponentUpdate
然后看return, 返回的是一个 tuple : [sharedState.getValue(), setSharedState]
-
sharedState.getValue(), 先不理, 反正就是共享的状态值
-
setSharedState
const setSharedState = useCallback( (v: React.SetStateAction<T>) => { sharedState.setValue(v); }, [sharedState], );
好家伙, 就是包装了一下
sharedState.setValue
-
同时我们注意到中间的一句
useListen(sharedState, listener);
比较显眼
一顿分析猛如虎之后, 我们可以得出两个关键:
SharedState
也就是ValueNotifier
到底是干嘛的 (因为代码里 SharedState 只是单纯地继承了 ValueNotifier 而已)useListen
是什么鬼
ValueNotifier
进到 listenable.ts
好吧, ValueNotifier
又是继承了 Listenable
, 那就先看看 Listenable
很简单, 就是一个类里面有个成员变量是数组
- hasListener, 数组长度是否大于0
- addListener, 数组push一个元素, 显然这里的元素就是listener
- removeListener, 数组删除一个元素
那就可以来看 ValueNotifier
的代码了:
首先可以看到, Listenable
的范型参数被限定为了 ValueListener
, 是一个函数, 这个函数的两个参数分别是current 和 prev;
成员变量只有一个构造函数传进来的value
, 这个value
的类型就是 ValueNotifier
的范型
接下来看方法:
- getValue, 返回 value
- setValue, 做了两个事情, 一个是改变this.value, 另外是调用this.notifyListeners
- notifyListeners, 接受value前后两个值, 通知所有订阅者, 也就是
Listenable
里数组里的所有函数
emmm, 看完了, 代码我都看得懂啊, 好像还是没get到什么, 接着看吧
useListen
这个一眼就看明白了啊, 就是 useEffect
里 addListener 和 cleanup 的时候 removeListener 而已啊
回过头看useSharedState
有了之前的阅读理解,同时结合一下 useSharedState
的实际用法 https://codesandbox.io/s/mystifying-cray-x2gcp?fontsize=14&hidenavigation=1&theme=dark&file=/src/App.tsx
可以发现, useSharedState 所做的事情, 其实大部分就是ValueNotifier
做的事情: 在setValue的时候通知 listener. 同时它加了一点料, 让我们setValue的时候可以更新 React 组件:
emmm, 好像通了! 🧐
解答用法疑问
Notify event between components
先看看 ChangeNotifier
是什么鬼
ok, 就是 ValueNotifier
的精简版
所以本质上REAME的那个示例, 就是个类似于 element.dispatchEvent(new Event('change'))
或者 eventEmitter.emit('change')
之类的东西而已, 没什么好纠结的 🤣
Create 'Controller' for controlling a component
这里我觉得README的例子好像不太好, 结合作者的回答, 应该是想把Controller的实例当成 Context.Provider 的value, 这样就可以跨组件共享Controller实例
然后我尝试把 redux 的 todos 的 example 改用这里的库实现了一下, 应该会让 Controller 的用法清晰一点 https://codesandbox.io/s/friendly-sid-f2ifw?file=/src/use-todos-service.js
好了, 写完这些我觉得我已经能get到答主讲的大部分内容了 👏
单元测试
既然都看了源码, 那么单测也不能放过啊
useSharedState
源文件位于 src/__tests__/index.test.tsx
这里用的测试框架是jest, 辅以官方的 ReactTestUtils
来操作dom
可以看到, 在beforeEach
和afterEach
做了container的清理和销毁, 因为每一个测试都会用 ReactDOM.render 把组件渲染在 container 上
这里作者测试了3个case, 一一看吧:
normal using
代码比较长就不贴了, 这里和demo其实很像:
-
有一个数字的sharedState
-
在三个组件里useSharedState: A组件永远更新; B组件用不更新; C组件只有前后的值加起来等于3的时候才更新
-
依次用js触发这三个按钮的点击时间, 断言渲染的值是否符合预期
值得注意的是, 触发button的点击事件, 都放在了 ReactTestUtils.act
中
参考 https://reactjs.org/docs/testing-recipes.html#act 这里面的定义, 可以理解为, 我们把触发事件、重新渲染组件等操作放在act
里的话, ReactTestUtils 能保证在后面代码的执行里, 这些操作的变更都已经应用在DOM上, 方便我们进行断言
on shouldUpdate change
主要是动态改变useState的第二个参数, 然后断言看它是否有用组件重新渲染
回想一下我们之前看 useSharedState的代码
shouldUpdateRef.current = shouldUpdate;
我们可是在每一次render里, 都把这个 shouldUpdate 存进 shouldUpdateRef.current 里的哈哈
updating in useEffect
这个用例不太明白为什么这么设计, 是要检测一下 setState
的引用有没有变, 会不会无限循环触发 useEffect 吗?
ChangeNotifier
源文件位于 src/__tests__/listenable.test.tsx
notify listeners
利用了一个全局变量flag(相对), 通过notifyListeners修改他的值来进行断言
ValueNotifier
源文件位于 src/__tests__/listenable.test.tsx
get & set value
断言 getValue 和 setValue 是否正常
listeners
利用了一个全局变量(相对), 通过断言它的值, 来判断 addEventListener 和 removeEventListener 是否正常
useListen
源文件位于 src/__tests__/listenable.test.tsx
normal using
这里的设计, 比较像完全没封装 useSharedState, 手动地调用他们来完成断言的测试
end
自己本身没有太多单测经验, 更多还是学习, 看看这些用例是怎么设计的.而且单元测试用例本身也是文档的一部分, 看清楚也会比较明白作者到底想用这个库做一些什么事情. 以后看别人代码应该也会花时间去看单测
因为我是站在有 Flutter 经验的角度去写的,所以可能会忽略了很多细节哈哈。controller 其实只是个存放 SharedState/Notifier 的 object 而已,方便把某个组件的所有共享状态打包起来,controller 里面甚至也可以包含其他 controller(例如用来控制本组件下的子组件)。另一个好处就像你说的,可以把 controller 的实例用 context 共享出去,这样就不用共享一堆 SharedState 了。
最后,感谢你的分析和对 README 的批评,我准备重新计划下怎么完善 README 和例子了🤔
(另外,你分析得出的结论都是正确的~
「是要检测一下 setState 的引用有没有变, 会不会无限循环触发 useEffect 吗?」
是的,我们要确保我们 hook 返回的 setState 引用不变,不然使用者在其他 hook 里去用 setState 可能回出问题。这也是为什么 setState 在返回前要 useCallback 包一遍的原因。
@nekocode 请教下作者
useSharedState里面的 onListen 为何要比较值,我看注释 If the state changed before the listener is added, notify the listener没想明白什么场景会发生这种情况?
@hardmanhong
具体可以看这个测试单元 https://github.com/nekocode/use-shared-state/blob/master/src/__tests__/index.test.tsx#L233
上图三个箭头处都用了 useEffect,这三个箭头的执行顺序刚好是一二三。当第二个箭头改变 shared state 后,第三个箭头才开始监听 shared state,所以此刻需要触发 Component2 重新渲染来获取最新的值。
@hardmanhong
具体可以看这个测试单元 https://github.com/nekocode/use-shared-state/blob/master/src/__tests__/index.test.tsx#L233上图三个箭头处都用了 useEffect,这三个箭头的执行顺序刚好是一二三。当第二个箭头改变 shared state 后,第三个箭头才开始监听 shared state,所以此刻需要触发 Component2 重新渲染来获取最新的值。
懂了,谢谢