22

如何在 React 中实现 keep alive

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=Mzg5NDAyNjc2MQ%3D%3D&%3Bmid=2247485164&%3Bidx=1&%3Bsn=7bd0da385639ad8c0ff9ad3c289487d2
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

什么是 keep alive

在 Vue 中,我们可以使用 keep-alive 包裹一个动态组件,从而 「缓存」 不活跃的实例,而不是直接销毁他们:

<keep-alive>
  <component :is="view"></component>
</keep-alive>

这对于某些路由切换等场景非常好用,例如,如果我们需要实现一个列表页和详情页,但在用户从详情页返回列表的时候,我们不希望重新请求接口获取,也不希望重置列表的过滤、排序等条件,那这时就可以对列表页的组件用 keep-alive 包裹一下,这样,当路由切换时,Vue 会将这个组件“ 「失活」 ”并缓存起来,而不是直接卸载掉。

最简单的方案

而在 React 中,其实一直以来都没有官方的 keep alive 解决方案,大部分开发者可能都会直接使用 display: none 来将 DOM 隐藏:

<div style={shouldHide ? {display: 'none'} : {}}>
  <Foo/>
</div>

但这种方案其实只是在“ 「视觉上」 ”将元素隐藏起来了,并没有真正的移除,那有没有可能把 DOM 树真的移除掉,同时又让组件不被销毁呢?

Portals

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。

这是 React 官方文档 [1] 上对 Portal 特性的介绍,值得注意的是,这里只是说“父组件以外的 DOM 节点”,但没有要求这个 DOM 节点是真的在页面上,还是 「只是存在于内存中」 。因此,我们可以先通过 document.createElement 在内存中创建一个元素,然后再通过 React.createPoral 把 React 子节点渲染到这个元素上,这样就实现了“空渲染”。

const targetElement = document.createElement('div')
ReactDOM.createPortal(child, targetElement)

基于这种方案,我们可以进一步封装出一个 Conditional 组件,从而实现通用性的条件渲染逻辑:

export const Conditional = props => {
  const [targetElement] = useState(() => document.createElement('div'))
  const containerRef = useRef()
  useLayoutEffect(() => {
    if (props.active) {
      containerRef.current.appendChild(targetElement)
    } else {
      try {
        containerRef.current.removeChild(targetElement)
      } catch (e) {}
    }
  }, [props.active])
  return (
    <>
      <div ref={containerRef} />
      {ReactDOM.createPortal(props.children, targetElement)}
    </>
  )
}

首先,我们创建了一个 targetElement ,并且通过 createPortalchildren 渲染到 targetElement 。然后,我们会创建一个容器 div 元素,并且通过 containerRef 拿到它的引用。最后,当 activetrue 时,我们会把 targetElement 手动添加到 containerRef.current 的内部,反之,则会从其内部移除掉 targetElement 。实际使用的方式如下:

<Conditional active={!shouldHide}>
  <Foo/>
</Conditional>

细心的读者可能会发现,目前我们的 Conditional 组件还有一点小小的瑕疵:当组件初次渲染时,不论当前的 activetrue 还是 falseConditional 组件都会将 props.children 渲染。这对大型应用可能会带来非常明显的性能问题,所以,我们可以为其增加“ 「懒加载」 ”的特性:

export const Conditional = props => {
  const [targetElement] = useState(() => document.createElement('div'))
  const containerRef = useRef()

  // 增加一个 ref 记录组件是否“被激活过”
  const activatedRef = useRef(false)
  activatedRef.current = activatedRef.current || props.active

  useLayoutEffect(() => {
    if (props.active) {
      containerRef.current.appendChild(targetElement)
    } else {
      try {
        containerRef.current.removeChild(targetElement)
      } catch (e) {}
    }
  }, [props.active])
  return (
    <>
      <div ref={containerRef} />
      {activatedRef.current && ( // 如果“被激活过”,才渲染 children
        ReactDOM.createPortal(props.children, targetElement)
      )}
    </>
  )
}

一些遗憾

不得不承认的是,基于 Portal 方案的 Conditional 组件并不能包治百病,和 Vue 的 keep-alive 相比,也存在不少缺憾:

  1. 需要手动控制 active ,不能直接基于子组件销毁/创建的生命周期事件
  2. 缺少失活/激活的生命周期时间,子组件无法感知自己是不是被缓存起来了

  3. 依赖了 ReactDOM ,对 SSR 不够友好

Reference

[1]

官方文档: https://link.zhihu.com/?target=https%3A//zh-hans.reactjs.org/docs/portals.html

  • :heart: 往期推荐文章

2020年双非二本前端找工作感受总结(8-9月)

三年前端寒冬入大厂,收获蚂蚁、字节 offer 面经分享

谈谈 React 5种最流行的状态管理库

:heart: 交流讨论

欢迎关注公众号  秋风的笔记 ,主要记录日常中觉得有意思的工具以及分享开发实践,保持深度和专注度。回复"好友"可加微信,秋风的笔记常年陪伴你的左右。

Avi6Vj6.jpg!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK