通俗易懂知React实现keep-alive原理,看不懂不要赞
去年写完最后一篇文章之后换了一家新公司,这一年忙死了,应该是我毕业以来最忙的一年,最近有些许空闲,所以又打算把这一年积攒到的一些经验总结一下,所以又来写文章啦!
那么第一篇文章,就来谈一谈React中如何实现keep-alive
1 为啥要使用keep-alive
其实本人对在React
中使用keep-alive
非常的鄙夷,觉得这又是一个为了面试题而面试题的需求,因为这个东西如果处理的不好的话,特别容易存在内存泄露问题。但是最近真的遇到了这个使用场景,起因是我需要在别人微前端架构下开发移动端模块,这个架构的路由没有使用任何的第三方库,是他们自己开发的,在跳转页面的时候,就会出现如下情况
A组件跳转到B组件的时候,会把A组件中所有的状态销毁,导致再回到A组件的时候,此时A组件已经不是原来的A组件了
为了解决这个问题,多次与平台交涉,他们给出的解决方案非常的操蛋,路由跳转返回唯一不变的是history.state.key
,他们让我把所有的状态根据key
存到全局状态中,这。。。
那不是随便一个状态都必须存全局,我表示无法接收,并且次架构已经定下来,也不可能大改了,因此我只能求助于keep-alive
,便上网搜起来了React
下keep-alive
的实现方式
搜了一圈发现,网上大部分的文章要么直接贴keep-alive
的代码,没有任何讲解,要么全程装逼,然后结尾甩个自己写的库给你,定制性极差,非常不友好。于是我就想搞懂原理后,写一个通俗易懂的文章,只讲解keep-alive
的实现原理,没有任何的多余代码,任何人都可在其基础上自行定制
2 keep-alive原理
React
下keep-alive
的实现原理非常的简单,keep-alive
的核心原理图如下
keep-alive
的实现原理非常的取巧,核心原理是将要渲染的组件放到不会被卸载的地方渲染,然后把渲染好的dom
移动到keep-alive
组件要显示的位置下,伪代码如下
<div class="keep-alive-container">
<div>渲染好的dom</div>
</div>
<div class="keep-alive"></div>
<script>
// 永远不会被卸载的组件
const keepAliveContainer = document.querySelector('.keep-alive-container')
// 要持久化的组件在要渲染在页面上的位置
const keepAlive = document.querySelector('.keep-alive')
// 移动渲染好的dom
Array
.from(keepAliveContainer.childNodes)
.forEach(node => {
keepAlive.appendChild(node)
})
</script>
3 简单实现
以下是keep-alive
的最简单实现,性能极差,可优化项极多,但是简单明了的阐述了React
实现keep-alive
的基本代码
import {
useRef,
useState,
useEffect,
useContext,
createContext,
createElement
} from 'react'
import ReactDom from 'react-dom'
const KeepAliveContext = createContext()
/** 用于创建keepAlive组件的方法 */
function createKeepAliveElement(id, element) {
return (props) => {
const { elements, onPushKeepAlive } = useContext(KeepAliveContext)
const keepAliveRef = useRef()
// 每次props发生变化,重新渲染组件
useEffect(() => {
onPushKeepAlive(
id,
createElement(element, props),
)
}, [props])
// 监听组件状态变化
useEffect(() => {
// 如果nodes存在,移动到当前组件的根节点下
if (elements[id]?.nodes) {
const keepAliveDom = keepAliveRef.current
elements[id].nodes.forEach(node=>{
keepAliveDom.appendChild(node)
})
}
}, [elements])
return (
<div
ref={keepAliveRef}
style={{ display: 'contents' }}
/>
)
}
}
function KeepAliveProvider({ children }) {
/**
* 存储需要借鸡生蛋的组件,存储结构如下
* {
* [唯一标识]: {
* id: 唯一标识
* element: 存储jsx,
* nodes: 存储真实的dom
* }
* }
*/
const [elements, setElement] = useState({})
// 通过次方法添加keepAlive组件
const onPushKeepAlive = (id, element) => {
const newElements = { ...elements }
newElements[id] = { ...elements[id], id, element, }
setElement(newElements)
}
return (
<KeepAliveContext.Provider value={{ elements, onPushKeepAlive }}>
{children}
{/* 借鸡生蛋的地方 */}
{
Object.values(elements).map(({ id, element }) => (
<div
key={id}
style={{ display: 'none' }}
ref={(dom) => {
// 把渲染好的dom存到状态里
if (dom && dom.children.length) {
const newElements = { ...elements }
newElements[id].nodes = Array.from(dom.children)
setElement(newElements)
}
}}
>
{element}
</div>
))
}
</KeepAliveContext.Provider>
)
}
// 创建一个可持久化的form组件
const Form = createKeepAliveElement('form', () => {
const [value, setValue] = useState('')
return (
<input
value={value}
onChange={e => setValue(e.target.value)}
/>
)
})
// 创建一个可持久化的计数组件
const Count = createKeepAliveElement('count', () => {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
)
})
function App() {
const [showForm, setShowForm] = useState(true)
return (
<KeepAliveProvider>
<button onClick={() => setShowForm(!showForm)}>切换组件</button>
{/* 每次切换的时候都销毁前组件 */}
{showForm ? <Form /> : <Count />}
</KeepAliveProvider>
)
}
ReactDom.render(<App />, document.querySelector('#root'))
效果如下
转载自:https://juejin.cn/post/7242969515344789565