跳至主要內容

18. 实现 Transition


18. 实现 Transition

1.Transition 使用

<script type="module">
	import {
		Transition,
		Teleport,
		defineAsyncComponent,
		createRenderer,
		h,
		render,
		Text,
		Fragment,
		ref,
		reactive,
		getCurrentInstance,
		onMounted,
		provide,
		inject,
		toRef,
		KeepAlive
	} from '/node_modules/@vue/runtime-dom/dist/runtime-dom.esm-browser.js';
	const props = {
		onBeforeEnter(el) {
			console.log(el, 'beforeEnter');
		},
		onEnter(el) {
			console.log(el, 'enter');
		},
		onLeave(el) {
			console.log(el, 'leave');
		}
	};
	render(
		h(Transition, props, {
			default: () => {
				return h(
					'div',
					{ style: { width: '100px', height: '100px', background: 'red' } },
					'haha'
				);
			}
		}),
		app
	);
	setTimeout(() => {
		render(
			h(Transition, props, {
				default: () => {
					return h(
						'p',
						{ style: { width: '100px', height: '100px', background: 'blue' } },
						'world'
					);
				}
			}),
			app
		);
	}, 4000);
	setTimeout(() => {
		render(
			h(Transition, props, {
				default: () => {
					return h(
						'div',
						{ style: { width: '100px', height: '100px', background: 'red' } },
						'haha'
					);
				}
			}),
			app
		);
	}, 8000);
</script>
<style>
	.v-enter-active,
	.v-leave-active {
		transition: opacity 2s ease;
	}

	.v-enter-from,
	.v-leave-to {
		opacity: 0;
	}
</style>

2.Transition 核心实现

import { isKeepAlive } from './keepAlive';
import { h } from './h';
function nextFrame(cb) {
	requestAnimationFrame(() => {
		requestAnimationFrame(cb);
	});
}
function resolveTransitionProps(rawProps) {
	const {
		name = 'v',
		enterFromClass = `${name}-enter-from`,
		enterActiveClass = `${name}-enter-active`,
		enterToClass = `${name}-enter-to`,
		leaveFromClass = `${name}-leave-from`,
		leaveActiveClass = `${name}-leave-active`,
		leaveToClass = `${name}-leave-to`,
		onBeforeEnter, // 进入前
		onEnter, // 进入
		onLeave // 离开时
	} = rawProps;

	return {
		onBeforeEnter(el) {
			onBeforeEnter && onBeforeEnter(el);
			el.classList.add(enterFromClass); // 进入前添加的类名
			el.classList.add(enterActiveClass);
		},
		onEnter(el, done) {
			const resolve = () => {
				// 进入完毕后全部移除
				el.classList.remove(enterActiveClass);
				el.classList.remove(enterToClass);
				done && done();
			};
			onEnter && onEnter(el, resolve);
			nextFrame(() => {
				el.classList.remove(enterFromClass);
				el.classList.add(enterToClass); // 进入后添加类名
				// 绑定transition组件

				// 用户没传递onEnter 或者onEnter参数只有一个
				if (!onEnter || onEnter.length <= 1) {
					// 监听transitionend事件
					el.addEventListener('transitionend', resolve);
				}
			});
		},
		onLeave(el, done) {
			const resolve = () => {
				// 进入完毕后全部移除
				el.classList.remove(leaveActiveClass);
				el.classList.remove(leaveToClass);
				done && done();
			};
			el.classList.add(leaveFromClass); // 离开
			document.body.offsetHeight; // 让leaveFromClass 立即影响变化
			el.classList.add(leaveActiveClass);
			nextFrame(() => {
				el.classList.remove(leaveFromClass);
				el.classList.add(leaveToClass); // 离开

				if (!onLeave || onLeave.length <= 1) {
					// 监听transitionend事件
					el.addEventListener('transitionend', resolve);
				}
			});
			onLeave && onLeave(el, resolve); // 调用leave
		}
	};
}
export const Transition = (props, { slots }) => {
	return h(BaseTransitionImpl, resolveTransitionProps(props), slots);
};

3.Transition 组件

function getKeepAliveChild(vnode) {
	// 是keep-alive 则拿keep-alive 插槽中的内容
	return isKeepAlive(vnode)
		? vnode.children
			? vnode.children.default()
			: undefined
		: vnode;
}

function resolveTransitionHooks(props) {
	let { onBeforeEnter, onEnter, onLeave } = props;
	const hooks = {
		beforeEnter(el) {
			onBeforeEnter && onBeforeEnter(el);
		},
		enter(el) {
			onEnter && onEnter(el);
		},
		leave(el, remove) {
			onLeave && onLeave(el, remove);
		}
	};
	return hooks;
}
export const BaseTransitionImpl = {
	name: 'BaseTransition',
	props: { onBeforeEnter: Function, onEnter: Function, onLeave: Function },
	setup(props, { slots }) {
		const instance = getCurrentInstance();
		return () => {
			const child = slots.default && slots.default();
			// 如果没有插槽则直接结束即可
			if (!child) {
				return;
			}
			// 获取内部插槽内容
			const innerChild = getKeepAliveChild(child);
			const enterHooks = resolveTransitionHooks(props);
			innerChild.transition = enterHooks;

			const oldChild = instance.subTree;
			const oldInnerChild = oldChild && getKeepAliveChild(oldChild);
			if (oldInnerChild) {
				// 意味着离开
				if (!isSameVNode(innerChild, oldChild)) {
					const leavingHooks = resolveTransitionHooks(props);
					oldInnerChild.transition = leavingHooks;
				}
			}
			return child;
		};
	}
};

