在這篇文章裡,我會實現一個可重用的函式來處理 JavaScript 延時非同步操作。
calc 是一個我們想要做剖析(效能分析)的非同步函式。按照慣例,它的最後一個引數是一個callback。我們像這樣使用 calc:
1 |
calc(arg, (err, res) => console.log(err || res)) |
或許,最簡單的對 calc 這樣的函式來剖析效能的方法是,增加一個計時邏輯到我們需要分析的地方:
1 2 3 4 5 6 |
const t0 = Date.now() calc(arg, (err, res) => { const t1 = Date.now() console.log(`Log: time: ${t1 = t0}`) console.log(err || res) }) |
但是,這不是一個可複用的解決方案。每一次我們想要對一個函式計時,我們得引入一個 t0 在外層作用域並且改變 callback 來測量和記錄時間。
對我來說理想的方式是能夠僅僅通過包裝一個非同步函式就能夠對它進行計時:
1 |
timeIt(calc)(arg, (err, res) => console.log(err || res)) |
timeIt 需要能夠很好地對每一個非同步函式完成剖析和記錄執行時間。
注意到 timeIt(calc) 有與原始的 calc 函式同樣的函式簽名,即它們接受同樣的引數和返回同樣的值,它只是增加了一個特性到 cale 上(能夠被記錄時間的特性)。
calc 和 timeIt(calc) 在任意時刻可以相互替代。
timeIt 本身是一個高階函式,因為它接受一個函式並返回一個函式。在我們的例子裡,它接受 calc 非同步函式,並返回一個函式與 calc 有同樣的引數和返回值。
下面演示我們如何實現 timeIt 函式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
const timeIt = R.curry((report, f) => (...args) => { const t0 = Date.now() const nArgs = R.init(args) const callback = R.last(args) nArgs.push((...args) => { const t1 = Date.now() callback(...args) report(t1 - t0, ...args) }) f(...nArgs) }) const timeIt1 = timeIt( (t, err, res) => console.log(`Log: ${err || res} produced after: ${t}`) ) const calc = (x, y, z, callback) => setTimeout(() => callback(null, x * y / z), 1000) calc(18, 7, 3, (err, res) => console.log(err || res)) timeIt1(calc)(18, 7, 3, (err, res) => console.log(err || res)) |
我使用了神奇的 Ramda 庫。你可以在 Ramda REPL 執行上面這段程式碼。
這個 timeIt 實現接受兩個引數:
- report: 一個函式用來生成剖析結果
- f: 我們想要做剖析的非同步函式
timeIt1 是一個方便實用的功能函式,它只是用 console.log 記錄時間測量結果。我們通過給更通用的timeIt 函式傳入 report 引數來定義它。
我們實現了目標,現在我們可以僅僅將非同步函式包裝在 timeIt1 中就可以對它計時了:
1 |
timeIt1(calc)(18, 7, 3, (err, res) => console.log(err || res)) |
通用的 timeIt 函式接收一個 report 回撥函式和一個非同步函式並返回一個新的非同步函式,這個非同步函式與原函式有同樣的引數和返回值。我們可以這麼使用:
1 2 3 4 5 6 7 |
timeIt( (time, ...result) => // report callback: log the time , asyncFunc )( parameters…, (...result) => // result of the async function ) |
現在讓我們深入 timeIt 的實現。我們可以簡單地生成一個通用函式類似 timeIt1,因為 timeIt 使用R.curry 科裡化了。
我不打算在這篇文章裡討論科裡化,但是下面這段程式碼演示了科裡化的主要用法:
1 2 3 4 5 6 |
const f = R.curry((x, y) => x + y) f(1, 10) // == 11 f(1)(10) // == 11 const plus1 = f(1) plus1(10) // == 11 |
另一方面,這種方式實現的 timeIt 有幾個問題:
1 2 3 4 5 |
(...args) => { const t1 = Date.now() callback(...args) report(t1 — t0, ...args) } |
這是一個匿名函式(又名 lambda,callback),它在原函式非同步執行之後被呼叫。主要的問題是這個函式沒有處理異常的機制。如果 callback 丟擲異常,report 就永遠不會被呼叫。
我們可以新增一個 try / catch 到這個 lambda 函式裡,然而問題的根源是 callback 和 report 是兩個 void 函式,它們沒有關聯在一起。timeIt 包含兩個延續(continuations)(report 和 callback)。如果我們只是在 console 下記錄執行時間或者如果我們確定不論 report 還是 callback 都不會丟擲異常,那麼一切正常。但是如果我們想要根據剖析結果來執行一些行為(所謂的自動擴容)那麼我們需要強化和釐清我們的程式中的延續序列。
我們將會在後續文章中討論一個使用 Promise 的解決方案。