Javascript 裝飾器模式和轉(zhuǎn)發(fā),call/apply

2023-02-17 10:51 更新

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ù)。
  • 緩存邏輯是獨(dú)立的,它沒有增加 ?slow? 本身的復(fù)雜性(如果有的話)。
  • 如果需要,我們可以組合多個(gè)裝飾器(其他裝飾器將遵循同樣的邏輯)。

使用 “func.call” 設(shè)定上下文

上面提到的緩存裝飾器不適用于對象方法。

例如,在下面的代碼中,?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)用 sayHisayHi.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? 是如何被傳遞的:

  1. 在經(jīng)過裝飾之后,?worker.slow? 現(xiàn)在是包裝器 ?function (x) { ... }?。
  2. 因此,當(dāng) ?worker.slow(2)? 執(zhí)行時(shí),包裝器將 ?2? 作為參數(shù),并且 ?this=worker?(它是點(diǎn)符號(hào) ?.? 之前的對象)。
  3. 在包裝器內(nèi)部,假設(shè)結(jié)果尚未緩存,?func.call(this, x)? 將當(dāng)前的 ?this?(?=worker?)和當(dāng)前的參數(shù)(?=2?)傳遞給原始方法。

傳遞多個(gè)參數(shù)

現(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):

  1. 實(shí)現(xiàn)一個(gè)新的(或使用第三方的)類似 map 的更通用并且允許多個(gè)鍵的數(shù)據(jù)結(jié)構(gòu)。
  2. 使用嵌套 map:?cache.set(min)? 將是一個(gè)存儲(chǔ)(鍵值)對 ?(max, result)? 的 ?Map?。所以我們可以使用 ?cache.get(min).get(max)? 來獲取 ?result?。
  3. 將兩個(gè)值合并為一個(gè)。為了靈活性,我們可以允許為裝飾器提供一個(gè)“哈希函數(shù)”,該函數(shù)知道如何將多個(gè)值合并為一個(gè)值。

對于許多實(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

我們可以使用 ?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ì)微的差別:

  • Spread 語法 ?...? 允許將 可迭代對象 ?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ī)范中幾乎“按原樣”解釋如下:

  1. 讓 ?glue? 成為第一個(gè)參數(shù),如果沒有參數(shù),則使用逗號(hào) ?","?。
  2. 讓 ?result? 為空字符串。
  3. 將 ?this[0]? 附加到 ?result?。
  4. 附加 ?glue? 和 ?this[1]?。
  5. 附加 ?glue? 和 ?this[2]?。
  6. ……以此類推,直到 ?this.length? 項(xiàng)目被粘在一起。
  7. 返回 ?result?。

因此,從技術(shù)上講,它需要 this 并將 this[0]this[1] ……等 join 在一起。它的編寫方式是故意允許任何類數(shù)組的 this 的(不是巧合,很多方法都遵循這種做法)。這就是為什么它也可以和 this=arguments 一起使用。

裝飾器和函數(shù)屬性

通常,用裝飾的函數(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í)它。

總結(jié)

裝飾器 是一個(gè)圍繞改變函數(shù)行為的包裝器。主要工作仍由該函數(shù)來完成。

裝飾器可以被看作是可以添加到函數(shù)的 “features” 或 “aspects”。我們可以添加一個(gè)或添加多個(gè)。而這一切都無需更改其代碼!

為了實(shí)現(xiàn) cachingDecorator,我們研究了以下方法:

通用的 呼叫轉(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ù),來檢查你掌握它們的程度吧。

任務(wù)


間諜裝飾器

重要程度: 5

創(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;
}

延時(shí)裝飾器

重要程度: 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);
  };

}

防抖裝飾器

重要程度: 5

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í)。


節(jié)流裝飾器

重要程度: 5

創(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() 一次。

在視覺上,它看起來像這樣:

  1. 對于第一個(gè)鼠標(biāo)移動(dòng),裝飾的變體立即將調(diào)用傳遞給 ?update?。這很重要,用戶會(huì)立即看到我們對其動(dòng)作的反應(yīng)。
  2. 然后,隨著鼠標(biāo)移動(dòng),直到 ?100ms? 沒有任何反應(yīng)。裝飾的變體忽略了調(diào)用。
  3. 在 ?100ms? 結(jié)束時(shí) —— 最后一個(gè)坐標(biāo)又發(fā)生了一次 update。
  4. 然后,最后,鼠標(biāo)停在某處。裝飾的變體會(huì)等到 ?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。

  1. 在第一次調(diào)用期間,?wrapper? 只運(yùn)行 ?func? 并設(shè)置冷卻狀態(tài)(?isThrottled = true?)。
  2. 在這種狀態(tài)下,所有調(diào)用都記憶在 ?savedArgs/savedThis? 中。請注意,上下文和參數(shù)(arguments)同等重要,應(yīng)該被記下來。我們同時(shí)需要他們以重現(xiàn)調(diào)用。
  3. ……然后經(jīng)過 ?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 以重置它。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)