一個有味道的函數
- 2019 年 12 月 5 日
- 筆記
一個有味道的函數
最近想到了一個自認為很有意思的面試題 如何實現一個
compose
函數。 函數接收數個參數,參數均為Function
類型,右側函數的執行結果將作為左側函數執行的參數來調用。
compose(arg => `${arg}%`, arg => arg.toFixed(2), arg => arg + 10)(5) // 15.00% compose(arg => arg.toFixed(2), arg => arg + 10)(5) // 15.00 compose(arg => arg + 10)(5) // 15
執行結果如上述代碼,有興趣的同學可以先自己實現一下再來看後續的。
1.0實現方案
大致的思路為:
- 獲取所有的參數
- 調用最後一個函數,並接收返回值
- 如果沒有後續的函數,返回數據,如果有,將返回值放入下一個函數中執行
所以這種情況用遞歸來實現會比較清晰一些
function compose (...funcs) { return function exec (arg) { let func = funcs.pop() let result = func(arg) // 執行函數,獲取返回值 // 如果後續還有函數,將返回值放入下一個函數執行 // 如果後續沒有了,直接返回 return funcs.length ? exec(result) : result } }
這樣,我們就實現了上述的compose
函數。 真是可喜可賀,可喜可賀。
本文完。
好了,如果現實生活中開發做需求也是如此爽快不做作就好了,但是,產品總是會來的,需求總是會改的。
2.0需求變更
我們現在有如下要求,函數需要支持Promise
對象,而且要兼容普通函數的方式。 示例代碼如下:
// 為方便閱讀修改的排版 compose( arg => new Promise((resolve, reject) => setTimeout(_ => resolve(arg.toFixed(2)), 1000 ) ), arg => arg + 10 )(5).then(data => { console.log(data) // 15.00 })
我們有如下代碼調用,對toFixed
函數的調用添加1000ms
的延遲。讓用戶覺得這個函數執行很慢,方便下次優化
所以,我們就需要去修改compose
函數了。 我們之前的代碼只能支持普通函數的處理,現在因為添加了Promise
對象的原因,所以我們要進行如下修改:
首先,異步函數改為同步函數是不存在的readFile/readFileSync
這類除外。 所以,最簡單的方式就是,我們將普通函數改為異步函數,也就是在普通函數外包一層Promise
。
function compose (...funcs) { return function exec (arg) { return new Promise((resolve, reject) => { let func = funcs.pop() let result = promiseify(func(arg)) // 執行函數,獲取返回值,並將返回值轉換為`Promise`對象 // 註冊`Promise`的`then`事件,並在裡邊進行下一次函數執行的準備 // 判斷後續是否還存在函數,如果有,繼續執行 // 如果沒有,直接返回結果 result.then(data => funcs.length ? exec(data).then(resolve).catch(reject) : resolve(data) ).catch(reject) }) } } // 判斷參數是否為`Promise` function isPromise (pro) { return pro instanceof Promise } // 將參數轉換為`Promise` function promiseify (pro) { // 如果結果為`Promise`,直接返回 if (isPromise(pro)) return pro // 如果結果為這些基本類型,說明是普通函數 // 我們給他包一層`Promise.resolve` if (['string', 'number', 'regexp', 'object'].includes(typeof pro)) return Promise.resolve(pro) }
我們針對compose
代碼的改動主要是集中在這幾處:
- 將
compose
的返回值改為了Promise
對象,這個是必然的,因為內部可能會包含Promise
參數,所以我們一定要返回一個Promise
對象 - 將各個函數執行的返回值包裝為了
Promise
對象,為了統一返回值。 - 處理函數返回值,監聽
then
和catch
、並將resolve
和reject
傳遞了過去。
3.0終極版
現在,我們又得到了一個新的需求,我們想要在其中某些函數執行中跳過部分代碼,先執行後續的函數,等到後續函數執行完後,再拿到返回值執行剩餘的代碼:
compose( data => new Promise((resolve, reject) => resolve(data + 2.5)), data => new Promise((resolve, reject) => resolve(data + 2.5)), async function c (data, next) { // async/await為Promise語法糖,不贅述 data += 10 // 數值 + 10 let result = await next(data) // 先執行後續的代碼 result -= 5 // 數值 - 5 return result }, (data, next) => new Promise((resolve, reject) => { next(data).then(data => { data = data / 100 // 將數值除以100限制百分比 resolve(`${data}%`) }).catch(reject) // 先執行後續的代碼 }), function d (data) { return data + 20 } )(15).then(console.log) // 0.45%
拿到需求後,陷入沉思。。。 好好地順序執行代碼,突然就變成了這個鳥樣,隨時可能會跳到後邊的函數去。 所以我們分析這個新需求的效果:
我們在函數執行到一半時,執行了next
,next
的返回值為後續函數的執行返回值。 也就是說,我們在next
中處理,直接調用隊列中的下一個函數即可; 然後監聽then
和catch
回調,即可在當前函數中獲取到返回值; 拿到返回值後就可以執行我們後續的代碼。
然後他的實現呢,也是非常的簡單,我們只需要修改如下代碼即可完成操作:
// 在這裡會強行調用`exec`並傳入參數 // 而`exec`的執行,則意味着`funcs`集合中又一個函數被從隊列中取出來 promiseify(func(arg, arg => exec(arg)))
也就是說,我們會提前執行下一個函數,而且下一個函數的then
事件註冊是在我們當前函數內部的,當我們拿到返回值後,就可以進行後續的處理了。 而我們所有的函數是存放在一個隊列里的,在我們提前執行完畢該函數後,後續的執行也就不會再出現了。避免了一個函數被重複執行的問題。
如果看到這裡已經很明白了,那麼恭喜,你已經了解了實現koajs
最核心的代碼: 中間件的實現方式、洋蔥模型

想必現在整個函數周遭散發著洋蔥
的味道。
參考資料
相關示例代碼倉庫
1.0,普通函數 2.0,Promise函數 3.0,支持洋蔥模型