需求场景
为了优化用户的体验,可能会遇到这样的需求:返回列表的时候,需要保持状态和滚动位置;或是页面内切换组件(比如切换 Tab )的时候,需要保持状态。
如果使用 Vue,就可以用 <keep-alive> 组件来让其包含的组件保留状态,实现组件缓存。但是在 React 中并没有这样的功能,而且在这个 issues 中也可以看到,官方认为 <keep-alive> 容易造成内存的泄露,因此不准备引入这样的 API,但也许未来会提供更好的缓存方式,所以目前还需通过其他方法实现这类需求。
方法对比
- redux 或 localStorage/sessionStorage
通过这几种方式把状态存储起来,再次回到页面时再把状态取出来使用,实现缓存的效果。
这种方式在数据量较少时可以很好地实现缓存,但在状态多或情况多变的时候,就会让改动较大、控制起来也比较复杂,不是一个通用的方法。 - 第三方库
- react-keep-alive
可以实现缓存的效果,但是会造成数据驱动失效。
虽然可以缓存最后一次状态渲染结果,但是后面数据变化无法再进行数据驱动。 - react-activation
可以实现缓存的效果,较推荐。 - umi-plugin-keep-alive
是基于 react-activation 的 umi 插件。
- react-keep-alive
react-activation 的使用
其实使用方法很简单,用 <KeepAlive> 包裹需要进行缓存的组件,并在一个不会卸载的父组件内包裹上 <AliveScope> 即可。
react-activation 分别为类组件和函数组件提供了生命周期 componentDidActivate、componentWillUnactivate 或 hooks useActivate、useUnactivate,来对应恢复缓存和进行缓存两种状态,以及是否保存滚动位置、手动控制缓存等等功能。
通过 作者的最简实现 可以看到组件缓存的效果和使用的示例,以及 react-activation 的最简实现方式,可以分析一下它的实现思路。
react-activation 的源码原理
最简实现:
import React, { Component, createContext } from 'react'
const { Provider, Consumer } = createContext()
const withScope = WrappedComponent => props => (
<Consumer>{keep => <WrappedComponent {...props} keep={keep} />}</Consumer>
)
export class AliveScope extends Component {
nodes = {}
state = {}
keep = (id, children) =>
new Promise(resolve =>
this.setState(
{
[id]: { id, children }
},
() => resolve(this.nodes[id])
)
)
render() {
return (
<Provider value={this.keep}>
{this.props.children}
{Object.values(this.state).map(({ id, children }) => (
<div
key={id}
ref={node => {
this.nodes[id] = node
}}
>
{children}
</div>
))}
</Provider>
)
}
}
@withScope
class KeepAlive extends Component {
constructor(props) {
super(props)
this.init(props)
}
init = async ({ id, children, keep }) => {
const realContent = await keep(id, children)
this.placeholder.appendChild(realContent)
}
render() {
return (
<div
ref={node => {
this.placeholder = node
}}
/>
)
}
}
export default KeepAlive
实现的过程大致是,由不会被卸载的 AliveScope 组件通过上下文,把一个 keep 方法传递出去。
然后一个高阶组件获取到 keep 方法,并把 children 属性传入 KeepAlive 组件(这也是 react-activation 实现了数据驱动,而 react-keep-alive 数据驱动失效,两个库的主要区别原因 )。
在 KeepAlive 组件中调用 keep 方法,把 children 属性缓存到 AliveScope 的 state 中。
在 state 更新后,把 ref (真实 DOM)返回给 KeepAlive 组件。KeepAlive 组件拿到真实 DOM 后,把它移动到自己组件内的某个占位中。
在 KeepAlive 组件卸载的以后,如果还需要重新加载,还可以从 AliveScope 组件中获取到缓存的虚拟 DOM 信息。
这个其实是伪造组件的思路,把 children 包裹起来并且传递出去,在缓存组件内被渲染,当前组件正常地更新卸载。
当前组件卸载的时候,children 也被卸载了,但是它的虚拟 DOM 已经被缓存在了缓存组件中。
这个组件重新被加载的时候,把缓存直接渲染后移入当前组件,就恢复了组件卸载前状态。
不过这个库也因为破坏了原来的渲染层级,遇到了一些已修复和还未修复的问题,还是希望官方可以支持实现这个功能吧。
参考
React 中的状态自动保存(KeepAlive)—— react-activation 作者
在React中实现和Vue一样舒适的keep-alive