最近想到了一個自認為很有意思的面試題
如何實現(xiàn)一個compose
函數(shù)摔敛。
函數(shù)接收數(shù)個參數(shù)廷蓉,參數(shù)均為Function
類型,右側函數(shù)的執(zhí)行結果將作為左側函數(shù)執(zhí)行的參數(shù)來調(diào)用舷夺。
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
執(zhí)行結果如上述代碼苦酱,有興趣的同學可以先自己實現(xiàn)一下再來看后續(xù)的。
1.0實現(xiàn)方案
大致的思路為:
- 獲取所有的參數(shù)
- 調(diào)用最后一個函數(shù)给猾,并接收返回值
- 如果沒有后續(xù)的函數(shù)疫萤,返回數(shù)據(jù),如果有敢伸,將返回值放入下一個函數(shù)中執(zhí)行
所以這種情況用遞歸來實現(xiàn)會比較清晰一些
function compose (...funcs) {
return function exec (arg) {
let func = funcs.pop()
let result = func(arg) // 執(zhí)行函數(shù)扯饶,獲取返回值
// 如果后續(xù)還有函數(shù),將返回值放入下一個函數(shù)執(zhí)行
// 如果后續(xù)沒有了池颈,直接返回
return funcs.length ? exec(result) : result
}
}
這樣尾序,我們就實現(xiàn)了上述的compose
函數(shù)。
真是可喜可賀躯砰,可喜可賀每币。
本文完。
好了琢歇,如果現(xiàn)實生活中開發(fā)做需求也是如此爽快不做作就好了兰怠,但是梦鉴,產(chǎn)品總是會來的,需求總是會改的揭保。
2.0需求變更
我們現(xiàn)在有如下要求肥橙,函數(shù)需要支持Promise
對象,而且要兼容普通函數(shù)的方式秸侣。
示例代碼如下:
// 為方便閱讀修改的排版
compose(
arg => new Promise((resolve, reject) =>
setTimeout(_ =>
resolve(arg.toFixed(2)),
1000
)
),
arg => arg + 10
)(5).then(data => {
console.log(data) // 15.00
})
我們有如下代碼調(diào)用存筏,對toFixed
函數(shù)的調(diào)用添加1000ms
的延遲。讓用戶覺得這個函數(shù)執(zhí)行很慢味榛,方便下次優(yōu)化
所以椭坚,我們就需要去修改compose
函數(shù)了。
我們之前的代碼只能支持普通函數(shù)的處理励负,現(xiàn)在因為添加了Promise
對象的原因藕溅,所以我們要進行如下修改:
首先匕得,異步函數(shù)改為同步函數(shù)是不存在的readFile/readFileSync
這類除外继榆。
所以,最簡單的方式就是汁掠,我們將普通函數(shù)改為異步函數(shù)略吨,也就是在普通函數(shù)外包一層Promise
。
function compose (...funcs) {
return function exec (arg) {
return new Promise((resolve, reject) => {
let func = funcs.pop()
let result = promiseify(func(arg)) // 執(zhí)行函數(shù)考阱,獲取返回值翠忠,并將返回值轉換為`Promise`對象
// 注冊`Promise`的`then`事件,并在里邊進行下一次函數(shù)執(zhí)行的準備
// 判斷后續(xù)是否還存在函數(shù)乞榨,如果有秽之,繼續(xù)執(zhí)行
// 如果沒有,直接返回結果
result.then(data => funcs.length ?
exec(data).then(resolve).catch(reject) :
resolve(data)
).catch(reject)
})
}
}
// 判斷參數(shù)是否為`Promise`
function isPromise (pro) {
return pro instanceof Promise
}
// 將參數(shù)轉換為`Promise`
function promiseify (pro) {
// 如果結果為`Promise`吃既,直接返回
if (isPromise(pro)) return pro
// 如果結果為這些基本類型考榨,說明是普通函數(shù)
// 我們給他包一層`Promise.resolve`
if (['string', 'number', 'regexp', 'object'].includes(typeof pro)) return Promise.resolve(pro)
}
我們針對compose
代碼的改動主要是集中在這幾處:
- 將
compose
的返回值改為了Promise
對象,這個是必然的鹦倚,因為內(nèi)部可能會包含Promise
參數(shù)河质,所以我們一定要返回一個Promise
對象 - 將各個函數(shù)執(zhí)行的返回值包裝為了
Promise
對象,為了統(tǒng)一返回值震叙。 - 處理函數(shù)返回值掀鹅,監(jiān)聽
then
和catch
、并將resolve
和reject
傳遞了過去媒楼。
3.0終極版
現(xiàn)在乐尊,我們又得到了一個新的需求,我們想要在其中某些函數(shù)執(zhí)行中跳過部分代碼划址,先執(zhí)行后續(xù)的函數(shù)扔嵌,等到后續(xù)函數(shù)執(zhí)行完后昏滴,再拿到返回值執(zhí)行剩余的代碼:
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 // 數(shù)值 + 10
let result = await next(data) // 先執(zhí)行后續(xù)的代碼
result -= 5 // 數(shù)值 - 5
return result
},
(data, next) => new Promise((resolve, reject) => {
next(data).then(data => {
data = data / 100 // 將數(shù)值除以100限制百分比
resolve(`${data}%`)
}).catch(reject) // 先執(zhí)行后續(xù)的代碼
}),
function d (data) { return data + 20 }
)(15).then(console.log) // 0.45%
拿到需求后对人,陷入沉思谣殊。。牺弄。
好好地順序執(zhí)行代碼姻几,突然就變成了這個鳥樣,隨時可能會跳到后邊的函數(shù)去势告。
所以我們分析這個新需求的效果:
我們在函數(shù)執(zhí)行到一半時蛇捌,執(zhí)行了next
,next
的返回值為后續(xù)函數(shù)的執(zhí)行返回值咱台。
也就是說络拌,我們在next
中處理,直接調(diào)用隊列中的下一個函數(shù)即可回溺;
然后監(jiān)聽then
和catch
回調(diào)春贸,即可在當前函數(shù)中獲取到返回值;
拿到返回值后就可以執(zhí)行我們后續(xù)的代碼遗遵。
然后他的實現(xiàn)呢萍恕,也是非常的簡單,我們只需要修改如下代碼即可完成操作:
// 在這里會強行調(diào)用`exec`并傳入?yún)?shù)
// 而`exec`的執(zhí)行车要,則意味著`funcs`集合中又一個函數(shù)被從隊列中取出來
promiseify(func(arg, arg => exec(arg)))
也就是說允粤,我們會提前執(zhí)行下一個函數(shù),而且下一個函數(shù)的then
事件注冊是在我們當前函數(shù)內(nèi)部的翼岁,當我們拿到返回值后类垫,就可以進行后續(xù)的處理了。
而我們所有的函數(shù)是存放在一個隊列里的琅坡,在我們提前執(zhí)行完畢該函數(shù)后悉患,后續(xù)的執(zhí)行也就不會再出現(xiàn)了。避免了一個函數(shù)被重復執(zhí)行的問題脑蠕。
如果看到這里已經(jīng)很明白了购撼,那么恭喜,你已經(jīng)了解了實現(xiàn)koajs
最核心的代碼:
中間件的實現(xiàn)方式谴仙、洋蔥模型
想必現(xiàn)在整個函數(shù)周遭散發(fā)著
洋蔥
的味道迂求。