100行JavaScript代码在React中优雅实现简单组件keep-Alive
React是近几年兴起的优秀前端框架。它的设计思路和源代码都非常好。什么是状态保存?假设如下场景:在移动端,用户访问列表页面。在向上滚动浏览列表页的过程中,随着滚动高度的逐渐增加,数据也会以逐页底部加载的形式逐渐增加。列表页浏览到某个位置,用户看到感兴趣的项目,点击查看其详情,进入详情页。从详情页返回列表页时,离开列表页时需要停留在浏览位置。未提交的表单、管理系统中可切换可关闭的功能标签等。这类数据随着用户交互逐渐变化或增长,在这里理解为状态。在交互过程中,由于某些原因需要暂时离开交互场景,需要在React中保存状态。我们通常使用路由来管理不同的页面。切换页面时,路由会卸载不匹配的页面组件。因此,在上面的列表页示例中,当用户从详情页选择返回列表页时,会返回到列表页的顶部,因为列表页组件被路由卸载后重新构建,并且状态丢失了。如何在Vue中保存React中的状态,我们可以通过标签轻松实现状态保存,这个标签会缓存不活跃的组件实例,而不是销毁它们。React中没有这样的功能。官方曾经有人提到过功能问题,但是官方认为这个功能容易造成内存泄漏,表示暂时不考虑支持,所以我们需要想出一个通用的解决方案:手动保存state手动保存状态是一种比较常见的方案,可以配合React组件的componentWillUnmount生命周期通过redux等状态管理层保存数据,通过componentDidMount周期性的恢复数据当需要的状态很少的时候saved,这种方式可以比较快的实现我们需要的功能,但是当数据量大或者情况发生变化的时候,手动保存状态就会变得很麻烦作为程序员,当然是越懒越好。为了不用担心每次如何保存和恢复数据,我们需要研究如何自动保存状态。原版react-keep-alive1500行TypeScript代码实现组件keep-inReactalive我的文章分析了源码,但是这个库有缺陷。虽然可以缓存上次的状态渲染结果,但是后续的数据变化不能数据驱动。此外,它是在React.createPortal的帮助下实现的。我和以下图书馆的作者都认为这是多余的。其实我们只需要将children属性抽取出来,重新封装HOC高层组件即可。总的来说,react-keep-alive这个库比较重,实现原理不难,但是体积大,故障多,源码跳来跳去。这真的很容易理解。react-activation优雅的实现效果实现:Paodingjieniu,源码分析react中keep-alive实现的最简单版本演示地址使用:开箱即用importReact,{useState}from'react'import{render}from'react-dom'importKeepAlive,{AliveScope}from'./KeepAlive'...functionApp(){const[show,setShow]=useState(true)return(setShow(show=>!show)}>ToggleNoKeepAlive
{show&&}WithKeepAlive
{show&&()} )}....render(,document.getElementById('root'))注意:缓存的虚拟DOM元素会保存在AliveScope组件中,所以不能卸载,使用AliveScope配合KeepAlive实现缓存效果,类似react-keep-alive。首先,让我们看看AliveScope组件的作用。导出类AliveScope扩展组件{节点={}state={}keep=(id,children)=>newPromise(resolve=>this.setState({[id]:{id,children}},()=>resolve(this.nodes[id])))render(){return({this.props.children}{Object.values(this.state).map(({id,children})=>({this.nodes[id]=node}}>{children}
))})}}它的源码只有几十行,非常简单,这里this.props.children是一个虚拟DOM。经过Babel编译和React处理后,最终会转化为真正的DOM节点渲染。从头开始编写一个迷你React框架。不太理解的可以看我的文章一步步分析:{this.props.children}是这个组件的所有子元素,必须使用React的ContextAPI渲染,通过KEEP方法到所有后代组件。每次调用该方法都会导致AliveScope组件重新渲染,然后刷新子组件,并返回一个真实的DOM节点,这个真实的DOM节点可以直接被DOM操作这个思维导图可以很清楚的表达我们的缓存实现方式。看不懂的就慢慢往下看KeepAlive组件的源码(
{keep=>})@withScopeclassKeepAliveextendsComponent{constructor(props){super(props)this.init(props)}init=async({id,children,keep})=>{constrealContent=awaitkeep(id,children)this.placeholder.appendChild(realContent)}render(){return(
{this.placeholder=node}}/>)}}exportdefaultKeepAlivewithScope是一个高阶组件,传入KeepAlive组件,返回一个新组件,这里装饰implementer,@withScope。其实最终导出默认是withScope(KeepAlive)。这是与react-keep-alive的真正区别。withScope使用contextapi捕获传入的虚拟DOM节点,并桥接父组件和KeepAlive组件之间的关系,一旦children属性发生变化,那么withScope就会被刷新,然后将新的children属性传递给KeepAlive组件,这样datadriver就可以刷新组件,印证了一句话,在计算机的世界里,如果有解决不了的问题,那就加一个中间层,如果还是不行,就加两个——来自未知coderPeterheretorun按照代码的逻辑,完整分析其简单的缓存机制实现,整体思路比较清晰,代码断点调试难度应该比较低。个人觉得这个库的设计和思路还是不错的,值得推广。作者也比较愿意答疑解惑,有问题可以在github上提问。另外,SegmentFault前端交流群还有名额。有需要的可以加我微信:CALASFxiaotan,里面有很多小姐姐。欢迎关注微信公众号:前端巅峰的感觉不错,记得点个赞~以后会有更多的源码分析