- 开始日期:2020-02-13
- RFC PR:18000
- React Issue:N/A
useMutableSource
useMutableSource
使 React 组件在 并发模式 (Concurrent Mode) 下具备安全高效读取可变外部源的能力。API 会检测渲染时的修改避免割裂,并会在源被修改后自动更新。
基本示例
这个 hook 旨在支持各种各样的可变源。下面是一些例子。
浏览器 API
useMutableSource
也能用于读取非传统源,例如,共用的 Location 对象,同时它能被订阅和拥有“版本”:
// 可以在模块作用域创建,就像 context:
const locationSource = createMutableSource(
window,
// 尽管没有典型的“版本”,但 href 属性是稳定的,
// 并且在无论 Location 哪部分变化时都改变,
// 所以把它用作版本是安全的。
() => window.location.href
);
// 由于这个方法不需要访问 props,
// 它能在声明在模块作用域里在组件间共用。
const getSnapshot = window => window.location.pathname;
// 这个方法能订阅根级别变化事件,
// 或者更具体快照事件。
//
//
// 由于这个方法不需要访问 props,
// 它能在声明在模块作用域里在组件间共用。
const subscribe = (window, callback) => {
window.addEventListener("popstate", callback);
return () => window.removeEventListener("popstate", callback);
};
function Example() {
const pathName = useMutableSource(locationSource, getSnapshot, subscribe);
// ...
}
使用 props 的选择器
有时 state 值是用组件 props
衍生的。这种情况下,useCallback
应该用于保证快照和订阅函数稳定。
// 可以在模块作用域创建,就像 context:
const userDataSource = createMutableSource(userData, () => userData.version);
// 这个方法能订阅根级别变化事件,
// 或者更具体快照事件。
// 这种情况下,由于 Example 仅读取 "friends" 值,
// 我们仅需要订阅那个值的变化
// (例如,"friends" 事件)
//
// 由于这个方法不需要访问 props,
// 它能在声明在模块作用域里在组件间共用。
const subscribe = (userData, callback) => {
userData.addEventListener("friends", callback);
return () => userData.removeEventListener("friends", callback);
};
function Example({ onlyShowFamily }) {
// 由于快照依赖 props,它必须内联创建。
// useCallback() 记住了该函数,
// 这使 useMutableSource() 知道何时安全的重用快照值。
const getSnapshot = useCallback(
userData =>
userData.friends
.filter(
friend => !onlyShowFamily || friend.relationshipType === "family"
)
.map(friend => friend.id),
[onlyShowFamily]
);
const friendIDs = useMutableSource(userDataSource, getSnapshot, subscribe);
// ...
}
Redux store
Redux 用户看似从不直接使用 useMutableSource
。他们使用 Redux 提供的 hook,Redux 内部使用 useMutableSource
。
模拟 Redux 实现
// 在某处,Redux store 需要被一个可变源对象包裹...
const mutableSource = createMutableSource(
reduxStore,
// 因为 state 是不可变得,所以它能被用作“版本”。
() => reduxStore.getState()
);
// 它可通过 Context API 被共用...
const MutableSourceContext = createContext(mutableSource);
// 由于这个方法不需要访问 props,
// 它能在声明在模块作用域里在 hook 间共用。
const subscribe = (store, callback) => store.subscribe(callback);
// 最简的 Redux 如何使用可变源 hook 的示例:
function useSelector(selector) {
const mutableSource = useContext(MutableSourceContext);
const getSnapshot = useCallback(store => selector(store.getState()), [
selector
]);
return useMutableSource(mutableSource, getSnapshot, subscribe);
}
用户组件代码示例
import { useSelector } from "react-redux";
function Example() {
// 用户提供的 selector 应该用 useCallback 保持不变。
// 这能避免每次更新时不必要的重新订阅。
// selector 如果需要也能使用例如 props 值。
const memoizedSelector = useCallback(state => state.users, []);
// Redux hook 会连接用户代码和 useMutableSource。
const users = useSelector(memoizedSelector);
// ...
}
Observables
Observables 没有固有的版本号所以它们和这个 API 不兼容。给 Observable 添加一个衍生的版本号是可能的,如下所示,但是 在渲染中这样做可能不安全,除非小心潜在的内存泄露。
function createBehaviorSubjectWithVersion(behaviorSubject) {
let version = 0;
const subscription = behaviorSubject.subscribe(() => {
version++;
});
return new Proxy(behaviorSubject, {
get: function(object, prop, receiver) {
if (prop === "version") {
return version;
} else if (prop === "destroy") {
return () => subscription.unsubscribe();
} else {
return object[prop];
}
}
});
}
动机
现在这个 API 最好的“替代品”是 Context API 和 useSubcription hook。
Context API
Context API 一般不适于整个树中大量组件共用的源,由于 context 变化导致非常频繁的更新(例如,请看 Redux v6 性能挑战)。(当前有改进提案:RFC 118 和 RFC 119。)
useSubcription
这个 Gist 概述了 useMutableSource
和 useSubscription
的区别。新 API 主要的优点是:
- 在渲染期间不会发生割裂(设置在订阅初始化之前)。
- 订阅可以是“局部的”所以部分可变源的更新仅影响相关的组件(不是所有的组件都读源的值)。这意味着通常情况下,这个钩子性能更好。
详细设计
useMutableSource
和 useSubscription 类似。
- 都需要一个用回调缓存的 "config" 对象一边从外部“源”读取值。
- 都需要订阅和取消订阅源的方法。
但是也有不同点:
useMutableSource
需要源作为显式参数。(React 使用这个值避免“割裂”和保证所有组件从一个特定源读值、用相同版本的数据渲染。)useMutableSource
需要从源读的值是不可变得快照。这使值能在高优渲染中重用,允许更耗性能的重绘能在需要的时候被延迟。
公开 API
type MutableSource<Source> = {|
/*…*/
|};
function createMutableSource<Source>(
source: Source,
getVersion: () => $NonMaybeType<mixed>
): MutableSource<Source> {
// ...
}
function useMutableSource<Source, Snapshot>(
source: MutableSource<Source>,
getSnapshot: (source: Source) => Snapshot,
subscribe: (source: Source, callback: () => void) => () => void
): Snapshot {
// ...
}
实现
根或者模块作用域改变
可变源需要追踪模块级别的两类信息:
- 执行中版本号(每个源、每次渲染被追踪)
- 待更新过期时间(每个根、每个源被追踪)
版本号
追踪源的版本在组件还没有订阅就从源读值的时候让我们避免割裂。
这种情况下,版本需要被检查以保证满足两者之一:
- 在当前渲染期间,这是第一次挂载的组件从源读值,或者
- 自上次读取版本号就没变过。(变了的版本号表明依赖的数据发生了变化,可能导致割裂。)
就像 Context,这个钩子应该支持多个并发渲染器(例如 ReactDOM 和 ReactART,React Native 和 React Fabric)。为了支持这个,我们将需追踪两个进行中的版本。(一个为“主”渲染器一个为“次”渲染器)。
当渲染器开始或者结束(或者放弃)一次批量任务时,这个值应该被重置。这个信息可被保存在:
- 在主或次字段的每个可变源自身。
- Con: 需要一个独立的数组/列表追踪可变源的显著变化。
- 在模块级别作为可变源和等待的主次版本号映射的
Map
- Con: 需要至少一个额外的
Map
结构。
- Con: 需要至少一个额外的
待更新过期时间
追踪每个源的待更新能使新挂载的组件读值、避免和在上次渲染中从相同源读值的组件的潜在冲突。
在更新中,如果当前渲染器的过期时间 ≤ 源中保存的过期时间,从源中读新值是安全的。否则缓存快照值应该被临时使用1。
当根被提交,全部的 ≤ 提交时间的待过期时间对于根可以被丢弃。
这个信息可被保存在:
- 每个 Fiber 根上,作为可变源和待更新过期时间的
Map
。 - 每个可变源上,作为 Fiber 根和待更新过期时间的
Map
。- Con: 需要一个独立的数据结构来映射根和显著变化的可变源(由于显著变化在提交时会在每个根上被清除)。
1 当配置在渲染器间变化时缓存快照值不能被重用。更多如下……
关于为什么同时需要待过期和版本的说明
尽管对更新有用,待更新过期时间对新挂载组件上的避免避免割裂并不足够,即使源已经被其他组件所使用。由于每个组件可能订阅 store 的不同部分,下面的场景是可能的:
- 一些组件挂载和订阅源 A。
- React 开始一次新的渲染。
- 一个新组件(非之前挂载的)从源 A 读值,然后 React yield。
- 源 A 变化不会通知任何当前已经订阅的组件,但是会影响新组件(它们现在没有订阅)。
- 另一个新组件(非之前挂载的)从源 A 读值。这时,对于该源没有待更新任务,但是它已经改变并且从它读值可能会导致割裂。
Hook 状态
useMutableSource()
钩子的 memoizedState 将需要追踪下列值:
- 用户提供的
getSnapshot
和subscribe
函数。 - 最后的(缓存的)快照值。
- 可变源本身(为了检测是否有新源提供)。
- (用户返回的)取消订阅的函数。
需处理的场景
在订阅前从源读值
当组件在尚未订阅的时候从可变源读值1,React 首先检测版本号去看在当前渲染中是否有其他值从这个源被读取。
- 如果存在记录过的版本号(即这不是第一次读)那它和源的当前版本是否匹配?
- ✓ 如果两个版本匹配,则读取是 安全的。
- 将快照值保存在
memoizedState
上。
- 将快照值保存在
- ✗ 如果版本变了, 则读取是 不安全的。
- 抛出并重启渲染。
- ✓ 如果两个版本匹配,则读取是 安全的。
如果不存在版本号,那读取 可能是安全的。我们接下来需要检测关于源的待更新以确认它。
-
✓ 如果没有待更新则读取是 安全的。
- 将快照值保存在
memoizedState
上。 - 保存版本号以便这次渲染中的后续读取。
- 将快照值保存在
-
✓ 如果当前过期时间 ≤ 等待时间,则读取是 安全的。
- 将快照值保存在
memoizedState
上。 - 保存版本号以便这次渲染中的后续读取。
- 将快照值保存在
-
✗ 如果当前过期时间 > 等待时间,则读取是 不安全的。
- 抛出并重启渲染。
在订阅后从源读值
React 终将当源改变时重新渲染,但是也可能因为其他原因重新渲染。甚至在修改事件中,React 可能需要处理该修改之前渲染高优更新。在这种情况下,组件不从变化源读值很重要,因为可能造成割裂。
如果组件在未触发订阅时再次渲染(或者作为不包含订阅改变的高优渲染的一部分),它一般能复用缓存的快照。
一个不可能情况是当 getSnapshot
发生改变。即使在依赖源未改变时,依赖 props
(或者其他组件 State
)快照选择器可能改变。这种情况下,缓存的快照对复用不安全,useMutableSource
将不得不抛出并重启渲染。
设计约束
-
对于使用相同 MutableSource 值的组件,仅在根内实施割裂保证。根之间的割裂是可能的:
-
从 store 读取和返回的值必须是不可变的如同例如类的 state 或者 props 对象。
- 例如 ✓
getSnapshot: source => Array.from(source.friendIDs)
- 例如 ✓
getSnapshot: source => source.friendIDs
- 值不需要字面不可变但是应该只是被克隆,所以它们能和 store 断开联系并且对外部源不会被变化所修改。
- 例如 ✓
-
可变源必须有某种形式的稳定版本。
- 版本应该是全局的(对于源的全部,不是源的部分)。
- 例如 ✓
getVersion: () => source.version
- 例如 ✗
getVersion: () => source.user.version
- 例如 ✓
- 版本应该在无论源的任何部分被修改时改变。
- 版本不必须是数字或者甚至不必须是一个单独属性。
- 它可以是数据的序列号形式,只要它是稳定唯一的。(例如,读查询参数可能把整个 URL 串视为版本。)
- 它可以是状态自身,如果值是不可变的(例如 Redux store 是可变的,但是它的 state 是不可变的)。
- 版本应该是全局的(对于源的全部,不是源的部分)。
替代品
参考上面的“动机”章节。
采用策略
这个钩子主要用于像 Redux 的库(和转换器)。和那些库的维护者一起工作以集成该钩子。
我们如何教授这个
新的 reactjs.org 文档和博客。
未解问题
- 是否存在一些普遍的/重要的该提案将不能支持的可变源类型?
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!