4.挂载元素

const mountElement = (vnode, container, anchor, parentComponent) => {
	const { type, props, children, shapeFlag, transition } = vnode;
	const el = (vnode.el = hostCreateElement(type));
	if (props) {
		for (let key in props) {
			hostPatchProp(el, key, null, props[key]);
		}
	}
	if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
		mountChildren(children, el);
	} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
		hostSetElementText(el, children);
	}
	// 插入前
	if (transition) {
		transition.beforeEnter(el);
	}
	// 插入
	hostInsert(el, container, anchor);
	// 插入后
	if (transition) {
		transition.enter(el);
	}
};
const remove = (vnode) => {
	const { type, el, transition } = vnode;
	const performRemove = () => {
		hostRemove(el);
	};
	if (transition) {
		// 触发离开后,在调用真实的删除操作
		transition.leave(el, performRemove);
	} else {
		performRemove();
	}
};

const unmount = (vnode, parentComponent) => {
	// ...
	remove(vnode);
};
import { isKeepAlive } from './keepAlive';
import { h } from './h';
import { isSameVNode } from './vnode';
import { getCurrentInstance } from './component';
function nextFrame(cb) {
	requestAnimationFrame(() => {
		requestAnimationFrame(cb);
	});
}
function resolveTransitionProps(rawProps) {
	const {
		name = 'v',
		enterFromClass = `${name}-enter-from`,
		enterActiveClass = `${name}-enter-active`,
		enterToClass = `${name}-enter-to`,
		leaveFromClass = `${name}-leave-from`,
		leaveActiveClass = `${name}-leave-active`,
		leaveToClass = `${name}-leave-to`,
		onBeforeEnter, // 进入前
		onEnter, // 进入
		onLeave // 离开时
	} = rawProps;

	return {
		onBeforeEnter(el) {
			onBeforeEnter && onBeforeEnter(el);
			el.classList.add(enterFromClass); // 进入前添加的类名
			el.classList.add(enterActiveClass);
		},
		onEnter(el) {
			const resolve = () => {
				// 进入完毕后全部移除
				el.classList.remove(enterActiveClass);
				el.classList.remove(enterToClass);
			};
			onEnter && onEnter(el, resolve);
			nextFrame(() => {
				el.classList.remove(enterFromClass);
				el.classList.add(enterToClass); // 进入后添加类名
				// 绑定transition组件

				// 用户没传递onEnter 或者onEnter参数只有一个
				if (!onEnter || onEnter.length <= 1) {
					// 监听transitionend事件
					el.addEventListener('transitionend', resolve);
				}
			});
		},
		onLeave(el, done) {
			const resolve = () => {
				// 进入完毕后全部移除
				el.classList.remove(leaveActiveClass);
				el.classList.remove(leaveToClass);
				done && done();
			};
			el.classList.add(leaveFromClass); // 离开
			document.body.offsetHeight; // 让leaveFromClass 立即影响变化
			el.classList.add(leaveActiveClass);
			nextFrame(() => {
				el.classList.remove(leaveFromClass);
				el.classList.add(leaveToClass); // 离开

				if (!onLeave || onLeave.length <= 1) {
					// 监听transitionend事件
					el.addEventListener('transitionend', resolve);
				}
			});
			onLeave && onLeave(el, resolve); // 调用leave
		}
	};
}

export const BaseTransitionImpl = {
	name: 'BaseTransition',
	props: ['onBeforeEnter', 'onEnter', 'onLeave'],
	setup(props, { slots }) {
		const instance = getCurrentInstance();
		return () => {
			const child = slots.default && slots.default();
			// 如果没有插槽则直接结束即可
			if (!child) {
				return;
			}
			// 获取内部插槽内容
			const innerChild = getKeepAliveChild(child);
			const enterHooks = resolveTransitionHooks(props);
			innerChild.transition = enterHooks;

			const oldChild = instance.subTree;
			const oldInnerChild = oldChild && getKeepAliveChild(oldChild);
			if (oldInnerChild) {
				// 意味着离开
				if (!isSameVNode(innerChild, oldChild)) {
					const leavingHooks = resolveTransitionHooks(props);
					oldInnerChild.transition = leavingHooks;
				}
			}
			return child;
		};
	}
};

export const Transition = (props, { slots }) => {
	return h(BaseTransitionImpl, resolveTransitionProps(props), slots);
};

function resolveTransitionHooks(props) {
	let { onBeforeEnter, onEnter, onLeave } = props;
	const hooks = {
		beforeEnter(el) {
			onBeforeEnter && onBeforeEnter(el);
		},
		enter(el) {
			onEnter && onEnter(el);
		},
		leave(el, remove) {
			onLeave && onLeave(el, remove);
		}
	};
	return hooks;
}
function getKeepAliveChild(vnode) {
	// 是keep-alive 则拿keep-alive 插槽中的内容
	return isKeepAlive(vnode)
		? vnode.children
			? vnode.children.default()
			: undefined
		: vnode;
}