perf: 中断打字
This commit is contained in:
@ -1,23 +1,16 @@
|
||||
/*
|
||||
* @Author: RenXiaoDong
|
||||
* @Date: 2025-08-20 23:26:23
|
||||
* @Description: 打字效果Hook
|
||||
* 实现文本内容的渐进式显示,模拟打字机效果
|
||||
*/
|
||||
import useState from '@/hooks/useState';
|
||||
import { computed, onWatcherCleanup, unref, watch } from 'vue';
|
||||
import { computed, onUnmounted, unref, watch } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import type { BubbleContentType } from '../types';
|
||||
|
||||
/**
|
||||
* 类型守卫:检查值是否为字符串类型
|
||||
* @param str - 待检查的值
|
||||
* @returns 是否为字符串
|
||||
*/
|
||||
function isString(str: any): str is string {
|
||||
return typeof str === 'string';
|
||||
function isString(content: BubbleContentType): content is string {
|
||||
return typeof content === 'string';
|
||||
}
|
||||
|
||||
// 类型守卫:判断是否为非字符串类型(用于快速排除)
|
||||
function isNonString(content: BubbleContentType): content is Exclude<BubbleContentType, string> {
|
||||
return !isString(content);
|
||||
}
|
||||
/**
|
||||
* 打字效果Hook
|
||||
* 当启用打字效果时,返回渐进式显示的内容和打字状态
|
||||
@ -40,75 +33,115 @@ const useTypedEffect = (
|
||||
const [prevContent, setPrevContent] = useState<BubbleContentType>('');
|
||||
// 当前打字位置(已显示的字符数)
|
||||
const [typingIndex, setTypingIndex] = useState<number>(1);
|
||||
let timer: number | null = null;
|
||||
|
||||
// 合并打字启用状态:只有在启用打字且内容为字符串时才生效
|
||||
// 合并启用状态:仅当 启用打字 且 内容是字符串 时生效
|
||||
const mergedTypingEnabled = computed(() => typingEnabled.value && isString(content.value));
|
||||
|
||||
// 监听内容变化,重置打字索引
|
||||
// 清理定时器
|
||||
const clearTypingTimer = () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 监听内容变化,重置打字状态
|
||||
watch(
|
||||
content,
|
||||
() => {
|
||||
const prevContentValue = unref(prevContent);
|
||||
// 更新上一次的内容记录
|
||||
setPrevContent(content.value);
|
||||
(newVal) => {
|
||||
const prevVal = unref(prevContent);
|
||||
setPrevContent(newVal);
|
||||
|
||||
// 如果未启用打字效果且内容为字符串
|
||||
if (!mergedTypingEnabled.value && isString(content.value)) {
|
||||
// 若外部触发中止,则保持当前索引,不再自动跳到全文
|
||||
// 非字符串类型:直接返回原始值,不执行打字逻辑
|
||||
if (isNonString(newVal)) {
|
||||
setTypingIndex(0); // 重置索引(无意义,仅为状态一致)
|
||||
clearTypingTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
// 字符串类型的逻辑
|
||||
if (!mergedTypingEnabled.value) {
|
||||
if (!(abortRef && abortRef.value)) {
|
||||
setTypingIndex(content.value.length);
|
||||
setTypingIndex(newVal.length); // 直接显示完整内容
|
||||
}
|
||||
} else if (
|
||||
// 如果内容为字符串,且新内容不是以旧内容开头,重置打字索引
|
||||
isString(content.value) &&
|
||||
isString(prevContentValue) &&
|
||||
content.value.indexOf(prevContentValue) !== 0
|
||||
isString(prevVal) &&
|
||||
newVal.indexOf(prevVal) !== 0 // 新内容不是旧内容的前缀,重置打字
|
||||
) {
|
||||
setTypingIndex(1);
|
||||
}
|
||||
clearTypingTimer();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// 启动打字效果
|
||||
// 打字逻辑(仅处理字符串类型)
|
||||
const startTyping = () => {
|
||||
clearTypingTimer();
|
||||
|
||||
// 终止条件:未启用打字 / 内容不是字符串 / 已中止
|
||||
if (!mergedTypingEnabled.value || isNonString(content.value) || (abortRef && abortRef.value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentContent = content.value; // 此时 TypeScript 可推断为 string(因为 mergedTypingEnabled 已过滤)
|
||||
const currentIndex = unref(typingIndex);
|
||||
|
||||
// 已打完所有字符,停止
|
||||
if (currentIndex >= currentContent.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算下一步索引(避免超出长度)
|
||||
const nextIndex = Math.min(currentIndex + typingStep.value, currentContent.length);
|
||||
|
||||
timer = window.setTimeout(() => {
|
||||
setTypingIndex(nextIndex);
|
||||
startTyping(); // 递归触发下一次打字
|
||||
}, typingInterval.value);
|
||||
};
|
||||
|
||||
// 监听打字相关依赖变化
|
||||
watch(
|
||||
[typingIndex, typingEnabled, content, abortRef as any],
|
||||
[typingEnabled, abortRef, () => content.value], // 监听 content 变化
|
||||
() => {
|
||||
// 只有在启用打字、内容为字符串且未显示完所有内容时才执行
|
||||
if (
|
||||
mergedTypingEnabled.value &&
|
||||
isString(content.value) &&
|
||||
unref(typingIndex) < content.value.length &&
|
||||
!(abortRef && abortRef.value)
|
||||
) {
|
||||
// 设置定时器,逐步增加显示字符数
|
||||
const id = setTimeout(() => {
|
||||
setTypingIndex(unref(typingIndex) + typingStep.value);
|
||||
}, typingInterval.value);
|
||||
|
||||
// 清理定时器,避免内存泄漏
|
||||
onWatcherCleanup(() => {
|
||||
clearTimeout(id);
|
||||
});
|
||||
}
|
||||
startTyping();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// 计算当前应该显示的内容
|
||||
const mergedTypingContent = computed(() =>
|
||||
// 如果启用打字且内容为字符串,显示部分内容;
|
||||
// 或外部中止时,固定在当前索引;否则显示全部内容
|
||||
(mergedTypingEnabled.value || (abortRef && abortRef.value)) && isString(content.value)
|
||||
? content.value.slice(0, unref(typingIndex))
|
||||
: content.value,
|
||||
// 计算当前显示的内容(核心:仅对字符串调用 slice)
|
||||
const mergedTypingContent = computed(() => {
|
||||
const currentContent = content.value;
|
||||
|
||||
// 非字符串类型:直接返回原始值(不处理打字)
|
||||
if (isNonString(currentContent)) {
|
||||
return currentContent;
|
||||
}
|
||||
|
||||
// 字符串类型:根据打字状态返回部分/完整内容
|
||||
if (mergedTypingEnabled.value || (abortRef && abortRef.value)) {
|
||||
return currentContent.slice(0, unref(typingIndex)); // 此时确定是 string,可安全调用 slice
|
||||
}
|
||||
|
||||
return currentContent; // 不启用打字时,返回完整字符串
|
||||
});
|
||||
|
||||
// 是否正在打字(仅字符串类型可能为 true)
|
||||
const isTyping = computed(
|
||||
() =>
|
||||
mergedTypingEnabled.value &&
|
||||
unref(typingIndex) < content.value.length && // 此时 content.value 已被 mergedTypingEnabled 限定为 string
|
||||
!(abortRef && abortRef.value),
|
||||
);
|
||||
|
||||
return [
|
||||
mergedTypingContent,
|
||||
// 计算是否正在打字:启用打字 + 内容为字符串 + 未显示完所有内容
|
||||
computed(() => mergedTypingEnabled.value && isString(content.value) && unref(typingIndex) < content.value.length),
|
||||
];
|
||||
// 组件卸载时清理定时器
|
||||
onUnmounted(() => {
|
||||
clearTypingTimer();
|
||||
});
|
||||
|
||||
return [mergedTypingContent, isTyping];
|
||||
};
|
||||
|
||||
export default useTypedEffect;
|
||||
|
||||
Reference in New Issue
Block a user