JavaScript 在處理函數(shù)時(shí)提供了非凡的靈活性。它們可以被傳遞,用作對象,現(xiàn)在我們將看到如何在它們之間 轉(zhuǎn)發(fā)(forward) 調(diào)用并 裝飾(decorate) 它們。
假設(shè)我們有一個(gè) CPU 重負(fù)載的函數(shù) slow(x)
,但它的結(jié)果是穩(wěn)定的。換句話說,對于相同的 x
,它總是返回相同的結(jié)果。
如果經(jīng)常調(diào)用該函數(shù),我們可能希望將結(jié)果緩存(記住)下來,以避免在重新計(jì)算上花費(fèi)額外的時(shí)間。
但是我們不是將這個(gè)功能添加到 slow()
中,而是創(chuàng)建一個(gè)包裝器(wrapper)函數(shù),該函數(shù)增加了緩存功能。正如我們將要看到的,這樣做有很多好處。
下面是代碼和解釋:
function slow(x) {
// 這里可能會(huì)有重負(fù)載的 CPU 密集型工作
alert(`Called with ${x}`);
return x;
}
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) { // 如果緩存中有對應(yīng)的結(jié)果
return cache.get(x); // 從緩存中讀取結(jié)果
}
let result = func(x); // 否則就調(diào)用 func
cache.set(x, result); // 然后將結(jié)果緩存(記?。┫聛? return result;
};
}
slow = cachingDecorator(slow);
alert( slow(1) ); // slow(1) 被緩存下來了,并返回結(jié)果
alert( "Again: " + slow(1) ); // 返回緩存中的 slow(1) 的結(jié)果
alert( slow(2) ); // slow(2) 被緩存下來了,并返回結(jié)果
alert( "Again: " + slow(2) ); // 返回緩存中的 slow(2) 的結(jié)果
在上面的代碼中,cachingDecorator
是一個(gè) 裝飾器(decorator):一個(gè)特殊的函數(shù),它接受另一個(gè)函數(shù)并改變它的行為。
其思想是,我們可以為任何函數(shù)調(diào)用 cachingDecorator
,它將返回緩存包裝器。這很棒啊,因?yàn)槲覀冇泻芏嗪瘮?shù)可以使用這樣的特性,而我們需要做的就是將 cachingDecorator
應(yīng)用于它們。
通過將緩存與主函數(shù)代碼分開,我們還可以使主函數(shù)代碼變得更簡單。
cachingDecorator(func)
的結(jié)果是一個(gè)“包裝器”:function(x)
將 func(x)
的調(diào)用“包裝”到緩存邏輯中:
從外部代碼來看,包裝的 slow
函數(shù)執(zhí)行的仍然是與之前相同的操作。它只是在其行為上添加了緩存功能。
總而言之,使用分離的 cachingDecorator
而不是改變 slow
本身的代碼有幾個(gè)好處:
cachingDecorator
? 是可重用的。我們可以將它應(yīng)用于另一個(gè)函數(shù)。slow
? 本身的復(fù)雜性(如果有的話)。上面提到的緩存裝飾器不適用于對象方法。
例如,在下面的代碼中,?worker.slow()
? 在裝飾后停止工作:
// 我們將對 worker.slow 的結(jié)果進(jìn)行緩存
let worker = {
someMethod() {
return 1;
},
slow(x) {
// 可怕的 CPU 過載任務(wù)
alert("Called with " + x);
return x * this.someMethod(); // (*)
}
};
// 和之前例子中的代碼相同
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) {
return cache.get(x);
}
let result = func(x); // (**)
cache.set(x, result);
return result;
};
}
alert( worker.slow(1) ); // 原始方法有效
worker.slow = cachingDecorator(worker.slow); // 現(xiàn)在對其進(jìn)行緩存
alert( worker.slow(2) ); // 蛤!Error: Cannot read property 'someMethod' of undefined
錯(cuò)誤發(fā)生在試圖訪問 this.someMethod
并失敗了的 (*)
行中。你能看出來為什么嗎?
原因是包裝器將原始函數(shù)調(diào)用為 (**)
行中的 func(x)
。并且,當(dāng)這樣調(diào)用時(shí),函數(shù)將得到 this = undefined
。
如果嘗試運(yùn)行下面這段代碼,我們會(huì)觀察到類似的問題:
let func = worker.slow;
func(2);
因此,包裝器將調(diào)用傳遞給原始方法,但沒有上下文 this
。因此,發(fā)生了錯(cuò)誤。
讓我們來解決這個(gè)問題。
有一個(gè)特殊的內(nèi)建函數(shù)方法 func.call(context, …args),它允許調(diào)用一個(gè)顯式設(shè)置 ?this
? 的函數(shù)。
語法如下:
func.call(context, arg1, arg2, ...)
它運(yùn)行 func
,提供的第一個(gè)參數(shù)作為 this
,后面的作為參數(shù)(arguments)。
簡單地說,這兩個(gè)調(diào)用幾乎相同:
func(1, 2, 3);
func.call(obj, 1, 2, 3)
它們調(diào)用的都是 func
,參數(shù)是 1
、2
和 3
。唯一的區(qū)別是 func.call
還會(huì)將 this
設(shè)置為 obj
。
例如,在下面的代碼中,我們在不同對象的上下文中調(diào)用 sayHi
:sayHi.call(user)
運(yùn)行 sayHi
并提供了 this=user
,然后下一行設(shè)置 this=admin
:
function sayHi() {
alert(this.name);
}
let user = { name: "John" };
let admin = { name: "Admin" };
// 使用 call 將不同的對象傳遞為 "this"
sayHi.call( user ); // John
sayHi.call( admin ); // Admin
在這里我們用帶有給定上下文和 phrase 的 call
調(diào)用 say
:
function say(phrase) {
alert(this.name + ': ' + phrase);
}
let user = { name: "John" };
// user 成為 this,"Hello" 成為第一個(gè)參數(shù)
say.call( user, "Hello" ); // John: Hello
在我們的例子中,我們可以在包裝器中使用 call
將上下文傳遞給原始函數(shù):
let worker = {
someMethod() {
return 1;
},
slow(x) {
alert("Called with " + x);
return x * this.someMethod(); // (*)
}
};
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) {
return cache.get(x);
}
let result = func.call(this, x); // 現(xiàn)在 "this" 被正確地傳遞了
cache.set(x, result);
return result;
};
}
worker.slow = cachingDecorator(worker.slow); // 現(xiàn)在對其進(jìn)行緩存
alert( worker.slow(2) ); // 工作正常
alert( worker.slow(2) ); // 工作正常,沒有調(diào)用原始函數(shù)(使用的緩存)
現(xiàn)在一切都正常工作了。
為了讓大家理解地更清晰一些,讓我們更深入地看看 ?this
? 是如何被傳遞的:
worker.slow
? 現(xiàn)在是包裝器 ?function (x) { ... }
?。worker.slow(2)
? 執(zhí)行時(shí),包裝器將 ?2
? 作為參數(shù),并且 ?this=worker
?(它是點(diǎn)符號(hào) ?.
? 之前的對象)。func.call(this, x)
? 將當(dāng)前的 ?this
?(?=worker
?)和當(dāng)前的參數(shù)(?=2
?)傳遞給原始方法。現(xiàn)在讓我們把 ?cachingDecorator
? 寫得更加通用。到現(xiàn)在為止,它只能用于單參數(shù)函數(shù)。
現(xiàn)在如何緩存多參數(shù) ?worker.slow
? 方法呢?
let worker = {
slow(min, max) {
return min + max; // scary CPU-hogger is assumed
}
};
// 應(yīng)該記住相同參數(shù)的調(diào)用
worker.slow = cachingDecorator(worker.slow);
之前,對于單個(gè)參數(shù) x
,我們可以只使用 cache.set(x, result)
來保存結(jié)果,并使用 cache.get(x)
來檢索并獲取結(jié)果。但是現(xiàn)在,我們需要記住 參數(shù)組合 (min,max)
的結(jié)果。原生的 Map
僅將單個(gè)值作為鍵(key)。
這兒有許多解決方案可以實(shí)現(xiàn):
cache.set(min)
? 將是一個(gè)存儲(chǔ)(鍵值)對 ?(max, result)
? 的 ?Map
?。所以我們可以使用 ?cache.get(min).get(max)
? 來獲取 ?result
?。對于許多實(shí)際應(yīng)用,第三種方式就足夠了,所以我們就用這個(gè)吧。
當(dāng)然,我們需要傳入的不僅是 x
,還需要傳入 func.call
的所有參數(shù)。讓我們回想一下,在 function()
中我們可以得到一個(gè)包含所有參數(shù)的偽數(shù)組(pseudo-array)arguments
,那么 func.call(this, x)
應(yīng)該被替換為 func.call(this, ...arguments)
。
這是一個(gè)更強(qiáng)大的 cachingDecorator
:
let worker = {
slow(min, max) {
alert(`Called with ${min},${max}`);
return min + max;
}
};
function cachingDecorator(func, hash) {
let cache = new Map();
return function() {
let key = hash(arguments); // (*)
if (cache.has(key)) {
return cache.get(key);
}
let result = func.call(this, ...arguments); // (**)
cache.set(key, result);
return result;
};
}
function hash(args) {
return args[0] + ',' + args[1];
}
worker.slow = cachingDecorator(worker.slow, hash);
alert( worker.slow(3, 5) ); // works
alert( "Again " + worker.slow(3, 5) ); // same (cached)
現(xiàn)在這個(gè)包裝器可以處理任意數(shù)量的參數(shù)了(盡管哈希函數(shù)還需要被進(jìn)行調(diào)整以允許任意數(shù)量的參數(shù)。一種有趣的處理方法將在下面講到)。
這里有兩個(gè)變化:
(*)
? 行中它調(diào)用 ?hash
? 來從 ?arguments
? 創(chuàng)建一個(gè)單獨(dú)的鍵。這里我們使用一個(gè)簡單的“連接”函數(shù),將參數(shù) ?(3, 5)
? 轉(zhuǎn)換為鍵 ?"3,5"
?。更復(fù)雜的情況可能需要其他哈希函數(shù)。(**)
? 行使用 ?func.call(this, ...arguments)
? 將包裝器獲得的上下文和所有參數(shù)(不僅僅是第一個(gè)參數(shù))傳遞給原始函數(shù)。我們可以使用 ?func.apply(this, arguments)
? 代替 ?func.call(this, ...arguments)
?。
內(nèi)建方法 func.apply 的語法是:
func.apply(context, args)
它運(yùn)行 func
設(shè)置 this=context
,并使用類數(shù)組對象 args
作為參數(shù)列表(arguments)。
call
和 apply
之間唯一的語法區(qū)別是,call
期望一個(gè)參數(shù)列表,而 apply
期望一個(gè)包含這些參數(shù)的類數(shù)組對象。
因此,這兩個(gè)調(diào)用幾乎是等效的:
func.call(context, ...args);
func.apply(context, args);
它們使用給定的上下文和參數(shù)執(zhí)行相同的 func
調(diào)用。
只有一個(gè)關(guān)于 args
的細(xì)微的差別:
...
? 允許將 可迭代對象 ?args
? 作為列表傳遞給 ?call
?。apply
? 只接受 類數(shù)組 ?args
?。……對于即可迭代又是類數(shù)組的對象,例如一個(gè)真正的數(shù)組,我們使用 call
或 apply
均可,但是 apply
可能會(huì)更快,因?yàn)榇蠖鄶?shù) JavaScript 引擎在內(nèi)部對其進(jìn)行了優(yōu)化。
將所有參數(shù)連同上下文一起傳遞給另一個(gè)函數(shù)被稱為“呼叫轉(zhuǎn)移(call forwarding)”。
這是它的最簡形式:
let wrapper = function() {
return func.apply(this, arguments);
};
當(dāng)外部代碼調(diào)用這種包裝器 wrapper
時(shí),它與原始函數(shù) func
的調(diào)用是無法區(qū)分的。
現(xiàn)在,讓我們對哈希函數(shù)再做一個(gè)較小的改進(jìn):
function hash(args) {
return args[0] + ',' + args[1];
}
截至目前,它僅適用于兩個(gè)參數(shù)。如果它可以適用于任何數(shù)量的 args
就更好了。
自然的解決方案是使用 arr.join 方法:
function hash(args) {
return args.join();
}
……不幸的是,這不行。因?yàn)槲覀冋谡{(diào)用 hash(arguments)
,arguments
對象既是可迭代對象又是類數(shù)組對象,但它并不是真正的數(shù)組。
所以在它上面調(diào)用 join
會(huì)失敗,我們可以在下面看到:
function hash() {
alert( arguments.join() ); // Error: arguments.join is not a function
}
hash(1, 2);
不過,有一種簡單的方法可以使用數(shù)組的 join 方法:
function hash() {
alert( [].join.call(arguments) ); // 1,2
}
hash(1, 2);
這個(gè)技巧被稱為 方法借用(method borrowing)。
我們從常規(guī)數(shù)組 [].join
中獲取(借用)join 方法,并使用 [].join.call
在 arguments
的上下文中運(yùn)行它。
它為什么有效?
那是因?yàn)樵椒?nbsp;arr.join(glue)
的內(nèi)部算法非常簡單。
從規(guī)范中幾乎“按原樣”解釋如下:
glue
? 成為第一個(gè)參數(shù),如果沒有參數(shù),則使用逗號(hào) ?","
?。result
? 為空字符串。this[0]
? 附加到 ?result
?。glue
? 和 ?this[1]
?。glue
? 和 ?this[2]
?。this.length
? 項(xiàng)目被粘在一起。result
?。因此,從技術(shù)上講,它需要 this
并將 this[0]
,this[1]
……等 join 在一起。它的編寫方式是故意允許任何類數(shù)組的 this
的(不是巧合,很多方法都遵循這種做法)。這就是為什么它也可以和 this=arguments
一起使用。
通常,用裝飾的函數(shù)替換一個(gè)函數(shù)或一個(gè)方法是安全的,除了一件小東西。如果原始函數(shù)有屬性,例如 func.calledCount
或其他,則裝飾后的函數(shù)將不再提供這些屬性。因?yàn)檫@是裝飾器。因此,如果有人使用它們,那么就需要小心。
例如,在上面的示例中,如果 slow
函數(shù)具有任何屬性,而 cachingDecorator(slow)
則是一個(gè)沒有這些屬性的包裝器。
一些包裝器可能會(huì)提供自己的屬性。例如,裝飾器會(huì)計(jì)算一個(gè)函數(shù)被調(diào)用了多少次以及花費(fèi)了多少時(shí)間,并通過包裝器屬性公開(expose)這些信息。
存在一種創(chuàng)建裝飾器的方法,該裝飾器可保留對函數(shù)屬性的訪問權(quán)限,但這需要使用特殊的 Proxy
對象來包裝函數(shù)。我們將在后面的 Proxy 和 Reflect 中學(xué)習(xí)它。
裝飾器 是一個(gè)圍繞改變函數(shù)行為的包裝器。主要工作仍由該函數(shù)來完成。
裝飾器可以被看作是可以添加到函數(shù)的 “features” 或 “aspects”。我們可以添加一個(gè)或添加多個(gè)。而這一切都無需更改其代碼!
為了實(shí)現(xiàn) cachingDecorator
,我們研究了以下方法:
func
?。func
? 將 ?context
? 作為 ?this
? 和類數(shù)組的 ?args
? 傳遞給參數(shù)列表。通用的 呼叫轉(zhuǎn)移(call forwarding) 通常是使用 apply
完成的:
let wrapper = function() {
return original.apply(this, arguments);
};
我們也可以看到一個(gè) 方法借用(method borrowing) 的例子,就是我們從一個(gè)對象中獲取一個(gè)方法,并在另一個(gè)對象的上下文中“調(diào)用”它。采用數(shù)組方法并將它們應(yīng)用于參數(shù) arguments
是很常見的。另一種方法是使用 Rest 參數(shù)對象,該對象是一個(gè)真正的數(shù)組。
在 JavaScript 領(lǐng)域里有很多裝飾器(decorators)。通過解決本章的任務(wù),來檢查你掌握它們的程度吧。
創(chuàng)建一個(gè)裝飾器 ?spy(func)
?,它應(yīng)該返回一個(gè)包裝器,該包裝器將所有對函數(shù)的調(diào)用保存在其 ?calls
? 屬性中。
每個(gè)調(diào)用都保存為一個(gè)參數(shù)數(shù)組。
例如:
function work(a, b) {
alert( a + b ); // work 是一個(gè)任意的函數(shù)或方法
}
work = spy(work);
work(1, 2); // 3
work(4, 5); // 9
for (let args of work.calls) {
alert( 'call:' + args.join() ); // "call:1,2", "call:4,5"
}
P.S. 該裝飾器有時(shí)對于單元測試很有用。它的高級形式是 Sinon.JS 庫中的 sinon.spy
。
由 spy(f)
返回的包裝器應(yīng)存儲(chǔ)所有參數(shù),然后使用 f.apply
轉(zhuǎn)發(fā)調(diào)用。
function spy(func) {
function wrapper(...args) {
// using ...args instead of arguments to store "real" array in wrapper.calls
wrapper.calls.push(args);
return func.apply(this, args);
}
wrapper.calls = [];
return wrapper;
}
重要程度: 5
創(chuàng)建一個(gè)裝飾器 delay(f, ms)
,該裝飾器將 f
的每次調(diào)用延時(shí) ms
毫秒。
例如:
function f(x) {
alert(x);
}
// create wrappers
let f1000 = delay(f, 1000);
let f1500 = delay(f, 1500);
f1000("test"); // 在 1000ms 后顯示 "test"
f1500("test"); // 在 1500ms 后顯示 "test"
換句話說,?delay(f, ms)
? 返回的是延遲 ?ms
? 后的 ?f
? 的變體。
在上面的代碼中,?f
? 是單個(gè)參數(shù)的函數(shù),但是你的解決方案應(yīng)該傳遞所有參數(shù)和上下文 ?this
?。
function delay(f, ms) {
return function() {
setTimeout(() => f.apply(this, arguments), ms);
};
}
let f1000 = delay(alert, 1000);
f1000("test"); // shows "test" after 1000ms
注意這里是如何使用箭頭函數(shù)的。我們知道,箭頭函數(shù)沒有自己的 this
和 arguments
,所以 f.apply(this, arguments)
從包裝器中獲取 this
和 arguments
。
如果我們傳遞一個(gè)常規(guī)函數(shù),setTimeout
將調(diào)用它且不帶參數(shù),并且 this=window
(假設(shè)我們在瀏覽器環(huán)境)。
我們?nèi)匀豢梢酝ㄟ^使用中間變量來傳遞正確的 this
,但這有點(diǎn)麻煩:
function delay(f, ms) {
return function(...args) {
let savedThis = this; // 將 this 存儲(chǔ)到中間變量
setTimeout(function() {
f.apply(savedThis, args); // 在這兒使用它
}, ms);
};
}
debounce(f, ms)
裝飾器的結(jié)果是一個(gè)包裝器,該包裝器將暫停對 f
的調(diào)用,直到經(jīng)過 ms
毫秒的非活動(dòng)狀態(tài)(沒有函數(shù)調(diào)用,“冷卻期”),然后使用最新的參數(shù)調(diào)用 f
一次。
換句話說,debounce
就像一個(gè)“接聽電話”的秘書,并一直等到 ms
毫秒的安靜時(shí)間之后,才將最新的呼叫信息傳達(dá)給“老板”(調(diào)用實(shí)際的 f
)。
舉個(gè)例子,我們有一個(gè)函數(shù) f
,并將其替換為 f = debounce(f, 1000)
。
然后,如果包裝函數(shù)分別在 0ms、200ms 和 500ms 時(shí)被調(diào)用了,之后沒有其他調(diào)用,那么實(shí)際的 f
只會(huì)在 1500ms 時(shí)被調(diào)用一次。也就是說:從最后一次調(diào)用開始經(jīng)過 1000ms 的冷卻期之后。
……并且,它將獲得最后一個(gè)調(diào)用的所有參數(shù),其他調(diào)用的參數(shù)將被忽略。
以下是其實(shí)現(xiàn)代碼(使用了 Lodash library 中的防抖裝飾器 ):
let f = _.debounce(alert, 1000);
f("a");
setTimeout( () => f("b"), 200);
setTimeout( () => f("c"), 500);
// 防抖函數(shù)從最后一次函數(shù)調(diào)用以后等待 1000ms,然后執(zhí)行:alert("c")
現(xiàn)在我們舉一個(gè)實(shí)際中的例子。假設(shè)用戶輸入了一些內(nèi)容,我們想要在用戶輸入完成時(shí)向服務(wù)器發(fā)送一個(gè)請求。
我們沒有必要為每一個(gè)字符的輸入都發(fā)送請求。相反,我們想要等一段時(shí)間,然后處理整個(gè)結(jié)果。
在 Web 瀏覽器中,我們可以設(shè)置一個(gè)事件處理程序 —— 一個(gè)在每次輸入內(nèi)容發(fā)生改動(dòng)時(shí)都會(huì)調(diào)用的函數(shù)。通常,監(jiān)聽所有按鍵輸入的事件的處理程序會(huì)被調(diào)用的非常頻繁。但如果我們?yōu)檫@個(gè)處理程序做一個(gè) 1000ms 的 debounce
處理,它僅會(huì)在最后一次輸入后的 1000ms 后被調(diào)用一次。
任務(wù)是實(shí)現(xiàn)一個(gè) debounce
裝飾器。
提示:如果你好好想想,實(shí)現(xiàn)它只需要幾行代碼 :)
function debounce(func, ms) {
let timeout;
return function() {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, arguments), ms);
};
}
調(diào)用 debounce
會(huì)返回一個(gè)包裝器。當(dāng)它被調(diào)用時(shí),它會(huì)安排一個(gè)在給定的 ms
之后對原始函數(shù)的調(diào)用,并取消之前的此類超時(shí)。
創(chuàng)建一個(gè)“節(jié)流”裝飾器 throttle(f, ms)
—— 返回一個(gè)包裝器。
當(dāng)被多次調(diào)用時(shí),它會(huì)在每 ms
毫秒最多將調(diào)用傳遞給 f
一次。
與防抖(debounce)裝飾器相比,其行為完全不同:
debounce
? 會(huì)在“冷卻(cooldown)”期后運(yùn)行函數(shù)一次。適用于處理最終結(jié)果。throttle
? 運(yùn)行函數(shù)的頻率不會(huì)大于所給定的時(shí)間 ?ms
? 毫秒。適用于不應(yīng)該經(jīng)常進(jìn)行的定期更新。換句話說,throttle
就像接電話的秘書,但是打擾老板(實(shí)際調(diào)用 f
)的頻率不能超過每 ms
毫秒一次。
讓我們看看現(xiàn)實(shí)生活中的應(yīng)用程序,以便更好地理解這個(gè)需求,并了解它的來源。
例如,我們想要跟蹤鼠標(biāo)移動(dòng)。
在瀏覽器中,我們可以設(shè)置一個(gè)函數(shù),使其在每次鼠標(biāo)移動(dòng)時(shí)運(yùn)行,并獲取鼠標(biāo)移動(dòng)時(shí)的指針位置。在使用鼠標(biāo)的過程中,此函數(shù)通常會(huì)執(zhí)行地非常頻繁,大概每秒 100 次(每 10 毫秒)。
我們想要在鼠標(biāo)指針移動(dòng)時(shí),更新網(wǎng)頁上的某些信息。
……但是更新函數(shù) update()
太重了,無法在每個(gè)微小移動(dòng)上都執(zhí)行。高于每 100ms 更新一次的更新頻次也沒有意義。
因此,我們將其包裝到裝飾器中:使用 throttle(update, 100)
作為在每次鼠標(biāo)移動(dòng)時(shí)運(yùn)行的函數(shù),而不是原始的 update()
。裝飾器會(huì)被頻繁地調(diào)用,但是最多每 100ms 將調(diào)用轉(zhuǎn)發(fā)給 update()
一次。
在視覺上,它看起來像這樣:
update
?。這很重要,用戶會(huì)立即看到我們對其動(dòng)作的反應(yīng)。100ms
? 沒有任何反應(yīng)。裝飾的變體忽略了調(diào)用。100ms
? 結(jié)束時(shí) —— 最后一個(gè)坐標(biāo)又發(fā)生了一次 update。100ms
? 到期,然后用最后一個(gè)坐標(biāo)運(yùn)行一次 ?update
?。因此,非常重要的是,處理最終的鼠標(biāo)坐標(biāo)。一個(gè)代碼示例:
function f(a) {
console.log(a);
}
// f1000 最多每 1000ms 將調(diào)用傳遞給 f 一次
let f1000 = throttle(f, 1000);
f1000(1); // 顯示 1
f1000(2); // (節(jié)流,尚未到 1000ms)
f1000(3); // (節(jié)流,尚未到 1000ms)
// 當(dāng) 1000ms 時(shí)間到...
// ...輸出 3,中間值 2 被忽略
P.S. 參數(shù)(arguments)和傳遞給 f1000
的上下文 this
應(yīng)該被傳遞給原始的 f
。
function throttle(func, ms) {
let isThrottled = false,
savedArgs,
savedThis;
function wrapper() {
if (isThrottled) { // (2)
savedArgs = arguments;
savedThis = this;
return;
}
isThrottled = true;
func.apply(this, arguments); // (1)
setTimeout(function() {
isThrottled = false; // (3)
if (savedArgs) {
wrapper.apply(savedThis, savedArgs);
savedArgs = savedThis = null;
}
}, ms);
}
return wrapper;
}
調(diào)用 throttle(func, ms)
返回 wrapper
。
wrapper
? 只運(yùn)行 ?func
? 并設(shè)置冷卻狀態(tài)(?isThrottled = true
?)。savedArgs/savedThis
? 中。請注意,上下文和參數(shù)(arguments)同等重要,應(yīng)該被記下來。我們同時(shí)需要他們以重現(xiàn)調(diào)用。ms
? 毫秒后,觸發(fā) ?setTimeout
?。冷卻狀態(tài)被移除(?isThrottled = false
?),如果我們忽略了調(diào)用,則將使用最后記憶的參數(shù)和上下文執(zhí)行 ?wrapper
?。第 3 步運(yùn)行的不是 func
,而是 wrapper
,因?yàn)槲覀儾粌H需要執(zhí)行 func
,還需要再次進(jìn)入冷卻狀態(tài)并設(shè)置 timeout 以重置它。
更多建議: