vue3-KeepAlive源码思路

163 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

组件的渲染是个递归过程,比较消耗性能,Vue提供了内置组件KeepAlive,用于缓存它包囊的组件。 ... 表示这个位置省略了一部分代码

KeepAlive

const KeepAliveImpl = {
	name: `KeepAlive`,
	// Marker for special handling inside the renderer. We are not using a ===
	// check directly on KeepAlive in the renderer, because importing it directly
	// would prevent it from being tree-shaken.
	__isKeepAlive: true,
	props: {
		include: [String, RegExp, Array],
		exclude: [String, RegExp, Array],
		max: [String, Number]
	},
	setup(props, { slots }) {
		...
		const sharedContext = instance.ctx;
		const cache = new Map();
		const keys = new Set();
		...
		sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
			const instance = vnode.component;
			move(vnode, container, anchor, 0 /* ENTER */, parentSuspense);
			// in case props have changed
			patch(instance.vnode, vnode, container, anchor, instance, parentSuspense, isSVG, vnode.slotScopeIds, optimized);
			queuePostRenderEffect(() => { // ⑦
				instance.isDeactivated = false;
				if (instance.a) {
					invokeArrayFns(instance.a); // 调用 onActivated 钩子
				}
				const vnodeHook = vnode.props && vnode.props.onVnodeMounted;
				if (vnodeHook) {
					invokeVNodeHook(vnodeHook, instance.parent, vnode);
				}
			}, parentSuspense);
			...
		};
		...
		let pendingCacheKey = null; // ③
		const cacheSubtree = () => {
			// fix #1621, the pendingCacheKey could be 0
			if (pendingCacheKey != null) {
				cache.set(pendingCacheKey, getInnerChild(instance.subTree));
			}
		};
		...
		return () => {
			...
			const children = slots.default();
			const rawVNode = children[0]; // ①
			...
			const comp = vnode.type;
			const key = vnode.key == null ? comp : vnode.key; // ②
			const cachedVNode = cache.get(key);
			...
			return isSuspense(rawVNode.type) ? rawVNode : vnode;
		};
	}
}

组件运行 render 函数生成vnode(这个vnode不是dom,vnode.el才是dom),后面说的vnode都是这个vnode

function handleSetupResult(instance, setupResult, isSSR) {
	if (isFunction(setupResult)) {
		// setup returned an inline render function
		if (instance.type.__ssrInlineRender) {
			// when the function's name is `ssrRender` (compiled by SFC inline mode),
			// set it as ssrRender instead.
			instance.ssrRender = setupResult;
		}
		else {
			instance.render = setupResult;
		}
	}
		...
}

setupResult是setup函数执行的返回值。可以看出 如果setupResult是函数。它将赋值给render。
即KeepAlive组件的render方法为其setup方法的返回值。即每次更新或者挂载组件。就是执行:

return () => {
	...
	const children = slots.default();
	const rawVNode = children[0]; // ①
	...
	let vnode = getInnerChild(rawVNode);
	const comp = vnode.type;
	const key = vnode.key == null ? comp : vnode.key; // ②
	const cachedVNode = cache.get(key);
	pendingCacheKey = key;
	if (cachedVNode) { // ③
		// copy over mounted state
		vnode.el = cachedVNode.el; // 获取缓存的dom
		vnode.component = cachedVNode.component; // 获取缓存的组件实例
		if (vnode.transition) {
			// recursively update transition hooks on subTree
			setTransitionHooks(vnode, vnode.transition);
		}
		// avoid vnode being mounted as fresh
		vnode.shapeFlag |= 512 /* ShapeFlags.COMPONENT_KEPT_ALIVE */;
		// make this key the freshest
		keys.delete(key);
		keys.add(key);
	}
		...
	return isSuspense(rawVNode.type) ? rawVNode : vnode;
};

生成vnode。
KeepAlive组件本身并不渲染,它要渲染和缓存的是它的子节点即slots.default()[0]

缓存设计

方法:看 **cacheSubtree **方法,instance.subTree被缓存在cache中。组件每次挂载时会更新这个缓存。 instance.subTree即slots.default()[0] :
伪代码:(《Vue.js设计与实现》 引入)

function mountComponent() {
	const componentOptions = vnode.type // vnode的type保存组件选项。
	const { render, data } = componentOptions
	const state = reactive(data())
	const instance = {
		isMounted: false,
		subTree: null
	}
	//  因为更新时是对比vnode。所以要在vnode上保存instance
	vnode.comoponent = instance

	effect(() => {
		const subTree = render.call(state)
		if(!instance.isMounted) {
			patch(null, subTree)
			instance.isMounted = true
		} else {
			// 实现补丁更新。
			patch(instance.subTree, subTree)
		}
		instance.subTree = subTree
	})
}

缓存使用:

看代码③,render函数的执行结果可以从catch缓存中获取
然后在patch过程中,KeepAlive自带的activate方法将被调用。
执行move(vnode, container, anchor, 0 /* ENTER */, parentSuspense);方法,将正确的dom(即vnode.el)插入正确的位置。

