最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 翻译:0147-use-mutable-source.md - 掘金

    正文概述 掘金(少君同学)   2021-09-12   525
    • 开始日期: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 概述了 useMutableSourceuseSubscription 的区别。新 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 {
      // ...
    }
    

    实现

    根或者模块作用域改变

    可变源需要追踪模块级别的两类信息:

    1. 执行中版本号(每个源、每次渲染被追踪)
    2. 待更新过期时间(每个根、每个源被追踪)

    版本号

    追踪源的版本在组件还没有订阅就从源读值的时候让我们避免割裂。

    这种情况下,版本需要被检查以保证满足两者之一:

    1. 在当前渲染期间,这是第一次挂载的组件从源读值,或者
    2. 自上次读取版本号就没变过。(变了的版本号表明依赖的数据发生了变化,可能导致割裂。)

    就像 Context,这个钩子应该支持多个并发渲染器(例如 ReactDOM 和 ReactART,React Native 和 React Fabric)。为了支持这个,我们将需追踪两个进行中的版本。(一个为“主”渲染器一个为“次”渲染器)。

    当渲染器开始或者结束(或者放弃)一次批量任务时,这个值应该被重置。这个信息可被保存在:

    • 在主或次字段的每个可变源自身。
      • Con: 需要一个独立的数组/列表追踪可变源的显著变化。
    • 在模块级别作为可变源和等待的主次版本号映射的 Map
      • Con: 需要至少一个额外的 Map 结构。

    待更新过期时间

    追踪每个源的待更新能使新挂载的组件读值、避免和在上次渲染中从相同源读值的组件的潜在冲突。

    在更新中,如果当前渲染器的过期时间 源中保存的过期时间,从源中读新值是安全的。否则缓存快照值应该被临时使用1

    当根被提交,全部的 提交时间的待过期时间对于根可以被丢弃。

    这个信息可被保存在:

    • 每个 Fiber 根上,作为可变源和待更新过期时间的 Map
    • 每个可变源上,作为 Fiber 根和待更新过期时间的 Map
      • Con: 需要一个独立的数据结构来映射根和显著变化的可变源(由于显著变化在提交时会在每个根上被清除)。

    1 当配置在渲染器间变化时缓存快照值不能被重用。更多如下……

    关于为什么同时需要待过期和版本的说明

    尽管对更新有用,待更新过期时间对新挂载组件上的避免避免割裂并不足够,即使源已经被其他组件所使用。由于每个组件可能订阅 store 的不同部分,下面的场景是可能的:

    1. 一些组件挂载和订阅源 A。
    2. React 开始一次新的渲染。
    3. 一个新组件(非之前挂载的)从源 A 读值,然后 React yield。
    4. 源 A 变化不会通知任何当前已经订阅的组件,但是会影响新组件(它们现在没有订阅)。
    5. 另一个新组件(非之前挂载的)从源 A 读值。这时,对于该源没有待更新任务,但是它已经改变并且从它读值可能会导致割裂。

    Hook 状态

    useMutableSource() 钩子的 memoizedState 将需要追踪下列值:

    • 用户提供的 getSnapshotsubscribe 函数。
    • 最后的(缓存的)快照值。
    • 可变源本身(为了检测是否有新源提供)。
    • (用户返回的)取消订阅的函数。

    需处理的场景

    在订阅前从源读值

    当组件在尚未订阅的时候从可变源读值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 文档和博客。

    未解问题

    • 是否存在一些普遍的/重要的该提案将不能支持的可变源类型?

    起源地下载网 » 翻译:0147-use-mutable-source.md - 掘金

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元