100行JavaScript代码在React中优雅的实现简单组件keepAlive

24次阅读

共计 3299 个字符,预计需要花费 9 分钟才能阅读完成。

React是近些年出现比较优秀的前端框架,它的设计思想,源码非常棒。

什么是状态保存?

假设有下述场景:

移动端中,用户访问了一个列表页,上拉浏览列表页的过程中,随着滚动高度逐渐增加,数据也将采用触底分页加载的形式逐步增加,列表页浏览到某个位置,用户看到了感兴趣的项目,点击查看其详情,进入详情页,从详情页退回列表页时,需要停留在离开列表页时的浏览位置上
类似的数据或场景还有已填写但未提交的表单、管理系统中可切换和可关闭的功能标签等,这类数据随着用户交互逐渐变化或增长,这里理解为状态,在交互过程中,因为某些原因需要临时离开交互场景,则需要对状态进行保存

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

如何实现 React 中的状态保存

Vue 中,我们可以非常便捷地通过 <keep-alive> 标签实现状态的保存,该标签会缓存不活动的组件实例,而不是销毁它们
而在 React 中并没有这个功能,曾经有人在官方提过功能 issues ,但官方认为这个功能容易造成内存泄露,表示暂时不考虑支持,所以我们需要自己想办法了

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

手动保存状态,是比较常见的解决方式,可以配合 React 组件的 componentWillUnmount 生命周期通过 redux 之类的状态管理层对数据进行保存,通过 componentDidMount 周期进行数据恢复
在需要保存的状态较少时,这种方式可以比较快地实现我们所需功能,但在数据量大或者情况多变时,手动保存状态就会变成一件麻烦事了
作为程序员,当然是尽可能懒啦,为了不需要每次都关心如何对数据进行保存恢复,我们需要研究如何自动保存状态

最初的版本react-keep-alive

1500 行 TypeScript 代码在 React 中实现组件 keep-alive 我的这篇文章对源码进行了解析,但是这个库存在断层现象,虽然可以缓存最后一次状态渲染结果,但是后面数据变化无法再进行数据驱动。而且是借助 React.createPortal 借助实现,我跟下面这个库的作者都觉得这是多余的,其实只需要抽取 children 属性,再封装一次 HOC 高阶组件即可。

总体来说,react-keep-alive这个库比较重,实现原理也不难,就是笨重,断层,源码跳来跳去,真的理清楚了就好

react-activation优雅的实现

效果实现:

庖丁解牛,源码解析

最简单版本的 react 中 keep-alive 实现演示地址

使用方式:开箱即用

import React, {useState} from 'react'
import {render} from 'react-dom'
import KeepAlive, {AliveScope} from './KeepAlive'

...

function App() {const [show, setShow] = useState(true)
  return (
    <div>
      <button onClick={() => setShow(show => !show)}>Toggle</button>
      <p> 无 KeepAlive</p>
      {show && <Counter />}
      <p> 有 KeepAlive</p>
      {show && (
        <KeepAlive id="Test">
          <Counter />
        </KeepAlive>
      )}
    </div>
  )
}

....


render(
  <AliveScope>
    <App />
  </AliveScope>,
  document.getElementById('root')
)

注意:缓存的虚拟 DOM 元素会储存在 AliveScope 组件中,所以它不能被卸载

使用 AliveScope 配合 KeepAlive 即可达到缓存效果,类似react-keep-alive

首先我们看看 AliveScope 组件做了什么事情


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>
    )
  }
}


它的源码只有几十行,很简单,这里的 this.props.children 是虚拟 DOM,经过Babel 编译和 React 处理,最终会转化成真实 DOM 节点渲染

从零自己编写一个 mini-React 框架 如果你不是很懂,那么可以看我的这篇文章

逐步解析:

 {this.props.children}

是这个组件的所有子元素,必须要渲染

使用 ReactContext API进行传递 KEEP 方法给所有的子孙组件,每次这个方法被调用,都会造成 AliveScope 组件重新渲染,进而刷新子组件,并且返回一个真实的 DOM 节点,这个真实的 DOM 节点就可以被直接 DOM 操作。

这张思维导图,可以很清楚的表示,我们的缓存实现方式, 如果看不懂,慢慢往下看

KeepAlive组件的源码

import React, {Component, createContext} from 'react'

const {Provider, Consumer} = createContext()

const withScope = WrappedCompoennt => props => (<Consumer>{keep => <WrappedCompoennt {...props} keep={keep} />}</Consumer>
)



@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

withScope是一个高阶组件,将 KeepAlive 组件传入,返回一个新的组件,这里使用了装饰器,@withScope. 其实最终 export default 的是withScope(KeepAlive)

这里就是跟 react-keep-alive 的真正区别,withScope使用了 context api 捕获了传入的虚拟 DOM 节点,桥接了父组件以及 KeepAlive 组件的关联,一旦 children 属性改变,那么 withScope 被刷新,进而传入新的 children 属性给 KeepAlive 组件,导致数据驱动可以进行组件刷新

这又印证了那句话

在计算机的世界里,如果出现解决不了的问题,那就加一个中间层,如果还不行就加两个 – 来自不知名码农Peter

这里按照代码运行逻辑,完整的解析了它的简单缓存机制实现,思路整体比较清晰,加上代码自己断点调试难度应该比较低,个人觉得这个库的设计和思想,都是不错的,值得推广,作者也是比较乐意解答问题。大家有问题可以在 github 上提问。

另外 SegmentFault 前端交流群还有名额,有需要的可以加我微信:CALASFxiaotan, 里面大量小姐姐哦

欢迎关注微信公众号:前端巅峰

觉得不错记得点个赞哦~ 以后会有更多的源码解析

正文完
 0