细节:

由代码①可以看到,KeepAlive组件内只能包含单个插槽。
由代码②可知,当KeepAlive的插槽的组件不存在key属性时,用KeepAlive的插槽的组件实例的type对象作为key。

<keep-alive>
	<router-view></router-view>
</keep-alive>

所以如果路由缓存写法如上。路由切换时,key指向的一直是RouterView组件。 cachedVNode 获取会出错。最终也不会执行KeepAlive相关的缓存功能。

<router-view #default="{Component}">
	<keep-alive>
		<component :is="Component"/>
	</keep-alive>
</router-view>

所以要写成这种形式

onActivated的调用

onActivated

function onActivated(hook, target) {
	registerKeepAliveHook(hook, "a" /* LifecycleHooks.ACTIVATED */, target);
}
function onDeactivated(hook, target) {
	registerKeepAliveHook(hook, "da" /* LifecycleHooks.DEACTIVATED */, target);
}
function registerKeepAliveHook(hook, type, target = currentInstance) {
	// cache the deactivate branch check wrapper for injected hooks so the same
	// hook can be properly deduped by the scheduler. "__wdc" stands for "with
	// deactivation check".
	const wrappedHook = hook.__wdc ||
		(hook.__wdc = () => {
			// only fire the hook if the target instance is NOT in a deactivated branch.
			let current = target;
			while (current) {
				if (current.isDeactivated) {
					return;
				}
				current = current.parent;
			}
			return hook();
		});
	injectHook(type, wrappedHook, target);
	// In addition to registering it on the target instance, we walk up the parent
	// chain and register it on all ancestor instances that are keep-alive roots.
	// This avoids the need to walk the entire component tree when invoking these
	// hooks, and more importantly, avoids the need to track child components in
	// arrays.
	if (target) { // ⑥
		let current = target.parent;
		while (current && current.parent) {
			if (isKeepAlive(current.parent.vnode)) {
				injectToKeepAliveRoot(wrappedHook, type, target, current);
			}
			current = current.parent;
		}
	}
}
function injectHook(type, hook, target = currentInstance, prepend = false) {
	if (target) {
		const hooks = target[type] || (target[type] = []);
		// cache the error handling wrapper for injected hooks so the same hook
		// can be properly deduped by the scheduler. "__weh" stands for "with error
		// handling".
		const wrappedHook = hook.__weh ||
			(hook.__weh = (...args) => {
				if (target.isUnmounted) {
					return;
				}
				// disable tracking inside all lifecycle hooks
				// since they can potentially be called inside effects.
				pauseTracking();
				// Set currentInstance during hook invocation.
				// This assumes the hook does not synchronously trigger other hooks, which
				// can only be false when the user does something really funky.
				setCurrentInstance(target);
				const res = callWithAsyncErrorHandling(hook, target, type, args);
				unsetCurrentInstance();
				resetTracking();
				return res;
			});
		if (prepend) {
			hooks.unshift(wrappedHook);
		}
		else {
			hooks.push(wrappedHook);
		}
		return wrappedHook;
	}
	else if ((process.env.NODE_ENV !== 'production')) {
		const apiName = toHandlerKey(ErrorTypeStrings[type].replace(/ hook$/, ''));
		warn(`${apiName} is called when there is no active component instance to be ` +
			 `associated with. ` +
			 `Lifecycle injection APIs can only be used during execution of setup().` +
			 (` If you are using async setup(), make sure to register lifecycle ` +
			  `hooks before the first await statement.`
			 ));
	}
}
function injectToKeepAliveRoot(hook, type, target, keepAliveRoot) {
	// injectHook wraps the original for error handling, so make sure to remove
	// the wrapped version.
	const injected = injectHook(type, hook, keepAliveRoot, true /* prepend */);
	...
}

onActivated的第一个参数(即onActivated内写的函数:hook)将保存在当前组件实例 (currentInstance) currentInstance[type]内。 onActivated的type是a。 即将函数保存在组件实例的a属性内。
看代码⑦,KeepAlive组件自带的activate方法运行时。 组件实例的a属性内的方法将被执行。即onActivated内的方法被执行。
看代码⑥,如果父组件是KeepAlive的,父组件的组件实例的a属性内也保持了这个hook。
所以当父组件的onActivated钩子内的函数被执行后,其所有子组件的onActivated钩子也会执行

function mountComponent() {
	const componentOptions = vnode.type // vnode的type保存组件选项。
	const { render, data } = componentOptions
	const state = reactive(data())
	const instance = {
		isMounted: false,
		subTree: null
	}
	//  因为更新时是对比vnode。所以要在vnode上保存instance
	vnode.comoponent = instance

	effect(() => {
		const subTree = render.call(state)
		if(!instance.isMounted) {
			patch(null, subTree)
			instance.isMounted = true
		} else {
			// 实现补丁更新。
			patch(instance.subTree, subTree)
		}
		instance.subTree = subTree
	})
}