写在开头
正式开始
- 什么是状态保存?
- 假设有下述场景:
- 移动端中,用户访问了一个列表页,上拉浏览列表页的过程当中,随着滚动高度逐渐增长,数据也将采用触底分页加载的形式逐步增长,列表页浏览到某个位置,用户看到了感兴趣的项目,点击查看其详情,进入详情页,从详情页退回列表页时,须要停留在离开列表页时的浏览位置上
- 相似的数据或场景还有已填写但未提交的表单、管理系统中可切换和可关闭的功能标签等,这类数据随着用户交互逐渐变化或增加,这里理解为状态,在交互过程当中,由于某些缘由须要临时离开交互场景,则须要对状态进行保存
- 在 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节点渲染
- 逐步解析:
{this.props.children}
- 是这个组件的全部子元素,必需要渲染
- 使用React的Context 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上提问。