1500行TypeScript代码在React中实现组件keep-alive
- 2019 年 10 月 5 日
- 筆記
现代框架的本质其实还是
Dom
操作,今天看到一句话特别喜欢,不要给自己设限,到最后,大多数的技术本质是相同的。
例如后端用到的Kafka , redis , sql事务写入 ,Nginx
负载均衡算法,diff
算法,GRPC
,Pb 协议
的序列化和反序列化,锁等等,都可以在前端被类似的大量复用逻辑,即便js
和Node.js
都是单线程的
认真看完本文与源码,你会收获不少东西

框架谁优谁劣,就像
Web
技术的开发效率与Native
开发的用户体验一样谁也不好一言而论谁高谁低,不过可以确定的是,web
技术已经越来越接近Native
端体验了
作者是一位跨平台桌面端开发的前端工程师,由于是即时通讯应用,项目性能要求很高。于是苦寻名医,为了达到想要的性能,最终选定了非常冷门的几种优化方案拼凑在一起
过程虽然非常曲折,但是市面上能用的方案都用到了,尝试过了,但是后面发现,极致的优化,并不是1+1=2
,要考虑业务的场景,因为一旦优化方案多了,他们之间的技术出发点,考虑的点可能会冲突。
这也是前端需要架构师的原因,开发重型应用如果前端有了一位架构师,那么会少走很多弯路。
后端也是如此
Vue.js
中的keep-alive
使用:
在Vue.js
中,尤大大是这样定义的:

keep-alive
主要用于保留组件状态或避免重新渲染
基础使用:
<keep-alive> <component :is="view"></component> </keep-alive>
大概思路:

这里本来做了
gif
图,不知道为什存后切换也是非常平滑,没有任何的闪屏

特别提示: 这里每个组件,下面还有一个1000行的列表哦~ 切换也是秒级
图看完了,开始梳理源码
第一步,初次渲染缓存
import {Provider , KeepAlive} from 'react-component-keepalive';
将需要缓存渲染的组件包裹,并且给一个name
属性即可
例如:
import Content from './Content.jsx' export default App extends React.PureComponent{ render(){ return( <div> <Provider> <KeepAlive name="Content"> <Content/> </KeepAlive> </Provider> </div> ) } }
这样这个组件你就可以在第二次需要渲染他的时候直接取缓存渲染了
下面是一组被缓存的一个组件,

仔细看上面的注释内容,再看当前body
中多出来的div

那么他们是不是对应上了呢? 会是怎样缓存渲染的呢?
到底怎么缓存的
找到库的源码入口:
import Provider from './components/Provider'; import KeepAlive from './components/KeepAlive'; import bindLifecycle from './utils/bindLifecycle'; import useKeepAliveEffect from './utils/useKeepAliveEffect'; export { Provider, KeepAlive, bindLifecycle, useKeepAliveEffect, };
最主要先看 Provider,KeepAlive
这两个组件:
缓存组件这个功能是通过 React.createPortal API
实现了这个效果。
react-component-keepalive
有两个主要的组件 <Provider>
和 <KeepAlive>
;<Provider>
负责保存组件的缓存,并在处理之前通过 React.createPortal API
将缓存的组件渲染在应用程序的外面。缓存的组件必须放在 <KeepAlive>
中,<KeepAlive>
会把在应用程序外面渲染的组件挂载到真正需要显示的位置。

