React 中的状态自动保存(KeepAlive) - V2EX
CJY0208
V2EX    React

React 中的状态自动保存(KeepAlive)

  •  1
     
  •   CJY0208 Sep 20, 2019 3390 views
    This topic created in 2426 days ago, the information mentioned may be changed or developed.

    图文无关

    什么是状态保存?

    假设有下述场景:

    移动端中,用户访问了一个列表页,上拉浏览列表页的过程中,随着滚动高度逐渐增加,数据也将采用触底分页加载的形式逐步增加,列表页浏览到某个位置,用户看到了感兴趣的项目,点击查看其详情,进入详情页,从详情页退回列表页时,需要停留在离开列表页时的浏览位置上

    类似的数据或场景还有已填写但未提交的表单管理系统中可切换和可关闭的功能标签等,这类数据随着用户交互逐渐变化或增长,这里理解为状态,在交互过程中,因为某些原因需要临时离开交互场景,则需要对状态进行保存

    React 中,我们通常会使用路由去管理不同的页面,而在切换页面时,路由将会卸载掉未匹配的页面组件,所以上述列表页例子中,当用户从详情页退回列表页时,会回到列表页顶部,因为列表页组件被路由卸载后重建了,状态被丢失

    如何实现 React 中的状态保存

    Vue 中,我们可以非常便捷地通过 <keep-alive> 标签实现状态的保存,该标签会缓存不活动的组件实例,而不是销毁它们

    而在 React 中并没有这个功能,曾经有人在官方提过功能 issues ,但官方认为这个功能容易造成内存泄露,表示暂时不考虑支持,所以我们需要自己想办法了

    常见的解决方式:手动保存状态

    手动保存状态,是比较常见的解决方式,可以配合 React 组件的 componentWillUnmount 生命周期通过 redux 之类的状态管理层对数据进行保存,通过 componentDidMount 周期进行数据恢复

    在需要保存的状态较少时,这种方式可以比较快地实现我们所需功能,但在数据量大或者情况多变时,手动保存状态就会变成一件麻烦事了

    作为程序员,当然是尽可能懒啦,为了不需要每次都关心如何对数据进行保存恢复,我们需要研究如何自动保存状态

    通过路由实现自动状态保存(通常使用 react-router

    既然 React 中状态的丢失是由于路由切换时卸载了组件引起的,那可以尝试从路由机制上去入手,改变路由对组件的渲染行为

    我们有以下的方式去实现这个功能

    1. 重写 <Route> 组件,可参考 react-live-route

      重写可以实现我们想的功能,但成本也比较高,需要注意对原始 <Route> 功能的保存,以及多个 react-router 版本的兼容

    2. 重写路由库,可参考 react-keeper

      重写路由库成本是一般开发者无法承受的,且完全替换掉路由方案是一个风险较大的事情,需要较为慎重地考虑

    3. 基于 <Route> 组件现有行为做拓展,可参考 react-router-cache-route

      在阅读了 <Route> 的源码后发现,如果使用 component 或者 render 属性,都无法避免路由在不匹配时被卸载掉的命运

      但将 children 属性当作方法来使用,我们就有手动控制渲染的行为的可能,关键代码在此处 https://github.com/ReactTraining/react-router/blob/master/packages/react-router/modules/Route.js#L41-L72

       // 节选自 Route 组件中的 render 函数 if (typeof children === "function") { children = children(props); // children 是函数时,将对 children 进行调用得到真实的渲染结果 if (children === undefined) { ... children = null; } } return ( <RouterContext.Provider value={props}> {children && !isEmptyChildren(children) ? children // children 存在时,将使用 children 进行渲染 : props.match ? component ? React.createElement(component, props) : render ? render(props) : null // 使用 render 属性无法阻止组件的卸载 : null // 使用 component 属性无法阻止组件的卸载 } </RouterContext.Provider> ); 

      基于上述源码探究,我们可以对 <Route> 进行拓展,将 <Route> 的不匹配行为由卸载调整为隐藏,如下

      <Route exact path="/list"> {props => ( <div style={props.match ? null : { display: 'none' }}> <List {...props} /> </div> )} </Route> 

      上述是最简的调整方式,实际情况中也需要考虑隐藏状态下 matchnull 导致组件报错的问题,且由于不再是组件卸载,所以和 TransitionGroup 配合得不好,导致转场动画难以实现

      使用 react-router-cache-route,得到的效果大致如下图,

    上述探究了通过路由入手实现自动状态保存的可能,以及现有的实现,但终究不是真实的、纯粹的 KeepAlive 功能,接下来我们尝试探究真实 KeepAlive 功能的实现

    模拟真实的 <KeepAlive> 功能

    以下是期望的使用方式

    function App() { const [show, setShow] = useState(true) return ( <div> <button OnClick={() => setShow(show => !show)}>Toggle</button> {show && ( <KeepAlive> <Test /> </KeepAlive> )} </div> ) } 

    实现原理说起来较为简单,由于 React 会卸载掉处于固有组件层级内的组件,所以我们需要将 <KeepAlive> 中的组件,也就是其 children 属性抽取出来,渲染到一个不会被卸载的组件 <Keeper> 内,再使用 DOM 操作将 <Keeper> 内的真实内容移入对应 <KeepAlive>,就可以实现此功能

    以下是 react-activation 的实现效果

    在线示例

    下图为 <KeepAlive> 的实现原理说明

    实际实现过程中,遇到了许多问题,都是由于打破了原有 React 层级关系引起的,例如

    • 渲染延迟(react-activation 中已修复)
    • Context 上下文功能失效(react-activation 中已修复)
    • Error Boundaries 失效(react-activation 中已修复)
    • React.Suspense & React.lazy 失效(react-activation 中已修复)
    • React 合成事件冒泡失效
    • 其他未发现的功能

    但上述问题,大多数是可以通过桥接机制修复的,具体可以参考此处 issues

    相同的、更早的实现还有 react-keep-alive

    结语

    状态缓存是应用中十分常见的需求,在需要处理的数据量较少时,使用手动状态缓存就可以解决大多数问题,但当情况复杂时,还需要尝试将缓存功能单独拎出来解决,以便在业务开发过程中更好地进行关注点分离

    目前的实现都有各自的问题,但其探究过程十分有趣,最好的方式仍是官方的支持,但目前还不能报太大期望

    No Comments Yet
    About     Help     Advertise     Blog     API     FAQ     Solana     3512 Online   Highest 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 36ms UTC 12:01 PVG 20:01 LAX 05:01 JFK 08:01
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86