react 中的 keep-alive

需求场景

为了优化用户的体验,可能会遇到这样的需求:返回列表的时候,需要保持状态和滚动位置;或是页面内切换组件(比如切换 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-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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值