这样很明了了,原来如此
开始源码:
Provider
组件生命周期
public componentDidMount() { //创建`body`的div标签 this.storeElement = createStoreElement(); this.forceUpdate(); }
createStoreElement
函数其实就是创建一个类似UUID
的附带注释内容的div
标签在body
中
import {prefix} from './createUniqueIdentification'; export default function createStoreElement(): HTMLElement { const keepAliveDOM = document.createElement('div'); keepAliveDOM.dataset.type = prefix; keepAliveDOM.style.display = 'none'; document.body.appendChild(keepAliveDOM); return keepAliveDOM; }
调用createStoreElement
的结果:

然后调用forceUpdate
强制更新一次组件
这个组件内部有大量变量锁:
export interface ICacheItem { children: React.ReactNode; //自元素节点 keepAlive: boolean; //是否缓存 lifecycle: LIFECYCLE; //枚举的生命周期名称 renderElement?: HTMLElement; //渲染的dom节点 activated?: boolean; // 已激活吗 ifStillActivate?: boolean; //是否一直保持激活 reactivate?: () => void; //重新激活的函数 } export interface ICache { [key: string]: ICacheItem; } export interface IKeepAliveProviderImpl { storeElement: HTMLElement; //刚才渲染在body中的div节点 cache: ICache; //缓存遵循接口 ICache 一个对象 key-value格式 keys: string[]; //缓存队列是一个数组,里面每一个key是字符串,一个标识 eventEmitter: any; //这是自己写的自定义事件触发模块 existed: boolean; //是否退出状态 providerIdentification: string; //提供的识别 setCache: (identification: string, value: ICacheItem) => void; 。//设置缓存 unactivate: (identification: string) => void; //设置不活跃状态 isExisted: () => boolean; //是否退出,会返回当前组件的Existed的值 }
上面看不懂 别急,看下面:

接着是Provider
组件真正渲染的内容代码:
<React.Fragment> {innerChildren} { keys.map(identification => { const currentCache = cache[identification]; const { keepAlive, children, lifecycle, } = currentCache; let cacheChildren = children; //中间省略若干细节判断 return ReactDOM.createPortal( ( cacheChildren ? ( <React.Fragment> <Comment>{identification}</Comment> {cacheChildren} <Comment onLoaded={() => this.startMountingDOM(identification)} >{identification}</Comment> </React.Fragment> ) : null ), storeElement, ); }) } </React.Fragment>
innerChildren
即是传入给Provider
的children
一开始我们看见的缓存组件内容显示的都是一个注释内容 那为什么可以渲染出东西来呢
Comment
组件是重点
Comment
组件
public render() { return <div />; }
初始返回是一个空的div
标签
但是看他的生命周期ComponentDidmount
public componentDidMount() { const node = ReactDOM.findDOMNode(this) as Element; const commentNode = this.createComment(); this.commentNode = commentNode; this.currentNode = node; this.parentNode = node.parentNode as Node; this.parentNode.replaceChild(commentNode, node); ReactDOM.unmountComponentAtNode(node); this.props.onLoaded(); }

这个逻辑到这里并没有完,我们需要进一步查看KeepAlive
组件源码
KeepAlive
源码:
组件componentDidMount
生命周期钩子:
public componentDidMount() { const { _container, } = this.props; const { notNeedActivate, identification, eventEmitter, keepAlive, } = _container; notNeedActivate(); const cb = () => { this.mount(); this.listen(); eventEmitter.off([identification, START_MOUNTING_DOM], cb); }; eventEmitter.on([identification, START_MOUNTING_DOM], cb); if (keepAlive) { this.componentDidActivate(); } }
其他逻辑先不管,重点看:
const cb = () => { this.mount(); this.listen(); eventEmitter.off([identification, START_MOUNTING_DOM], cb); }; eventEmitter.on([identification, START_MOUNTING_DOM], cb);
当接收到事件被触发后,调用`mout和listen`方法,然后取消监听这个事件
private mount() { const { _container: { cache, identification, storeElement, setLifecycle, }, } = this.props; this.setMounted(true); const {renderElement} = cache[identification]; setLifecycle(LIFECYCLE.UPDATING); changePositionByComment(identification, renderElement, storeElement); }
changePositionByComment
这个函数是整个调用的重点,下面会解析
private listen() { const { _container: { identification, eventEmitter, }, } = this.props; eventEmitter.on( [identification, COMMAND.CURRENT_UNMOUNT], this.bindUnmount = this.componentWillUnmount.bind(this), ); eventEmitter.on( [identification, COMMAND.CURRENT_UNACTIVATE], this.bindUnactivate = this.componentWillUnactivate.bind(this), ); }
listen
函数监听的自定义事件为了触发componentWillUnmount
和componentWillUnactivate
COMMAND.CURRENT_UNMOUNT
这些都是枚举而已
changePositionByComment
函数:
export default function changePositionByComment(identification: string, presentParentNode: Node, originalParentNode: Node) { if (!presentParentNode || !originalParentNode) { return; } const elementNodes = findElementsBetweenComments(originalParentNode, identification); const commentNode = findComment(presentParentNode, identification); if (!elementNodes.length || !commentNode) { return; } elementNodes.push(elementNodes[elementNodes.length - 1].nextSibling as Node); elementNodes.unshift(elementNodes[0].previousSibling as Node); // Deleting comment elements when using commet components will result in component uninstallation errors for (let i = elementNodes.length - 1; i >= 0; i--) { presentParentNode.insertBefore(elementNodes[i], commentNode); } originalParentNode.appendChild(commentNode); }
老规矩,上图解析源码:

很多人看起来云里雾里,其实最终的实质就是通过了Coment
组件的注释,来查找到对应的需要渲染真实节点再进行替换,而这些节点都是缓存在内存中,DOM
操作速度远比框架对比后渲染快。这里再次得到体现
这个库,无论是否路由组件都可以使用,虚拟列表+缓存KeepAlive组件的Demo体验地址
库原链接地址为了项目安全,我自己重建了仓库自己定制开发这个库
感谢原先作者的贡献 在我出现问题时候也第一时间给了我技术支持 谢谢!
新的库名叫react-component-keepalive
直接可以在npm
中找到
npm i react-component-keepalive
就可以正常使用了