【React】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;

你可能感兴趣的:(React)