点击上方“前端自习课”关注,学习起来~

作者:陈俊宇

https://github.com/CJY0208

什么是状态保存?

假设有下述场景:

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

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

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

如何实现 React 中的状态保存

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

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

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

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

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

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

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

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

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

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

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

  2. 替换路由库为 react-keeper[5]

    完全替换掉路由方案是一个风险较大的事情,需要较为慎重地考虑3.

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

    在阅读了 <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>

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

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

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

模拟真实的 <KeepAlive> 功能

以下是期望的使用方式


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

以下是 react-activation[8] 的实现效果

在线示例[9]

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

  • 渲染延迟

  • Provider 上下文功能失效

  • Error Boundaries 失效

  • React.Suspense & React.lazy 失效

  • React 合成事件冒泡失效

  • 其他未发现的功能

但上述问题,大多数是可以通过桥接机制修复的

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

结语

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

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

References

[1] <keep-alive>https://cn.vuejs.org/v2/api/#keep-alive[2] issues: https://github.com/facebook/react/issues/12039[3] react-router: https://reacttraining.com/react-router/[4] react-live-route: https://github.com/fi3ework/react-live-route[5] react-keeper: https://github.com/vifird/react-keeper[6] react-router-cache-route: https://github.com/CJY0208/react-router-cache-route/blob/master/README_CN.md[7] react-router-cache-route: https://github.com/CJY0208/react-router-cache-route/blob/master/README_CN.md[8] react-activation: https://github.com/CJY0208/react-activation/blob/master/README_CN.md[9] 在线示例: https://codesandbox.io/s/affectionate-beaver-solkt[10] react-keep-alive: https://github.com/StructureBuilder/react-keep-alive


【React】377- 实现 React 中的状态自动保存的更多相关文章

  1. 利用express-session插件实现nodejs中登录状态的保存

    什么是session? session就是会话,客户端和服务器直接的会话.他的粒度比http链接更粗,一次会话包含了多次连接.即一个session是多次http连接的集合.从我的客户端连接到服务器到关 ...

  2. Android开发中Activity状态的保存与恢复

    当置于后台的Activity因内存紧张被系统自动回收的时候,再次启动它的话他会重新调用onCretae()从而丢失了之前置于后台前的状态. 这时候就要重写Activity的两个方法来保存和恢复状态,具 ...

  3. VS Code 中的LaTeX自动保存问题

    最近更新了VS Code,编辑LaTeX文档是出现了一个很恼人的问题:只要文档一改动,立马就Build,在更新之前是没有这个问题的. 解决方案如下:在设置里找到Auto Build: Run选项,将其 ...

  4. DynamicsCRM中的自动保存

    DynamicsCRM的自动保存功能 在DynamicsCRM2013开始,引入了自动保存功能. 保存一条记录 在新建一条记录的时候, 你必须在左上角手动点击保存按钮.如下图: 当保存完后,会发现,左 ...

  5. http协议无状态中的 "状态" 到底指的是什么?!

    引子: 最近在好好了解http,发现对介绍http的第一句话[http协议是无状态的,无连接的]就无法理解了:无状态的[状态]到底指的是什么?! 找了很多资料不仅没有发现有一针见血正面回答这个问题的, ...

  6. http协议无状态中的 "状态" 到底指的是什么?!(转载)

    转载自:https://www.cnblogs.com/bellkosmos/p/5237146.html   引子: 最近在好好了解http,发现对介绍http的第一句话[http协议是无状态的,无 ...

  7. 教你如何在React及Redux项目中进行服务端渲染

    服务端渲染(SSR: Server Side Rendering)在React项目中有着广泛的应用场景 基于React虚拟DOM的特性,在浏览器端和服务端我们可以实现同构(可以使用同一份代码来实现多端 ...

  8. React 新 Context API 在前端状态管理的实践

    本文转载至:今日头条技术博客 众所周知,React的单向数据流模式导致状态只能一级一级的由父组件传递到子组件,在大中型应用中较为繁琐不好管理,通常我们需要使用Redux来帮助我们进行管理,然而随着Re ...

  9. React Native之React速学教程(中)

    概述 本篇为<React Native之React速学教程>的第一篇.本篇将从React的特点.如何使用React.JSX语法.组件(Component)以及组件的属性,状态等方面进行讲解 ...

随机推荐

  1. suseoj 1212: 推箱子问题(bfs)

    1212: 推箱子问题 时间限制: 1 Sec  内存限制: 128 MB提交: 60  解决: 13[提交][状态][讨论版][命题人:liyuansong] 题目描述 码头仓库是划分为n×m个格子 ...

  2. RHEL7.2 SSH无密码登录非root用户

    1 修改三台虚拟机的/ect/hosts文件 [hadoop@hadoop01 ~]$ cat /etc/hosts 127.0.0.1 localhost localhost.localdomain ...

  3. 优秀的github项目学习

    优秀的github项目学习 后期会陆续添加遇到的优秀项目 https://github.com/chaijunkun

  4. 缓冲&缓存&对象池概念的理解

    一).缓冲 作用:缓解程序上下层之间的性能差异. 1).当上层组件的性能优于下层组件时加入缓冲机制可以减少上层组件对下 层组件的等待时间. 2).上层组件不需要等待下层组件接收全部数据,即可返回操作, ...

  5. go modules 学习

    go modules 学习 tags:golang 安装 只需要golang的版本是1.11及之后的,这个模块就内置好了 环境变量 (1) 配置GoLang的GOROOT (2) 可以不配置GoLan ...

  6. 读写分离很难吗?springboot结合aop简单就实现了

    目录 前言 环境部署 开始项目 注意 參考: 前言 入职新公司到现在也有一个月了,完成了手头的工作,前几天终于有时间研究下公司旧项目的代码.在研究代码的过程中,发现项目里用到了Spring Aop来实 ...

  7. Netty Pipeline与ChannelHandler那些事

    Pipeline和ChannelHandler是Netty处理流程的重要组成部分,ChannelHandler对应一个个业务处理器,Pipeline则是负责将各个ChannelHandler串起来的& ...

  8. PHP基于Redis实现轻量级延迟队列

    延迟队列,顾名思义它是一种带有延迟功能的消息队列. 那么,是在什么场景下我才需要这样的队列呢? 一.背景 先看看一下业务场景: 1.会员过期前3天发送召回通知 2.订单支付成功后,5分钟后检测下游环节 ...

  9. Sublime Text最好的中文教程

    原文链接:http://lucida.me/blog/sublime-text-complete-guide/ 摘要(Abstract) 本文系统全面的介绍了Sublime Text,旨在成为最优秀的 ...

  10. mysql中给查询出的结果集添加自增序号

    select (@i:=@i+1) i,emp.* from emp,(select @i:=0) it 按部门分组并按薪资总和从大到小排序求薪资总和第二高的部门名称和薪资总和:select c.en ...