這篇文章主要介紹了在團(tuán)隊(duì)項(xiàng)目開發(fā)中使用 TypeScript 的價(jià)值,重點(diǎn)講解了如何用 TypeScript 實(shí)現(xiàn)一個(gè)類型安全的防抖函數(shù)。從函數(shù)框架、參數(shù)添加、定時(shí)器邏輯到使用泛型優(yōu)化、添加 cancel 方法和 JSDoc 注釋等步驟,詳細(xì)闡述了防抖函數(shù)的實(shí)現(xiàn)過程及優(yōu)點(diǎn),強(qiáng)調(diào)了其提升代碼安全性和健壯性的作用。
為什么要去使用 TypeScript ? 一直以來 TypeScript
的存在都備受爭議,很多人認(rèn)為他加重了前端開發(fā)的負(fù)擔(dān),特別是在它的嚴(yán)格類型系統(tǒng)和 JavaScript
的靈活性之間的矛盾上引發(fā)了不少討論。
支持者認(rèn)為 TypeScript
提供了強(qiáng)類型檢查、豐富的 IDE 支持和更好的代碼重構(gòu)能力,從而提高了大型項(xiàng)目的代碼質(zhì)量和可維護(hù)性。
然而,也有很多開發(fā)者認(rèn)為 TypeScript
加重了開發(fā)負(fù)擔(dān),帶來了不必要的復(fù)雜性 ,尤其是在小型項(xiàng)目或快速開發(fā)場景中,它的嚴(yán)格類型系統(tǒng)可能顯得過于繁瑣,限制了 JavaScript
本身的動態(tài)和自由特性
但是隨著項(xiàng)目規(guī)模的增大和團(tuán)隊(duì)協(xié)作的復(fù)雜性增加,TypeScript
的優(yōu)勢也更加明顯。因?yàn)槟悴豢赡苤竿麍F(tuán)隊(duì)中所有人的知識層次和開發(fā)習(xí)慣都達(dá)到同一水準(zhǔn)!你也不可能保證團(tuán)隊(duì)中的其他人都能夠完全正確的使用你封裝的組件、函數(shù)!
在大型項(xiàng)目中我們往往會封裝到很多工具函數(shù)、組件等等,我們不可能在使用到組件時(shí)跑去看這個(gè)組件的實(shí)現(xiàn)邏輯,而 TypeScript
的類型提示正好彌補(bǔ)了這一點(diǎn)。通過明確的類型注解,TypeScript
可以在代碼中直接提示每個(gè)組件的輸入輸出、參數(shù)類型和預(yù)期結(jié)果,讓開發(fā)者只需在 IDE 中懸停或查看提示信息,就能了解組件的用途和使用方式,而不需要翻閱具體實(shí)現(xiàn)邏輯。
這時(shí)你可能會說,使用 JSDoc
也能夠?qū)崿F(xiàn)類似的效果。的確,JSDoc
可以通過注釋的形式對函數(shù)、參數(shù)、返回值等信息進(jìn)行詳細(xì)描述,甚至可以生成文檔。
然而,JSDoc
依賴于開發(fā)者的自覺維護(hù) ,且其檢查和提示能力遠(yuǎn)不如 TypeScript
強(qiáng)大和全面。TypeScript
的類型系統(tǒng)是在編譯階段強(qiáng)制執(zhí)行的 ,這意味著所有類型定義都是真正的 “硬性約束” ,能在代碼運(yùn)行前捕獲錯(cuò)誤,而不僅僅是提示。
在實(shí)際開發(fā)中,JSDoc
的確能讓我們知道參數(shù)類型,但它只是一種 “約定” ,而不是真正的約束。這意味著,如果同事在使用工具函數(shù)時(shí)不小心寫錯(cuò)了類型,比如傳了字符串而不是數(shù)字,JSDoc
只能通過注釋告訴你正確的使用方法,卻無法在你出錯(cuò)時(shí)立即給出警告。
然而在 TypeScript
中,類型系統(tǒng)會在代碼編寫階段實(shí)時(shí)檢查 。比如,你定義的函數(shù)要求傳入數(shù)字類型的參數(shù),如果有人傳入了字符串,IDE 立刻會報(bào)錯(cuò)提醒你,防止錯(cuò)誤進(jìn)一步傳播。
所以,TypeScript 的價(jià)值就在于它提供了一層代碼保護(hù),讓代碼有了“硬約束”,團(tuán)隊(duì)在開發(fā)過程中更加節(jié)省心智負(fù)擔(dān),顯著提升開發(fā)體驗(yàn)和生產(chǎn)力,少出錯(cuò)、更高效。
接下來我們來使用 TypeScript
寫一個(gè)基礎(chǔ)的防抖函數(shù)作為示例。通過類型定義和參數(shù)注解,我們不僅能讓防抖函數(shù)更加通用且類型安全,還能充分利用 TypeScript
的類型檢查優(yōu)勢,從而提高代碼的可讀性和可維護(hù)性。
這樣的實(shí)現(xiàn)方式將有效地降低潛在的運(yùn)行時(shí)錯(cuò)誤,特別是在大型項(xiàng)目中,可以使團(tuán)隊(duì)成員之間的協(xié)作能夠更加順暢,并且避免一些低級問題。
?
功能點(diǎn)講解 防抖函數(shù)的主要功能是:在指定的延遲時(shí)間內(nèi),如果函數(shù)多次調(diào)用,只有最后一次調(diào)用會生效 。這一功能尤其適合優(yōu)化用戶輸入等高頻事件。
防抖函數(shù)的核心功能 函數(shù)執(zhí)行的延遲控制 :函數(shù)調(diào)用后不立即執(zhí)行,而是等待一段時(shí)間。如果在等待期間再次調(diào)用函數(shù),之前的等待會被取消,重新計(jì)時(shí)。
立即執(zhí)行選項(xiàng) :有時(shí)我們希望函數(shù)在第一次調(diào)用時(shí)立即執(zhí)行,然后在延遲時(shí)間內(nèi)避免再次調(diào)用。
取消功能 :我們還希望在某些情況下手動取消延遲執(zhí)行的函數(shù),比如當(dāng)頁面卸載或需要重新初始化時(shí)。
第一步:編寫函數(shù)框架 在開始封裝防抖函數(shù)之前,我們首先應(yīng)該想到的就是要寫一個(gè)函數(shù),假設(shè)這個(gè)函數(shù)名叫 debounce
。我們先創(chuàng)建它的基本框架:
typescript 代碼解讀 復(fù)制代碼function debounce () { // 函數(shù)的邏輯將在這里編寫 }
這一步非常簡單,先定義一個(gè)空函數(shù),這個(gè)函數(shù)就是我們的防抖函數(shù)。在后續(xù)步驟中,我們會逐步向這個(gè)函數(shù)中添加功能。
第二步:添加基本的參數(shù) 防抖函數(shù)的第一個(gè)功能是控制某個(gè)函數(shù)的執(zhí)行 ,因此,我們需要傳遞一個(gè)需要防抖的函數(shù) 。其次,防抖功能依賴于一個(gè)延遲時(shí)間 ,這意味著我們還需要添加一個(gè)用于設(shè)置延遲的參數(shù)。
讓我們擴(kuò)展一下 debounce
函數(shù),為它添加兩個(gè)基本的參數(shù):
func
:需要防抖的目標(biāo)函數(shù)。
duration
:防抖的延遲時(shí)間,單位是毫秒。
typescript 代碼解讀 復(fù)制代碼function debounce (func: Function , duration: number ) { // 函數(shù)的邏輯將在這里編寫 }
第三步:為防抖功能引入定時(shí)器邏輯 防抖的核心邏輯就是通過定時(shí)器 (setTimeout
),讓函數(shù)執(zhí)行延后。那么我們需要用一個(gè)變量來保存這個(gè)定時(shí)器,以便在函數(shù)多次調(diào)用時(shí)可以取消之前的定時(shí)器。
typescript 代碼解讀 復(fù)制代碼function debounce (func: Function , duration: number ) { let timer : ReturnType <typeof setTimeout > | null = null ; // 定時(shí)器變量 }
let timer: ReturnType<typeof setTimeout> | null = null
:我們使用一個(gè)變量 timer
來存儲定時(shí)器的返回值。
clearTimeout(timer)
:每次調(diào)用防抖函數(shù)時(shí),都會清除之前的定時(shí)器,這樣就保證了函數(shù)不會被立即執(zhí)行,直到等待時(shí)間結(jié)束。
setTimeout
:在指定的延遲時(shí)間后執(zhí)行傳入的目標(biāo)函數(shù) func
,并傳遞原始參數(shù)。
為什么寫成了 ReturnType<typeof setTimeout> | null
這樣的類型 ?
在 JavaScript
中,setTimeout
是一個(gè)內(nèi)置函數(shù),用來設(shè)置一個(gè)延遲執(zhí)行的任務(wù)。它的基本語法如下:
typescript 代碼解讀 復(fù)制代碼let id = setTimeout (() => { console .log ("Hello, world!" ); }, 1000 );
setTimeout
返回一個(gè)定時(shí)器 ID (在瀏覽器中是一個(gè)數(shù)字),這個(gè) ID 用來唯一標(biāo)識這個(gè)定時(shí)器。如果你想取消定時(shí)器,你可以使用 clearTimeout(id)
,其中 id
就是這個(gè)返回的定時(shí)器 ID。
ReturnType<T>
是 TypeScript 提供的一個(gè)工具類型 ,它的作用是幫助我們獲取某個(gè)函數(shù)類型的返回值類型 。我們通過泛型 T
來傳入一個(gè)函數(shù)類型 ,然后 ReturnType<T>
就會返回這個(gè)函數(shù)的返回值類型。在這里我們可以用它來獲取 setTimeout
函數(shù)的返回類型。
為什么需要使用 ReturnType<typeof setTimeout>
? 由于不同的 JavaScript
運(yùn)行環(huán)境中,setTimeout
的返回值類型是不同的:
為了兼容不同的環(huán)境,我們需要用 ReturnType<typeof setTimeout>
來動態(tài)獲取 setTimeout
返回的類型,而不是手動指定類型(比如 number
或 Timeout
)。
typescript 代碼解讀 復(fù)制代碼let timer : ReturnType <typeof setTimeout >;
這里 ReturnType<typeof setTimeout>
表示我們根據(jù) setTimeout
的返回值類型自動推導(dǎo)出變量 timer
的類型,不管是數(shù)字(瀏覽器)還是對象(Node.js),TypeScript 會自動處理。為什么需要設(shè)置聯(lián)合類型 | null
? 在我們的防抖函數(shù)實(shí)現(xiàn)中,定時(shí)器 timer
并不是一開始就設(shè)置好的。我們需要在每次調(diào)用防抖函數(shù)時(shí)動態(tài)設(shè)置定時(shí)器 ,所以初始狀態(tài)下,timer
的值應(yīng)該是 null
。 使用 | null
表示聯(lián)合類型 ,它允許 timer
變量既可以是 setTimeout
返回的值,也可以是 null
,表示目前還沒有設(shè)置定時(shí)器。
typescript 代碼解讀 復(fù)制代碼let timer : ReturnType <typeof setTimeout > | null = null ;
第四步:返回一個(gè)新函數(shù) 在防抖函數(shù) debounce
中,我們希望當(dāng)它被調(diào)用時(shí),返回一個(gè)新的函數(shù)。這是防抖函數(shù)的核心機(jī)制,因?yàn)槊看握{(diào)用返回的新函數(shù),實(shí)際上是在控制目標(biāo)函數(shù) func
的執(zhí)行。 具體的想法是這樣的:我們并不直接執(zhí)行傳入的目標(biāo)函數(shù) func
,而是返回一個(gè)新函數(shù),這個(gè)新函數(shù)在被調(diào)用時(shí)會受到防抖的控制。
因此,我們要修改 debounce
函數(shù),使它返回一個(gè)新的函數(shù),真正控制 func
的執(zhí)行時(shí)機(jī)。
typescript 代碼解讀 復(fù)制代碼function debounce (func: Function , duration: number ) { let timer : ReturnType <typeof setTimeout > | null = null ; // 定時(shí)器變量 return function () { // 防抖邏輯將在這里編寫 }; }
返回新函數(shù) :當(dāng) debounce
被調(diào)用時(shí),它返回一個(gè)新函數(shù)。這個(gè)新函數(shù)是每次調(diào)用時(shí)執(zhí)行防抖邏輯的入口。
為什么返回新函數(shù)? :因?yàn)槲覀冃枰诿看问录|發(fā)時(shí)(例如用戶輸入時(shí))執(zhí)行防抖操作,而不是直接執(zhí)行傳入的目標(biāo)函數(shù) func
。
第五步:清除之前的定時(shí)器 為了實(shí)現(xiàn)防抖功能,每次調(diào)用返回的新函數(shù)時(shí),我們需要先清除之前的定時(shí)器。如果之前有一個(gè)定時(shí)器在等待執(zhí)行目標(biāo)函數(shù),我們應(yīng)該將其取消,然后重新設(shè)置一個(gè)新的定時(shí)器。 這個(gè)步驟的關(guān)鍵就是使用 clearTimeout(timer)
。
typescript 代碼解讀 復(fù)制代碼function debounce (func: Function , duration: number ) { let timer : ReturnType <typeof setTimeout > | null = null ; // 定時(shí)器變量 return function () { if (timer) { clearTimeout (timer); // 清除之前的定時(shí)器 } // 下面將設(shè)置新的定時(shí)器 }; }
if (timer)
:我們檢查 timer
是否有值。如果它有值,說明之前的定時(shí)器還在等待執(zhí)行,我們需要將其清除。
clearTimeout(timer)
:這就是清除之前的定時(shí)器,防止之前的調(diào)用被執(zhí)行。這個(gè)操作非常關(guān)鍵,因?yàn)樗_保了只有最后一次調(diào)用(在延遲時(shí)間后)才會真正觸發(fā)目標(biāo)函數(shù)。
第六步:設(shè)置新的定時(shí)器 現(xiàn)在我們需要在每次調(diào)用返回的新函數(shù)時(shí),重新設(shè)置一個(gè)新的定時(shí)器 ,讓它在指定的延遲時(shí)間 duration
之后執(zhí)行目標(biāo)函數(shù) func
。 這時(shí)候就要使用 setTimeout
來設(shè)置定時(shí)器,并在延遲時(shí)間后執(zhí)行目標(biāo)函數(shù)。
typescript 代碼解讀 復(fù)制代碼function debounce (func: Function , duration: number ) { let timer : ReturnType <typeof setTimeout > | null = null ; // 定時(shí)器變量 return function () { if (timer) { clearTimeout (timer); // 清除之前的定時(shí)器 } timer = setTimeout (() => { func (); // 延遲后調(diào)用目標(biāo)函數(shù) }, duration); }; }
setTimeout
:我們使用 setTimeout
來設(shè)置一個(gè)新的定時(shí)器,定時(shí)器將在 duration
毫秒后執(zhí)行傳入的目標(biāo)函數(shù) func
。
func()
:這是目標(biāo)函數(shù)的實(shí)際執(zhí)行點(diǎn)。定時(shí)器到達(dá)延遲時(shí)間時(shí),它會執(zhí)行目標(biāo)函數(shù) func
。
timer = setTimeout(...)
:我們將定時(shí)器的 ID 存儲在 timer
變量中,以便后續(xù)可以使用 clearTimeout(timer)
來清除定時(shí)器。
第七步:支持參數(shù)傳遞 接下來是讓這個(gè)防抖函數(shù)能夠接受參數(shù),并將這些參數(shù)傳遞給目標(biāo)函數(shù) func
。 為了實(shí)現(xiàn)這個(gè)功能,我們需要用到 ...args
來捕獲所有傳入的參數(shù),并在執(zhí)行目標(biāo)函數(shù)時(shí)將這些參數(shù)傳遞過去。
typescript 代碼解讀 復(fù)制代碼function debounce (func: Function , duration: number ) { let timer : ReturnType <typeof setTimeout > | null = null ; // 定時(shí)器變量 return function (...args: any [] ) { // 接收傳入的參數(shù) if (timer) { clearTimeout (timer); // 清除之前的定時(shí)器 } timer = setTimeout (() => { func (...args); // 延遲后調(diào)用目標(biāo)函數(shù),并傳遞參數(shù) }, duration); }; }
...args: any[]
:這表示新函數(shù)可以接收任意數(shù)量的參數(shù),并將這些參數(shù)存儲在 args
數(shù)組中。
func(...args)
:當(dāng)定時(shí)器到達(dá)延遲時(shí)間后,調(diào)用目標(biāo)函數(shù) func
,并將 args
中的所有參數(shù)傳遞給它。這確保了目標(biāo)函數(shù)能接收到我們傳入的所有參數(shù)。
到這里,我們一個(gè)基本的防抖函數(shù)的實(shí)現(xiàn)。這個(gè)防抖函數(shù)實(shí)現(xiàn)了以下基本功能:
函數(shù)執(zhí)行的延遲控制 :每次調(diào)用時(shí),都重新設(shè)置定時(shí)器,確保函數(shù)不會立即執(zhí)行,而是在延遲結(jié)束后才執(zhí)行。
多參數(shù)支持 :通過 ...args
,防抖函數(shù)能夠接收多個(gè)參數(shù),并將它們傳遞給目標(biāo)函數(shù)。
清除之前的定時(shí)器 :在每次調(diào)用時(shí),如果定時(shí)器已經(jīng)存在,先清除之前的定時(shí)器,確保只有最后一次調(diào)用才會生效。
但是,這樣就完了嗎? 在當(dāng)前的實(shí)現(xiàn)中,debounce
函數(shù)的定義是 debounce(func: Function, duration: number)
,其中 func: Function
用來表示目標(biāo)函數(shù)。這種定義雖然可以工作,但它存在明顯的缺陷和不足之處,尤其是在 TypeScript 強(qiáng)調(diào)類型安全的情況下。
缺陷 1:缺乏參數(shù)類型檢查 Function
是一種非常寬泛的類型,它允許目標(biāo)函數(shù)接收任何類型、任意數(shù)量的參數(shù)。因此定義目標(biāo)函數(shù) func
為 Function
類型意味著 TypeScript 無法對目標(biāo)函數(shù)的 參數(shù)類型進(jìn)行任何檢查。
typescript 代碼解讀 復(fù)制代碼const debounced = debounce ((a: number , b: number ) => { console .log (a + b); }, 200 ); debounced ("hello" , "world" ); // 這里不會報(bào)錯(cuò),參數(shù)類型不匹配,但仍會被調(diào)用
在這個(gè)例子中,我們定義了一個(gè)目標(biāo)函數(shù),期望它接受兩個(gè)數(shù)字類型 的參數(shù),但在實(shí)際調(diào)用時(shí)卻傳入了兩個(gè)字符串。 這種情況下 TypeScript 不會提示任何錯(cuò)誤,因?yàn)?nbsp;Function
類型沒有對參數(shù)類型進(jìn)行限制。這種類型檢查的缺失可能導(dǎo)致運(yùn)行時(shí)錯(cuò)誤或者邏輯上的錯(cuò)誤。
缺陷 2:返回值類型不安全 同樣,定義 func
為 Function
類型時(shí),TypeScript 無法推斷目標(biāo)函數(shù)的返回值類型 。這意味著防抖函數(shù)不能保證目標(biāo)函數(shù)的返回值是符合預(yù)期的類型,可能導(dǎo)致返回值在其他地方被錯(cuò)誤使用。
typescript 代碼解讀 復(fù)制代碼const debounced = debounce (() => { return "result" ; }, 200 ); const result = debounced (); // TypeScript 不知道返回值類型,認(rèn)為是 undefined
在這個(gè)例子中,雖然目標(biāo)函數(shù)明確返回了一個(gè)字符串 "result"
,但 debounced
函數(shù)的返回值類型未被推斷出來,因此 TypeScript 會認(rèn)為它的返回值是 void
或 undefined
,即使目標(biāo)函數(shù)實(shí)際上返回了 string
。
缺陷 3:缺乏目標(biāo)函數(shù)的簽名限制 由于 Function
類型允許任何形式的函數(shù),因此 TypeScript 也無法檢查目標(biāo)函數(shù)的參數(shù)個(gè)數(shù)和類型是否匹配。這種情況下,如果防抖函數(shù)返回的新函數(shù)接收了錯(cuò)誤數(shù)量或類型的參數(shù),可能導(dǎo)致函數(shù)行為異常或意外的運(yùn)行時(shí)錯(cuò)誤。
typescript 代碼解讀 復(fù)制代碼const debounced = debounce ((a: number ) => { console .log (a); }, 200 ); debounced (1 , 2 , 3 ); // TypeScript 不會報(bào)錯(cuò),但多余的參數(shù)不會被使用
雖然目標(biāo)函數(shù)只期望接收一個(gè)參數(shù),但在調(diào)用時(shí)傳入了多個(gè)參數(shù)。TypeScript 不會進(jìn)行任何警告或報(bào)錯(cuò),因?yàn)?nbsp;Function
類型允許這種寬泛的調(diào)用,這可能會導(dǎo)致開發(fā)者誤以為這些參數(shù)被使用。
總結(jié) func: Function
的缺陷 缺乏參數(shù)類型檢查 :任何數(shù)量、任意類型的參數(shù)都可以傳遞給目標(biāo)函數(shù) ,導(dǎo)致潛在的參數(shù)類型錯(cuò)誤。
返回值類型不安全 :目標(biāo)函數(shù)的返回值類型無法被推斷 ,導(dǎo)致 TypeScript 無法確保返回值的類型正確。
函數(shù)簽名不受限制 :沒有對目標(biāo)函數(shù)的參數(shù)個(gè)數(shù)和類型進(jìn)行檢查 ,容易導(dǎo)致邏輯錯(cuò)誤或參數(shù)使用不當(dāng)。
這些缺陷使得代碼在類型安全性和健壯性上存在不足,可能導(dǎo)致運(yùn)行時(shí)錯(cuò)誤或者隱藏的邏輯漏洞。
下一步的改進(jìn) 為了解決這些缺陷,我們可以通過泛型 的方式為目標(biāo)函數(shù)添加類型限制,確保目標(biāo)函數(shù)的參數(shù)和返回值類型都能被準(zhǔn)確地推斷和檢查。這會是我們接下來要進(jìn)行的優(yōu)化。
第八步:使用泛型優(yōu)化 為了克服 func: Function
帶來的缺陷,我們可以通過 泛型 來優(yōu)化防抖函數(shù)的類型定義,確保目標(biāo)函數(shù)的參數(shù)和返回值都能在編譯時(shí)進(jìn)行類型檢查。使用泛型不僅可以解決參數(shù)類型和返回值類型的檢查問題,還可以提升代碼的靈活性和安全性。
如何使用泛型進(jìn)行優(yōu)化? 我們將通過引入兩個(gè)泛型參數(shù)來改進(jìn)防抖函數(shù)的類型定義:
A
:表示目標(biāo)函數(shù)的參數(shù)類型 ,可以是任意類型和數(shù)量的參數(shù),確保防抖函數(shù)在接收參數(shù)時(shí)能進(jìn)行類型檢查。
R
:表示目標(biāo)函數(shù)的返回值類型 ,確保防抖函數(shù)返回的值與目標(biāo)函數(shù)一致。
typescript 代碼解讀 復(fù)制代碼function debounce<A extends any [], R>( func : (...args: A ) => R, // 使用泛型 A 表示參數(shù),R 表示返回值類型 duration : number // 延遲時(shí)間,以毫秒為單位 ): (...args: A ) => R { // 返回新函數(shù),參數(shù)類型與目標(biāo)函數(shù)相同,返回值類型為 R let timer : ReturnType <typeof setTimeout > | null = null ; // 定時(shí)器變量 let lastResult : R; // 存儲目標(biāo)函數(shù)的返回值 return function (...args: A ): R { // 返回的新函數(shù),參數(shù)類型由 A 推斷 if (timer) { clearTimeout (timer); // 清除之前的定時(shí)器 } timer = setTimeout (() => { lastResult = func (...args); // 延遲后調(diào)用目標(biāo)函數(shù),并存儲返回值 }, duration); return lastResult; // 返回上一次執(zhí)行的結(jié)果,如果尚未執(zhí)行則返回 undefined }; }
A extends any[]
:A
表示目標(biāo)函數(shù)的參數(shù)類型,A
是一個(gè)數(shù)組類型,能夠適應(yīng)目標(biāo)函數(shù)接收多個(gè)參數(shù)的場景。通過泛型,防抖函數(shù)能夠根據(jù)目標(biāo)函數(shù)的簽名推斷出參數(shù)類型并進(jìn)行檢查。
R
:R
表示目標(biāo)函數(shù)的返回值類型,防抖函數(shù)能夠確保返回值類型與目標(biāo)函數(shù)一致。如果目標(biāo)函數(shù)返回值類型為 string
,防抖函數(shù)也會返回 string
,這樣可以防止返回值類型不匹配。
lastResult
:用來存儲目標(biāo)函數(shù)的最后一次返回值。每次調(diào)用目標(biāo)函數(shù)時(shí)會更新 lastResult
,并在調(diào)用時(shí)返回上一次執(zhí)行的結(jié)果,確保防抖函數(shù)返回正確的返回值。
泛型優(yōu)化后的優(yōu)點(diǎn) :
類型安全的參數(shù)傳遞 : 通過泛型 A
,防抖函數(shù)可以根據(jù)目標(biāo)函數(shù)的簽名進(jìn)行類型檢查,確保傳入的參數(shù)與目標(biāo)函數(shù)一致,避免參數(shù)類型錯(cuò)誤。
typescript 代碼解讀 復(fù)制代碼const debounced1 = debounce ((a: number , b: string ) => { console .log (a, b); }, 300 ); debounced1 (42 , "hello" ); // 正確,參數(shù)類型匹配 debounced1 ("42" , 42 ); // 錯(cuò)誤,類型不匹配
返回值類型安全 : 泛型 R
確保了防抖函數(shù)的返回值與目標(biāo)函數(shù)的返回值類型一致,防止不匹配的類型被返回。
typescript 代碼解讀 復(fù)制代碼const debounced = debounce (() => { return "result" ; }, 200 ); const result = debounced (); // 返回值為 string console .log (result); // 輸出 "result"
支持多參數(shù)傳遞 : 泛型 A
表示參數(shù)類型數(shù)組,這意味著目標(biāo)函數(shù)可以接收多個(gè)參數(shù),防抖函數(shù)會將這些參數(shù)正確傳遞給目標(biāo)函數(shù)。而如果防抖函數(shù)返回的新函數(shù)接收了錯(cuò)誤數(shù)量或類型的參數(shù),會直接報(bào)錯(cuò)提示。
typescript 代碼解讀 復(fù)制代碼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
方法并處理返回值類型 在前面的步驟中,我們已經(jīng)實(shí)現(xiàn)了一個(gè)可以延遲執(zhí)行的防抖函數(shù),并且支持參數(shù)傳遞和返回目標(biāo)函數(shù)的結(jié)果。 但是,由于防抖函數(shù)的執(zhí)行是異步延遲 的,因此在初次調(diào)用時(shí),防抖函數(shù)可能無法立即返回結(jié)果 。因此函數(shù)的返回值我們需要使用 undefined
來表示目標(biāo)函數(shù)的返回結(jié)果可能出現(xiàn)還沒生成的情況。 除此之外,我們還要為防抖函數(shù)添加一個(gè) cancel
方法,用于手動取消防抖的延遲執(zhí)行。為什么需要 cancel
方法? 在一些場景下,可能需要手動取消防抖操作,例如:
為了解決這些需求,cancel
方法可以幫助我們在定時(shí)器還未觸發(fā)時(shí),清除定時(shí)器并停止目標(biāo)函數(shù)的執(zhí)行。
typescript 代碼解讀 復(fù)制代碼// 定義帶有 cancel 方法的防抖函數(shù)類型 type DebouncedFunction <A extends any [], R> = { (...args : A): R | undefined ; // 防抖函數(shù)本身,返回值可能為 R 或 undefined cancel : () => void ; // `cancel` 方法,用于手動清除防抖 }; // 實(shí)現(xiàn)防抖函數(shù) function debounce<A extends any [], R>( func : (...args: A ) => R, // 泛型 A 表示參數(shù)類型,R 表示返回值類型 duration : number // 延遲時(shí)間 ): DebouncedFunction <A, R> { // 返回帶有 cancel 方法的防抖函數(shù) let timer : ReturnType <typeof setTimeout > | null = null ; // 定時(shí)器變量 let lastResult : R | undefined ; // 用于存儲目標(biāo)函數(shù)的返回值 // 防抖邏輯的核心函數(shù) const debouncedFn = function (...args: A ): R | undefined { if (timer) { clearTimeout (timer); // 清除之前的定時(shí)器 } // 設(shè)置新的定時(shí)器 timer = setTimeout (() => { lastResult = func (...args); // 延遲后執(zhí)行目標(biāo)函數(shù),并存儲返回值 }, duration); // 返回上一次的結(jié)果或 undefined return lastResult; }; // 添加 `cancel` 方法,用于手動取消防抖 debouncedFn.cancel = function () { if (timer) { clearTimeout (timer); // 清除定時(shí)器 timer = null ; // 重置定時(shí)器 } }; return debouncedFn; // 返回帶有 `cancel` 方法的防抖函數(shù) }
返回值類型 R | undefined
:
R
:代表目標(biāo)函數(shù)的返回值類型,例如 number
或 string
。
undefined
:在防抖函數(shù)的首次調(diào)用或目標(biāo)函數(shù)尚未執(zhí)行時(shí),返回 undefined
,表示結(jié)果尚未生成。
lastResult
用于存儲目標(biāo)函數(shù)上一次執(zhí)行的結(jié)果,防抖函數(shù)在每次調(diào)用時(shí)會返回該結(jié)果,或者在尚未執(zhí)行時(shí)返回 undefined
。
cancel
方法 :
讓我們來看一個(gè)具體的使用示例,展示如何使用防抖函數(shù),并在需要時(shí)手動取消操作。
typescript 代碼解讀 復(fù)制代碼// 定義一個(gè)簡單的目標(biāo)函數(shù) const debouncedLog = debounce ((message: string ) => { console .log (message); return message; }, 300 ); // 第一次調(diào)用防抖函數(shù),目標(biāo)函數(shù)將在 300 毫秒后執(zhí)行 debouncedLog ("Hello" ); // 如果不取消,300ms 后會輸出 "Hello" // 手動取消防抖,目標(biāo)函數(shù)不會執(zhí)行 debouncedLog.cancel ();
在這個(gè)示例中:
調(diào)用 debouncedLog("Hello")
:會啟動一個(gè) 300 毫秒的延遲執(zhí)行,目標(biāo)函數(shù)計(jì)劃在 300 毫秒后執(zhí)行,并輸出 "Hello"
。
調(diào)用 debouncedLog.cancel()
:會清除定時(shí)器,目標(biāo)函數(shù)不會執(zhí)行,避免了不必要的操作。
第十步:將防抖函數(shù)作為工具函數(shù)單獨(dú)放在一個(gè) ts
文件中并添加 JSDoc 注釋 在編寫好防抖函數(shù)之后,下一步是將其作為一個(gè)工具函數(shù)放入單獨(dú)的 .ts
文件中,以便在項(xiàng)目中重復(fù)使用。同時(shí),我們可以為函數(shù)添加詳細(xì)的 JSDoc 注釋 ,方便使用者了解函數(shù)的作用、參數(shù)、返回值及用法。
1. 將防抖函數(shù)放入單獨(dú)的文件 首先,我們可以創(chuàng)建一個(gè)名為 debounce.ts
的文件,并將防抖函數(shù)的代碼放在其中。
typescript 代碼解讀 復(fù)制代碼// debounce.ts export type DebouncedFunction <A extends any [], R> = { (...args : A): R | undefined ; // 防抖函數(shù)本身,返回值可能為 R 或 undefined cancel : () => void ; // `cancel` 方法,用于手動清除防抖 }; /** * 創(chuàng)建一個(gè)防抖函數(shù),確保在最后一次調(diào)用后,目標(biāo)函數(shù)只會在指定的延遲時(shí)間后執(zhí)行。 * 防抖函數(shù)可以防止某個(gè)函數(shù)被頻繁調(diào)用,例如用戶輸入事件、滾動事件或窗口調(diào)整大小等場景。 * * @template A - 函數(shù)接受的參數(shù)類型。 * @template R - 函數(shù)的返回值類型。 * @param {(...args: A) => R } func - 需要防抖的目標(biāo)函數(shù)。該函數(shù)將在延遲時(shí)間后執(zhí)行。 * @param {number } duration - 延遲時(shí)間(以毫秒為單位)。在這個(gè)時(shí)間內(nèi),如果再次調(diào)用函數(shù),將重新計(jì)時(shí)。 * @returns {DebouncedFunction<A, R> } 一個(gè)防抖后的函數(shù),該函數(shù)包括一個(gè) `cancel` 方法用于清除防抖。 * * @example * const debouncedLog = debounce((message: string) => { * console.log(message); * return message; * }, 300); * * debouncedLog("Hello"); // 300ms 后輸出 "Hello" * debouncedLog.cancel(); // 取消防抖,函數(shù)不會執(zhí)行 */ export function debounce<A extends any [], R>( func : (...args: A ) => R, duration : number ): DebouncedFunction <A, R> { let timer : ReturnType <typeof setTimeout > | null = null ; // 定時(shí)器變量 let lastResult : R | undefined ; // 存儲目標(biāo)函數(shù)的返回值 const debouncedFn = function (...args: A ): R | undefined { if (timer) { clearTimeout (timer); // 清除之前的定時(shí)器 } timer = setTimeout (() => { lastResult = func (...args); // 延遲后執(zhí)行目標(biāo)函數(shù),并存儲返回值 }, duration); return lastResult; // 返回上次執(zhí)行的結(jié)果,如果尚未執(zhí)行則返回 undefined }; debouncedFn.cancel = function () { if (timer) { clearTimeout (timer); // 清除定時(shí)器,防止目標(biāo)函數(shù)被執(zhí)行 timer = null ; // 重置定時(shí)器 } }; return debouncedFn; }
2. 詳細(xì)的 JSDoc 注釋說明 通過添加 JSDoc 注釋,能夠?yàn)楹瘮?shù)使用者提供清晰的文檔信息,說明防抖函數(shù)的功能、參數(shù)類型、返回值類型,以及如何使用它。JSDoc 注釋的結(jié)構(gòu)說明 :
@template A, R
:說明泛型 A
是函數(shù)接受的參數(shù)類型,R
是目標(biāo)函數(shù)的返回值類型。
@param
:解釋函數(shù)的輸入?yún)?shù),說明 func
是目標(biāo)函數(shù),duration
是防抖的延遲時(shí)間。
@returns
:說明返回值是一個(gè)帶有 cancel
方法的防抖函數(shù),函數(shù)返回值類型是 R | undefined
。
@example
:為函數(shù)提供示例,展示防抖函數(shù)的典型用法,包括取消防抖操作。
使用 JSDoc 生成文檔 通過在 .ts
文件中添加 JSDoc 注釋,可以借助 TypeScript 編輯器或 IDE(如 VSCode/Webstorm)自動生成代碼提示和函數(shù)文檔說明,提升開發(fā)體驗(yàn)。 例如,當(dāng)開發(fā)者在使用 debounce
函數(shù)時(shí),可以自動看到函數(shù)的說明和參數(shù)類型提示:
回顧:泛型防抖函數(shù)的最終效果 通過前面各個(gè)步驟的優(yōu)化,我們已經(jīng)構(gòu)建了一個(gè)類型安全的防抖函數(shù),結(jié)合泛型實(shí)現(xiàn)了以下關(guān)鍵功能:
類型安全的參數(shù)傳遞 : 通過泛型 A
,防抖函數(shù)能夠根據(jù)目標(biāo)函數(shù)的簽名進(jìn)行參數(shù)類型檢查,確保傳入的參數(shù)與目標(biāo)函數(shù)的類型一致。如果傳入的參數(shù)類型不匹配,TypeScript 將在編譯時(shí)報(bào)錯(cuò),避免運(yùn)行時(shí)的潛在錯(cuò)誤。
typescript 代碼解讀 復(fù)制代碼const debounced1 = debounce ((a: number , b: string ) => { console .log (a, b); }, 300 ); debounced1 (42 , "hello" ); // 正確,參數(shù)類型匹配 debounced1 ("42" , 42 ); // 錯(cuò)誤,類型不匹配
在上面的例子中,TypeScript 會檢查參數(shù)類型,確保傳入的參數(shù)符合預(yù)期的類型。錯(cuò)誤的參數(shù)類型會被及時(shí)捕捉。
返回值類型安全 : 泛型 R
確保防抖函數(shù)的返回值與目標(biāo)函數(shù)的返回值類型保持一致。TypeScript 可以根據(jù)目標(biāo)函數(shù)的返回值類型推斷防抖函數(shù)的返回值,防止不匹配的類型被返回。
typescript 代碼解讀 復(fù)制代碼const debounced = debounce (() => { return "result" ; }, 200 ); const result = debounced (); // 返回值為 string console .log (result); // 輸出 "result"
在這個(gè)例子中,debounce
返回的防抖函數(shù)的返回值類型為 string
或者 undefind
,因?yàn)樵诜蓝逗瘮?shù)的實(shí)現(xiàn)中,目標(biāo)函數(shù)是延遲執(zhí)行的,因此在初次調(diào)用 或在延遲期間 ,debounced
函數(shù)返回的結(jié)果可能尚未生成,與目標(biāo)函數(shù)的返回值類型預(yù)期一致。
支持多參數(shù)傳遞 : 泛型 A
表示目標(biāo)函數(shù)的參數(shù)類型數(shù)組,這意味著防抖函數(shù)可以正確傳遞多個(gè)參數(shù),并確保類型安全。如果傳入了錯(cuò)誤數(shù)量或類型的參數(shù),TypeScript 會提示開發(fā)者進(jìn)行修正。
typescript 代碼解讀 復(fù)制代碼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."
在這個(gè)例子中,防抖函數(shù)正確地將多個(gè)參數(shù)傳遞給目標(biāo)函數(shù),并輸出目標(biāo)函數(shù)的正確返回值。傳入的參數(shù)數(shù)量或類型不正確時(shí),TypeScript 會發(fā)出報(bào)錯(cuò)提示。
總結(jié) 至此,我 們完整實(shí)現(xiàn)并優(yōu)化了一個(gè)類型安全的防抖函數(shù),并通過泛型確保參數(shù)和返回值的類型安全。此外,我們還詳細(xì)講解了如何為防抖函數(shù)添加 cancel
方法,并處理延遲執(zhí)行的返回值 R | undefined
。最后,我們將防抖函數(shù)封裝在一個(gè)單獨(dú)的 TypeScript 文件中,并為其添加了 JSDoc 注釋,使其成為一個(gè)可復(fù)用的工具函數(shù)。
通過這種方式,防抖函數(shù)不僅功能強(qiáng)大,還能在編譯時(shí)提供類型檢查,減少運(yùn)行時(shí)的潛在錯(cuò)誤。TypeScript
的類型系統(tǒng)幫助我們提升了代碼的安全性和健壯性。 最后,我們給出完整的的代碼如下:
// debounce.ts
export type DebouncedFunction<A extends any[], R> = {
(...args: A): R | undefined; // 防抖函數(shù)本身,返回值可能為 R 或 undefined
cancel: () => void; // `cancel` 方法,用于手動清除防抖
};
/**
* 創(chuàng)建一個(gè)防抖函數(shù),確保在最后一次調(diào)用后,目標(biāo)函數(shù)只會在指定的延遲時(shí)間后執(zhí)行。
* 防抖函數(shù)可以防止某個(gè)函數(shù)被頻繁調(diào)用,例如用戶輸入事件、滾動事件或窗口調(diào)整大小等場景。
*
* @template A - 函數(shù)接受的參數(shù)類型。
* @template R - 函數(shù)的返回值類型。
* @param {(...args: A) => R} func - 需要防抖的目標(biāo)函數(shù)。該函數(shù)將在延遲時(shí)間后執(zhí)行。
* @param {number} duration - 延遲時(shí)間(以毫秒為單位)。在這個(gè)時(shí)間內(nèi),如果再次調(diào)用函數(shù),將重新計(jì)時(shí)。
* @returns {DebouncedFunction<A, R>} 一個(gè)防抖后的函數(shù),該函數(shù)包括一個(gè) `cancel` 方法用于清除防抖。
*
* @example
* const debouncedLog = debounce((message: string) => {
* console.log(message);
* return message;
* }, 300);
*
* debouncedLog("Hello"); // 300ms 后輸出 "Hello"
* debouncedLog.cancel(); // 取消防抖,函數(shù)不會執(zhí)行
*/
export function debounce<A extends any[], R>(
func: (...args: A) => R,
duration: number
): DebouncedFunction<A, R> {
let timer: ReturnType<typeof setTimeout> | null = null; // 定時(shí)器變量
let lastResult: R | undefined; // 存儲目標(biāo)函數(shù)的返回值
const debouncedFn = function (...args: A): R | undefined {
if (timer) {
clearTimeout(timer); // 清除之前的定時(shí)器
}
timer = setTimeout(() => {
lastResult = func(...args); // 延遲后執(zhí)行目標(biāo)函數(shù),并存儲返回值
}, duration);
return lastResult; // 返回上次執(zhí)行的結(jié)果,如果尚未執(zhí)行則返回 undefined
};
debouncedFn.cancel = function () {
if (timer) {
clearTimeout(timer); // 清除定時(shí)器,防止目標(biāo)函數(shù)被執(zhí)行
timer = null; // 重置定時(shí)器
}
};
return debouncedFn;
}
該文章在 2024/11/18 17:52:59 編輯過