什么年代了?还不懂为什么一定要在团队项目开发中去使用 TypeScript ?
为什么要去使用 TypeScript ?
一直以来 TypeScript
的存在都备受争议,很多人认为他加重了前端开发的负担,特别是在它的严格类型系统和 JavaScript
的灵活性之间的矛盾上引发了不少讨论。
支持者认为 TypeScript
提供了强类型检查、丰富的 IDE 支持和更好的代码重构能力,从而提高了大型项目的代码质量和可维护性。
然而,也有很多开发者认为 TypeScript
加重了开发负担,带来了不必要的复杂性,尤其是在小型项目或快速开发场景中,它的严格类型系统可能显得过于繁琐,限制了 JavaScript
本身的动态和自由特性
但是随着项目规模的增大和团队协作的复杂性增加,TypeScript
的优势也更加明显。因为你不可能指望团队中所有人的知识层次和开发习惯都达到同一水准!你也不可能保证团队中的其他人都能够完全正确的使用你封装的组件、函数!
在大型项目中我们往往会封装到很多工具函数、组件等等,我们不可能在使用到组件时跑去看这个组件的实现逻辑,而
TypeScript
的类型提示正好弥补了这一点。通过明确的类型注解,TypeScript
可以在代码中直接提示每个组件的输入输出、参数类型和预期结果,让开发者只需在 IDE 中悬停或查看提示信息,就能了解组件的用途和使用方式,而不需要翻阅具体实现逻辑。这时你可能会说,使用
JSDoc
也能够实现类似的效果。的确,JSDoc
可以通过注释的形式对函数、参数、返回值等信息进行详细描述,甚至可以生成文档。然而,
JSDoc
依赖于开发者的自觉维护,且其检查和提示能力远不如TypeScript
强大和全面。TypeScript
的类型系统是在编译阶段强制执行的,这意味着所有类型定义都是真正的 “硬性约束”,能在代码运行前捕获错误,而不仅仅是提示。在实际开发中,
JSDoc
的确能让我们知道参数类型,但它只是一种 “约定” ,而不是真正的约束。这意味着,如果同事在使用工具函数时不小心写错了类型,比如传了字符串而不是数字,JSDoc
只能通过注释告诉你正确的使用方法,却无法在你出错时立即给出警告。然而在
TypeScript
中,类型系统会在代码编写阶段实时检查。比如,你定义的函数要求传入数字类型的参数,如果有人传入了字符串,IDE 立刻会报错提醒你,防止错误进一步传播。所以,TypeScript 的价值就在于它提供了一层代码保护,让代码有了“硬约束”,团队在开发过程中更加节省心智负担,显著提升开发体验和生产力,少出错、更高效。
接下来我们来使用 TypeScript
写一个基础的防抖函数作为示例。通过类型定义和参数注解,我们不仅能让防抖函数更加通用且类型安全,还能充分利用 TypeScript
的类型检查优势,从而提高代码的可读性和可维护性。
这样的实现方式将有效地降低潜在的运行时错误,特别是在大型项目中,可以使团队成员之间的协作能够更加顺畅,并且避免一些低级问题。
功能点讲解
防抖函数的主要功能是:在指定的延迟时间内,如果函数多次调用,只有最后一次调用会生效。这一功能尤其适合优化用户输入等高频事件。
防抖函数的核心功能
- 函数执行的延迟控制:函数调用后不立即执行,而是等待一段时间。如果在等待期间再次调用函数,之前的等待会被取消,重新计时。
- 立即执行选项:有时我们希望函数在第一次调用时立即执行,然后在延迟时间内避免再次调用。
- 取消功能:我们还希望在某些情况下手动取消延迟执行的函数,比如当页面卸载或需要重新初始化时。
第一步:编写函数框架
在开始封装防抖函数之前,我们首先应该想到的就是要写一个函数,假设这个函数名叫 debounce
。我们先创建它的基本框架:
function debounce() {
// 函数的逻辑将在这里编写
}
这一步非常简单,先定义一个空函数,这个函数就是我们的防抖函数。在后续步骤中,我们会逐步向这个函数中添加功能。
第二步:添加基本的参数
防抖函数的第一个功能是控制某个函数的执行,因此,我们需要传递一个需要防抖的函数。其次,防抖功能依赖于一个延迟时间,这意味着我们还需要添加一个用于设置延迟的参数。
让我们扩展一下 debounce
函数,为它添加两个基本的参数:
func
:需要防抖的目标函数。duration
:防抖的延迟时间,单位是毫秒。
function debounce(func: Function, duration: number) {
// 函数的逻辑将在这里编写
}
func
是需要防抖的函数。每当防抖函数被调用时,我们实际上是在控制这个func
函数的执行。duration
是延迟时间。这个参数控制了在多长时间后执行目标函数。
第三步:为防抖功能引入定时器逻辑
防抖的核心逻辑就是通过定时器(setTimeout
),让函数执行延后。那么我们需要用一个变量来保存这个定时器,以便在函数多次调用时可以取消之前的定时器。
function debounce(func: Function, duration: number) {
let timer: ReturnType<typeof setTimeout> | null = null; // 定时器变量
}
let timer: ReturnType<typeof setTimeout> | null = null
:我们使用一个变量timer
来存储定时器的返回值。clearTimeout(timer)
:每次调用防抖函数时,都会清除之前的定时器,这样就保证了函数不会被立即执行,直到等待时间结束。setTimeout
:在指定的延迟时间后执行传入的目标函数func
,并传递原始参数。
为什么写成了 ReturnType<typeof setTimeout> | null
这样的类型 ?
在 JavaScript
中,setTimeout
是一个内置函数,用来设置一个延迟执行的任务。它的基本语法如下:
let id = setTimeout(() => {
console.log("Hello, world!");
}, 1000);
setTimeout
返回一个定时器 ID(在浏览器中是一个数字),这个 ID 用来唯一标识这个定时器。如果你想取消定时器,你可以使用 clearTimeout(id)
,其中 id
就是这个返回的定时器 ID。
ReturnType<T>
是 TypeScript 提供的一个工具类型,它的作用是帮助我们获取某个函数类型的返回值类型。我们通过泛型T
来传入一个函数类型,然后ReturnType<T>
就会返回这个函数的返回值类型。在这里我们可以用它来获取setTimeout
函数的返回类型。
为什么需要使用 ReturnType<typeof setTimeout>
?
由于不同的 JavaScript
运行环境中,setTimeout
的返回值类型是不同的:
- 在浏览器中,
setTimeout
返回的是一个数字 ID。 - 在Node.js 中,
setTimeout
返回的是一个对象(Timeout
对象)。
为了兼容不同的环境,我们需要用 ReturnType<typeof setTimeout>
来动态获取 setTimeout
返回的类型,而不是手动指定类型(比如 number
或 Timeout
)。
let timer: ReturnType<typeof setTimeout>;
这里 ReturnType<typeof setTimeout>
表示我们根据 setTimeout
的返回值类型自动推导出变量 timer
的类型,不管是数字(浏览器)还是对象(Node.js),TypeScript 会自动处理。
为什么需要设置联合类型 | null
?
在我们的防抖函数实现中,定时器 timer
并不是一开始就设置好的。我们需要在每次调用防抖函数时动态设置定时器,所以初始状态下,timer
的值应该是 null
。
使用 | null
表示联合类型,它允许 timer
变量既可以是 setTimeout
返回的值,也可以是 null
,表示目前还没有设置定时器。
let timer: ReturnType<typeof setTimeout> | null = null;
ReturnType<typeof setTimeout>
:表示timer
可以是setTimeout
返回的定时器 ID。| null
:表示在初始状态下,timer
没有定时器,它的值为null
。
第四步:返回一个新函数
在防抖函数 debounce
中,我们希望当它被调用时,返回一个新的函数。这是防抖函数的核心机制,因为每次调用返回的新函数,实际上是在控制目标函数 func
的执行。
具体的想法是这样的:我们并不直接执行传入的目标函数 func
,而是返回一个新函数,这个新函数在被调用时会受到防抖的控制。
因此,我们要修改 debounce
函数,使它返回一个新的函数,真正控制 func
的执行时机。
function debounce(func: Function, duration: number) {
let timer: ReturnType<typeof setTimeout> | null = null; // 定时器变量
return function () {
// 防抖逻辑将在这里编写
};
}
- 返回新函数:当
debounce
被调用时,它返回一个新函数。这个新函数是每次调用时执行防抖逻辑的入口。 - 为什么返回新函数? :因为我们需要在每次事件触发时(例如用户输入时)执行防抖操作,而不是直接执行传入的目标函数
func
。
第五步:清除之前的定时器
为了实现防抖功能,每次调用返回的新函数时,我们需要先清除之前的定时器。如果之前有一个定时器在等待执行目标函数,我们应该将其取消,然后重新设置一个新的定时器。
这个步骤的关键就是使用 clearTimeout(timer)
。
function debounce(func: Function, duration: number) {
let timer: ReturnType<typeof setTimeout> | null = null; // 定时器变量
return function () {
if (timer) {
clearTimeout(timer); // 清除之前的定时器
}
// 下面将设置新的定时器
};
}
if (timer)
:我们检查timer
是否有值。如果它有值,说明之前的定时器还在等待执行,我们需要将其清除。clearTimeout(timer)
:这就是清除之前的定时器,防止之前的调用被执行。这个操作非常关键,因为它确保了只有最后一次调用(在延迟时间后)才会真正触发目标函数。
第六步:设置新的定时器
现在我们需要在每次调用返回的新函数时,重新设置一个新的定时器,让它在指定的延迟时间 duration
之后执行目标函数 func
。
这时候就要使用 setTimeout
来设置定时器,并在延迟时间后执行目标函数。
function debounce(func: Function, duration: number) {
let timer: ReturnType<typeof setTimeout> | null = null; // 定时器变量
return function () {
if (timer) {
clearTimeout(timer); // 清除之前的定时器
}
timer = setTimeout(() => {
func(); // 延迟后调用目标函数
}, duration);
};
}
setTimeout
:我们使用setTimeout
来设置一个新的定时器,定时器将在duration
毫秒后执行传入的目标函数func
。func()
:这是目标函数的实际执行点。定时器到达延迟时间时,它会执行目标函数func
。timer = setTimeout(...)
:我们将定时器的 ID 存储在timer
变量中,以便后续可以使用clearTimeout(timer)
来清除定时器。
第七步:支持参数传递
接下来是让这个防抖函数能够接受参数,并将这些参数传递给目标函数 func
。
为了实现这个功能,我们需要用到 ...args
来捕获所有传入的参数,并在执行目标函数时将这些参数传递过去。
function debounce(func: Function, duration: number) {
let timer: ReturnType<typeof setTimeout> | null = null; // 定时器变量
return function (...args: any[]) { // 接收传入的参数
if (timer) {
clearTimeout(timer); // 清除之前的定时器
}
timer = setTimeout(() => {
func(...args); // 延迟后调用目标函数,并传递参数
}, duration);
};
}
...args: any[]
:这表示新函数可以接收任意数量的参数,并将这些参数存储在args
数组中。func(...args)
:当定时器到达延迟时间后,调用目标函数func
,并将args
中的所有参数传递给它。这确保了目标函数能接收到我们传入的所有参数。
到这里,我们一个基本的防抖函数的实现。这个防抖函数实现了以下基本功能:
- 函数执行的延迟控制:每次调用时,都重新设置定时器,确保函数不会立即执行,而是在延迟结束后才执行。
- 多参数支持:通过
...args
,防抖函数能够接收多个参数,并将它们传递给目标函数。 - 清除之前的定时器:在每次调用时,如果定时器已经存在,先清除之前的定时器,确保只有最后一次调用才会生效。
但是,这样就完了吗?
在当前的实现中,
debounce
函数的定义是debounce(func: Function, duration: number)
,其中func: Function
用来表示目标函数。这种定义虽然可以工作,但它存在明显的缺陷和不足之处,尤其是在 TypeScript 强调类型安全的情况下。
缺陷 1:缺乏参数类型检查
Function
是一种非常宽泛的类型,它允许目标函数接收任何类型、任意数量的参数。因此定义目标函数 func
为 Function
类型意味着 TypeScript 无法对目标函数的参数类型进行任何检查。
const debounced = debounce((a: number, b: number) => {
console.log(a + b);
}, 200);
debounced("hello", "world"); // 这里不会报错,参数类型不匹配,但仍会被调用
在这个例子中,我们定义了一个目标函数,期望它接受两个数字类型的参数,但在实际调用时却传入了两个字符串。
这种情况下 TypeScript 不会提示任何错误,因为 Function
类型没有对参数类型进行限制。这种类型检查的缺失可能导致运行时错误或者逻辑上的错误。
缺陷 2:返回值类型不安全
同样,定义 func
为 Function
类型时,TypeScript 无法推断目标函数的返回值类型。这意味着防抖函数不能保证目标函数的返回值是符合预期的类型,可能导致返回值在其他地方被错误使用。
const debounced = debounce(() => {
return "result";
}, 200);
const result = debounced(); // TypeScript 不知道返回值类型,认为是 undefined
在这个例子中,虽然目标函数明确返回了一个字符串 "result"
,但 debounced
函数的返回值类型未被推断出来,因此 TypeScript 会认为它的返回值是 void
或 undefined
,即使目标函数实际上返回了 string
。
缺陷 3:缺乏目标函数的签名限制
由于 Function
类型允许任何形式的函数,因此 TypeScript 也无法检查目标函数的参数个数和类型是否匹配。这种情况下,如果防抖函数返回的新函数接收了错误数量或类型的参数,可能导致函数行为异常或意外的运行时错误。
const debounced = debounce((a: number) => {
console.log(a);
}, 200);
debounced(1, 2, 3); // TypeScript 不会报错,但多余的参数不会被使用
虽然目标函数只期望接收一个参数,但在调用时传入了多个参数。TypeScript 不会进行任何警告或报错,因为 Function
类型允许这种宽泛的调用,这可能会导致开发者误以为这些参数被使用。
总结 func: Function
的缺陷
- 缺乏参数类型检查:任何数量、任意类型的参数都可以传递给目标函数,导致潜在的参数类型错误。
- 返回值类型不安全:目标函数的返回值类型无法被推断,导致 TypeScript 无法确保返回值的类型正确。
- 函数签名不受限制:没有对目标函数的参数个数和类型进行检查,容易导致逻辑错误或参数使用不当。
这些缺陷使得代码在类型安全性和健壮性上存在不足,可能导致运行时错误或者隐藏的逻辑漏洞。
下一步的改进
为了解决这些缺陷,我们可以通过泛型的方式为目标函数添加类型限制,确保目标函数的参数和返回值类型都能被准确地推断和检查。这会是我们接下来要进行的优化。
第八步:使用泛型优化
为了克服 func: Function
带来的缺陷,我们可以通过 泛型 来优化防抖函数的类型定义,确保目标函数的参数和返回值都能在编译时进行类型检查。使用泛型不仅可以解决参数类型和返回值类型的检查问题,还可以提升代码的灵活性和安全性。
如何使用泛型进行优化?
我们将通过引入两个泛型参数来改进防抖函数的类型定义:
A
:表示目标函数的参数类型,可以是任意类型和数量的参数,确保防抖函数在接收参数时能进行类型检查。R
:表示目标函数的返回值类型,确保防抖函数返回的值与目标函数一致。
function debounce<A extends any[], R>(
func: (...args: A) => R, // 使用泛型 A 表示参数,R 表示返回值类型
duration: number // 延迟时间,以毫秒为单位
): (...args: A) => R { // 返回新函数,参数类型与目标函数相同,返回值类型为 R
let timer: ReturnType<typeof setTimeout> | null = null; // 定时器变量
let lastResult: R; // 存储目标函数的返回值
return function (...args: A): R { // 返回的新函数,参数类型由 A 推断
if (timer) {
clearTimeout(timer); // 清除之前的定时器
}
timer = setTimeout(() => {
lastResult = func(...args); // 延迟后调用目标函数,并存储返回值
}, duration);
return lastResult; // 返回上一次执行的结果,如果尚未执行则返回 undefined
};
}
A extends any[]
:A
表示目标函数的参数类型,A
是一个数组类型,能够适应目标函数接收多个参数的场景。通过泛型,防抖函数能够根据目标函数的签名推断出参数类型并进行检查。R
:R
表示目标函数的返回值类型,防抖函数能够确保返回值类型与目标函数一致。如果目标函数返回值类型为string
,防抖函数也会返回string
,这样可以防止返回值类型不匹配。lastResult
:用来存储目标函数的最后一次返回值。每次调用目标函数时会更新lastResult
,并在调用时返回上一次执行的结果,确保防抖函数返回正确的返回值。
泛型优化后的优点:
- 类型安全的参数传递:
通过泛型A
,防抖函数可以根据目标函数的签名进行类型检查,确保传入的参数与目标函数一致,避免参数类型错误。const debounced1 = debounce((a: number, b: string) => {
console.log(a, b);
}, 300);
debounced1(42, "hello"); // 正确,参数类型匹配
debounced1("42", 42); // 错误,类型不匹配 - 返回值类型安全:
泛型R
确保了防抖函数的返回值与目标函数的返回值类型一致,防止不匹配的类型被返回。const debounced = debounce(() => {
return "result";
}, 200);
const result = debounced(); // 返回值为 string
console.log(result); // 输出 "result" - 支持多参数传递:
泛型A
表示参数类型数组,这意味着目标函数可以接收多个参数,防抖函数会将这些参数正确传递给目标函数。而如果防抖函数返回的新函数接收了错误数量或类型的参数,会直接报错提示。const debounced = debounce((name: string, age: number) => {
return `${name} is ${age} years old.`;
}, 300);
const result = debounced("Alice", 30);
console.log(result); // 输出 "Alice is 30 years old."
第九步:添加 cancel
方法并处理返回值类型
在前面的步骤中,我们已经实现了一个可以延迟执行的防抖函数,并且支持参数传递和返回目标函数的结果。
但是,由于防抖函数的执行是异步延迟的,因此在初次调用时,防抖函数可能无法立即返回结果。因此函数的返回值我们需要使用 undefined
来表示目标函数的返回结果可能出现还没生成的情况。
除此之外,我们还要为防抖函数添加一个 cancel
方法,用于手动取消防抖的延迟执行。
为什么需要 cancel
方法?
在一些场景下,可能需要手动取消防抖操作,例如:
- 用户取消了操作,不希望目标函数再执行。
- 某个事件或操作已经不再需要处理,因此需要取消延迟中的函数调用。
为了解决这些需求,cancel
方法可以帮助我们在定时器还未触发时,清除定时器并停止目标函数的执行。
// 定义带有 cancel 方法的防抖函数类型
type DebouncedFunction<A extends any[], R> = {
(...args: A): R | undefined; // 防抖函数本身,返回值可能为 R 或 undefined
cancel: () => void; // `cancel` 方法,用于手动清除防抖
};
// 实现防抖函数
function debounce<A extends any[], R>(
func: (...args: A) => R, // 泛型 A 表示参数类型,R 表示返回值类型
duration: number // 延迟时间
): DebouncedFunction<A, R> { // 返回带有 cancel 方法的防抖函数
let timer: ReturnType<typeof setTimeout> | null = null; // 定时器变量
let lastResult: R | undefined; // 用于存储目标函数的返回值
// 防抖逻辑的核心函数
const debouncedFn = function (...args: A): R | undefined {
if (timer) {
clearTimeout(timer); // 清除之前的定时器
}
// 设置新的定时器
timer = setTimeout(() => {
lastResult = func(...args); // 延迟后执行目标函数,并存储返回值
}, duration);
// 返回上一次的结果或 undefined
return lastResult;
};
// 添加 `cancel` 方法,用于手动取消防抖
debouncedFn.cancel = function () {
if (timer) {
clearTimeout(timer); // 清除定时器
timer = null; // 重置定时器
}
};
return debouncedFn; // 返回带有 `cancel` 方法的防抖函数
}
- 返回值类型
R | undefined
:R
:代表目标函数的返回值类型,例如number
或string
。undefined
:在防抖函数的首次调用或目标函数尚未执行时,返回undefined
,表示结果尚未生成。lastResult
用于存储目标函数上一次执行的结果,防抖函数在每次调用时会返回该结果,或者在尚未执行时返回undefined
。
cancel
方法:cancel
方法的作用是清除当前的定时器,防止目标函数在延迟时间结束后被执行。- 通过调用
clearTimeout(timer)
,我们可以停止挂起的防抖操作,并将timer
重置为null
,表示当前没有挂起的定时器。
让我们来看一个具体的使用示例,展示如何使用防抖函数,并在需要时手动取消操作。
// 定义一个简单的目标函数
const debouncedLog = debounce((message: string) => {
console.log(message);
return message;
}, 300);
// 第一次调用防抖函数,目标函数将在 300 毫秒后执行
debouncedLog("Hello"); // 如果不取消,300ms 后会输出 "Hello"
// 手动取消防抖,目标函数不会执行
debouncedLog.cancel();
在这个示例中:
- 调用
debouncedLog("Hello")
:会启动一个 300 毫秒的延迟执行,目标函数计划在 300 毫秒后执行,并输出"Hello"
。 - 调用
debouncedLog.cancel()
:会清除定时器,目标函数不会执行,避免了不必要的操作。
第十步:将防抖函数作为工具函数单独放在一个 ts
文件中并添加 JSDoc 注释
在编写好防抖函数之后,下一步是将其作为一个工具函数放入单独的 .ts
文件中,以便在项目中重复使用。同时,我们可以为函数添加详细的 JSDoc 注释,方便使用者了解函数的作用、参数、返回值及用法。
1. 将防抖函数放入单独的文件
首先,我们可以创建一个名为 debounce.ts
的文件,并将防抖函数的代码放在其中。
// debounce.ts
export type DebouncedFunction<A extends any[], R> = {
(...args: A): R | undefined; // 防抖函数本身,返回值可能为 R 或 undefined
cancel: () => void; // `cancel` 方法,用于手动清除防抖
};
/**
* 创建一个防抖函数,确保在最后一次调用后,目标函数只会在指定的延迟时间后执行。
* 防抖函数可以防止某个函数被频繁调用,例如用户输入事件、滚动事件或窗口调整大小等场景。
*
* @template A - 函数接受的参数类型。
* @template R - 函数的返回值类型。
* @param {(...args: A) => R} func - 需要防抖的目标函数。该函数将在延迟时间后执行。
* @param {number} duration - 延迟时间(以毫秒为单位)。在这个时间内,如果再次调用函数,将重新计时。
* @returns {DebouncedFunction<A, R>} 一个防抖后的函数,该函数包括一个 `cancel` 方法用于清除防抖。
*
* @example
* const debouncedLog = debounce((message: string) => {
* console.log(message);
* return message;
* }, 300);
*
* debouncedLog("Hello"); // 300ms 后输出 "Hello"
* debouncedLog.cancel(); // 取消防抖,函数不会执行
*/
export function debounce<A extends any[], R>(
func: (...args: A) => R,
duration: number
): DebouncedFunction<A, R> {
let timer: ReturnType<typeof setTimeout> | null = null; // 定时器变量
let lastResult: R | undefined; // 存储目标函数的返回值
const debouncedFn = function (...args: A): R | undefined {
if (timer) {
clearTimeout(timer); // 清除之前的定时器
}
timer = setTimeout(() => {
lastResult = func(...args); // 延迟后执行目标函数,并存储返回值
}, duration);
return lastResult; // 返回上次执行的结果,如果尚未执行则返回 undefined
};
debouncedFn.cancel = function () {
if (timer) {
clearTimeout(timer); // 清除定时器,防止目标函数被执行
timer = null; // 重置定时器
}
};
return debouncedFn;
}
2. 详细的 JSDoc 注释说明
通过添加 JSDoc 注释,能够为函数使用者提供清晰的文档信息,说明防抖函数的功能、参数类型、返回值类型,以及如何使用它。
JSDoc 注释的结构说明:
@template A, R
:说明泛型A
是函数接受的参数类型,R
是目标函数的返回值类型。@param
:解释函数的输入参数,说明func
是目标函数,duration
是防抖的延迟时间。@returns
:说明返回值是一个带有cancel
方法的防抖函数,函数返回值类型是R | undefined
。@example
:为函数提供示例,展示防抖函数的典型用法,包括取消防抖操作。
使用 JSDoc 生成文档
通过在 .ts
文件中添加 JSDoc 注释,可以借助 TypeScript 编辑器或 IDE(如 VSCode/Webstorm)自动生成代码提示和函数文档说明,提升开发体验。
例如,当开发者在使用 debounce
函数时,可以自动看到函数的说明和参数类型提示:
回顾:泛型防抖函数的最终效果
通过前面各个步骤的优化,我们已经构建了一个类型安全的防抖函数,结合泛型实现了以下关键功能:
- 类型安全的参数传递:
通过泛型A
,防抖函数能够根据目标函数的签名进行参数类型检查,确保传入的参数与目标函数的类型一致。如果传入的参数类型不匹配,TypeScript 将在编译时报错,避免运行时的潜在错误。const debounced1 = debounce((a: number, b: string) => {
console.log(a, b);
}, 300);
debounced1(42, "hello"); // 正确,参数类型匹配
debounced1("42", 42); // 错误,类型不匹配在上面的例子中,TypeScript 会检查参数类型,确保传入的参数符合预期的类型。错误的参数类型会被及时捕捉。
- 返回值类型安全:
泛型R
确保防抖函数的返回值与目标函数的返回值类型保持一致。TypeScript 可以根据目标函数的返回值类型推断防抖函数的返回值,防止不匹配的类型被返回。const debounced = debounce(() => {
return "result";
}, 200);
const result = debounced(); // 返回值为 string
console.log(result); // 输出 "result"
在这个例子中,debounce
返回的防抖函数的返回值类型为string
或者undefind
,因为在防抖函数的实现中,目标函数是延迟执行的,因此在初次调用或在延迟期间,debounced
函数返回的结果可能尚未生成,与目标函数的返回值类型预期一致。 - 支持多参数传递:
泛型A
表示目标函数的参数类型数组,这意味着防抖函数可以正确传递多个参数,并确保类型安全。如果传入了错误数量或类型的参数,TypeScript 会提示开发者进行修正。const debounced = debounce((name: string, age: number) => {
return `${name} is ${age} years old.`;
}, 300);
const result = debounced("Alice", 30);
console.log(result); // 输出 "Alice is 30 years old."在这个例子中,防抖函数正确地将多个参数传递给目标函数,并输出目标函数的正确返回值。传入的参数数量或类型不正确时,TypeScript 会发出报错提示。
总结
至此,我们完整实现并优化了一个类型安全的防抖函数,并通过泛型确保参数和返回值的类型安全。此外,我们还详细讲解了如何为防抖函数添加 cancel
方法,并处理延迟执行的返回值 R | undefined
。最后,我们将防抖函数封装在一个单独的 TypeScript 文件中,并为其添加了 JSDoc 注释,使其成为一个可复用的工具函数。
通过这种方式,防抖函数不仅功能强大,还能在编译时提供类型检查,减少运行时的潜在错误。TypeScript
的类型系统帮助我们提升了代码的安全性和健壮性。
最后,我们给出完整的的代码如下:
// debounce.ts
export type DebouncedFunction<A extends any[], R> = {
(...args: A): R | undefined; // 防抖函数本身,返回值可能为 R 或 undefined
cancel: () => void; // `cancel` 方法,用于手动清除防抖
};
/**
* 创建一个防抖函数,确保在最后一次调用后,目标函数只会在指定的延迟时间后执行。
* 防抖函数可以防止某个函数被频繁调用,例如用户输入事件、滚动事件或窗口调整大小等场景。
*
* @template A - 函数接受的参数类型。
* @template R - 函数的返回值类型。
* @param {(...args: A) => R} func - 需要防抖的目标函数。该函数将在延迟时间后执行。
* @param {number} duration - 延迟时间(以毫秒为单位)。在这个时间内,如果再次调用函数,将重新计时。
* @returns {DebouncedFunction<A, R>} 一个防抖后的函数,该函数包括一个 `cancel` 方法用于清除防抖。
*
* @example
* const debouncedLog = debounce((message: string) => {
* console.log(message);
* return message;
* }, 300);
*
* debouncedLog("Hello"); // 300ms 后输出 "Hello"
* debouncedLog.cancel(); // 取消防抖,函数不会执行
*/
export function debounce<A extends any[], R>(
func: (...args: A) => R,
duration: number
): DebouncedFunction<A, R> {
let timer: ReturnType<typeof setTimeout> | null = null; // 定时器变量
let lastResult: R | undefined; // 存储目标函数的返回值
const debouncedFn = function (...args: A): R | undefined {
if (timer) {
clearTimeout(timer); // 清除之前的定时器
}
timer = setTimeout(() => {
lastResult = func(...args); // 延迟后执行目标函数,并存储返回值
}, duration);
return lastResult; // 返回上次执行的结果,如果尚未执行则返回 undefined
};
debouncedFn.cancel = function () {
if (timer) {
clearTimeout(timer); // 清除定时器,防止目标函数被执行
timer = null; // 重置定时器
}
};
return debouncedFn;
}
来源:juejin.cn/post/7431889821168812073