【React】react-keep-alive实现原理
## 前言- 上次看见了peter谭的分享,终于完全搞懂了react-keep-alive。以前写的那个版本只能临时凑合用,解决不了根本问题。- 这个实现的思路很值得借鉴,并不是一个常规思路。
·
前言
- 上次看见了peter谭的分享,终于完全搞懂了react-keep-alive。以前写的那个版本只能临时凑合用,解决不了根本问题。
- 这个实现的思路很值得借鉴,并不是一个常规思路。
普通状态示例
- 有人可能不懂啥是keepalive,用下面示例举例:
- 使用cra创建个项目,使用counter进行制作。
import React, { useState } from "react";
import "./App.css";
function Counter() {
const [state, setState] = useState(0);
return (
<div>
{state}
<button
onClick={() => {
setState((v) => v + 1);
}}
>
++++
</button>
</div>
);
}
function App() {
const [show, setShow] = useState(true);
return (
<div className="App">
<button onClick={() => setShow(!show)}>切换</button>
<div>
无keepalive
{show && <Counter></Counter>}
</div>
<div>
无keepalive
{show && <Counter></Counter>}
</div>
</div>
);
}
export default App;
- 很显然,目前啥都没有,如果点击counter,它自己的状态变了,再点击app的show切换,一切又变成0。
- 这里就需要解决show切换时,有keepAlive的组件它不是0。
- 以前文章keepalive思路可以做,但废性能。
- 我根据peter谭的分享自己实现发现实现不出来。后来专门去研究作者和peter谭分享的简易示例发现其实里面不是那么简单。
原理
- 这个原理有点hack的意思,并不是peter谭文章上面写的那么简单。
- 子组件传给父组件需要渲染的东西,父组件进行渲染获取dom传给子组件,子组件需要搞个dom,然后把父组件dom插进去,这样这个dom就有子组件的渲染状态并且使用父组件的生命周期。
- 这里有2层概念,一个是fiber树上的显示,一个是真实dom的展示。
- 由于fiber树不强检测是否跟真实dom匹配(否则就不会出现key渲染的错误了一次key错误导致的错误渲染),利用这个特性,可以控制组件的渲染状态。
- keepalive组件可以拿到要渲染的虚拟dom,将其转移至alivescope组件,alivescope就是前面说的父组件,keepalive就是前面说的子组件。
- 父组件拿到子组件传来的虚拟dom进行渲染,这样,在其fiber树上就会表示这个组件已经被其渲染了。
- 关键点来了,渲染了后,父组件就可以获取这个组件dom,再把dom传给子组件,子组件获取dom后,插入自己已渲染的dom里。
- 这样就会导致,父组件渲染了dom,并且有这个dom的fiber,享受父组件的生命周期,但是渲染却是按照子组件渲染的逻辑走(就跟key错误误删一个道理),当子组件卸载时,fiber会commit掉子组件的dom,当然子组件fiber没有记录父组件有个dom跑子组件下了,结果这个dom就一起跟着被干掉了。
- 实际fiber树上仍然有这个dom,因为这个dom在父组件。
- 当子组件重新显示时,子组件dom加载完毕后,会调用父组件方法,重新获取父组件目前已经渲染的dom,有人可能奇怪,这个dom不是已经被干掉了?实际dom会缓存在fiber里,前面删掉的那个只是替代品。所以这里可能会产生个bug,就是如果alivescope组件的刷新频次与子组件调用不同会导致多个父组件上面dom显示,就是重影。比如作者的github issue里有人提出了重影bug,但是没人知道咋回事github issue
- 这样子组件又把父组件传来的dom给渲染出来了,而且是未卸载的状态。
代码
- 作者的代码里用了js装饰器,这玩意不推荐使用,因为js装饰器和ts装饰器不是一个东西,两者冲突,ts装饰器未来也不太可能支持js装饰器,所以写东西尽量别用装饰器写。另外ts社区说js装饰器已经大变动一次了,所以这种实验性语法最好别写,哪天又变了代码跑不起来不知道咋回事。
- 我将作者代码改造了下,使用函数组件写,只有2个组件,更便于阅读,无实验性语法。
示例组件:
import React, { useState } from "react";
import { render } from "react-dom";
import KeepAlive, { AliveScope } from "./keep";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
count: {count}
<button onClick={() => setCount((count) => count + 1)}>add</button>
</div>
);
}
function App() {
const [show, setShow] = useState(true);
return (
<AliveScope>
<div>
<button onClick={() => setShow((show) => !show)}>Toggle</button>
<p>无 KeepAlive</p>
{show && <Counter />}
<p>有 KeepAlive</p>
{show && (
<KeepAlive id="Test">
<Counter />
</KeepAlive>
)}
</div>
</AliveScope>
);
}
render(<App />, document.getElementById("root"));
keep.js组件
import React, {
createContext,
useState,
useEffect,
useRef,
useContext,
useMemo,
} from "react";
const Context = createContext();
export function AliveScope(props) {
const [state, setState] = useState({});
const ref = useMemo(() => {
return {};
}, []);
const keep = useMemo(() => {
return (id, children) =>
new Promise((resolve) => {
setState({
[id]: { id, children },
});
setTimeout(() => {
//需要等待setState渲染完拿到实例返回给子组件。
resolve(ref[id]);
});
});
}, [ref]);
return (
<Context.Provider value={keep}>
{props.children}
{Object.values(state).map(({ id, children }) => (
<div
key={id}
ref={(node) => {
ref[id] = node;
}}
>
{children}
</div>
))}
</Context.Provider>
);
}
function KeepAlive(props) {
const keep = useContext(Context);
useEffect(() => {
const init = async ({ id, children }) => {
const realContent = await keep(id, children);
if (ref.current) {
ref.current.appendChild(realContent);
}
};
init(props);
}, [props, keep]);
const ref = useRef(null);
return <div ref={ref} />;
}
export default KeepAlive;
更多推荐
所有评论(0)