無心中在掘金看到一篇寫compose函數的文章《感謝 compose 函數,讓個人代碼屎山💩逐漸美麗了起來~》,以前這個命題我面試的時候問過不少面試者,還挺有體會的。正好談一談node
我不會直接問你知道compose
函數嗎,通常簡歷上寫熟悉react技術棧(reudx,react-router等等),我會問知道redux
中間件的實現原理嗎?這個問題其實本質上是問同步的compose
函數怎麼寫,什麼是同步compose
呢?react
compose
就是執行一系列的任務(函數),好比有如下任務隊列(數組裏都是函數)webpack
let tasks = [step1, step2, step3, step4]
複製代碼
每個step
都是一個步驟,按照步驟一步一步的執行到結尾,這就是一個compose
, compose
在函數式編程中是一個很重要的工具函數,在這裏實現的compose
有三點說明web
也就是實現以下:面試
(...args) => step1(step2(setp3(step4(...args))))
複製代碼
如何優雅的實現呢,一個reduce函數便可,redux中間件源碼大體也是這樣實現的編程
function compose(...funcs) {
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
複製代碼
可能有同窗看不懂這個代碼,很正常,咱們簡單分析一下redux
const a = () => console.log(1);
const b = () => console.log(2);
const c = () => console.log(3);
compose(a, b, c)(); // 分別打印 3 、 2 、 1
複製代碼
咱們看看compose組合的時候發生了啥,數組
const k = compose(a, b) 等價於
const k = (...args) => a(b(...args));
compose(k, c) 等價於
(...args) => k(c(...args))
因此compose(a,b,c) 等價於
(...args) => a(b(c(...args)))
複製代碼
若是仍是看不懂,不要急,後面有迭代的方式來實現這個compose函數,就很簡單了。redux這樣寫法更簡潔一些。promise
而後若是對方答上來了,並且使用過koa框架,我會問koa中間件的原理是什麼,能寫一個嗎?它跟同步compose函數有什麼區別。微信
區別就是這些函數都是異步的,上面reduce的寫法就不適用了,好比說
const a = () => setTimeout(()=>console.log(1), 1000);
const b = () => setTimeout(()=>console.log(2), 500);
const c = () => setTimeout(()=>console.log(3), 2000);
複製代碼
很明顯,咱們須要一個異步compose函數來解決問題,這個還能夠引伸爲一道微信的面試題,叫lazyMan,很出名的一道題,你們能夠去搜一下,異步compose在koa源碼裏面有實現,咱們看看框架是怎麼實現的:
假設有三個異步函數fn一、fn二、fn3,實現以下
function fn1(next) {
console.log(1);
next();
}
function fn2(next) {
console.log(2);
next();
}
function fn3(next) {
console.log(3);
next();
}
const middleware = [fn1, fn2, fn3]
複製代碼
參數裏的next是koa函數的一個特色,好比fn1函數,它調用next,fn2纔會執行,不調用不執行,同理fn2調用next,fn3纔會執行。
這就是koa處理異步的方式,採用next方式(還有一種promise方式後面介紹)
// koa有個特色,調用next參數表示調用下一個函數
function fn1(next) {
console.log(1);
next();
}
function fn2(next) {
console.log(2);
next();
}
function fn3(next) {
console.log(3);
next();
}
middleware = [fn1, fn2, fn3]
function compose(middleware){
function dispatch (index){
if(index == middleware.length) return ;
var curr;
curr = middleware[index];
// 這裏使用箭頭函數,讓函數延遲執行
return curr(() => dispatch(++index))
}
dispatch(0)
};
compose(middleware);
複製代碼
好了前言實在太多了。。。。咱們進入正題
這個庫是webpack實現鉤子函數的核心庫,爲何要提到它呢,由於它就是各類compose函數的終極解決方案,並且咱們還能夠好好學學這些大佬寫的代碼是如何封裝的
咱們把全部的compose作一個分類:大致是分同步和異步的
分類:
Sync*(同步版本compose):
Async*(異步版本compose):
AsyncParallelHook 不關心返回值,併發異步函數而已,沒順序要求
AsyncSeriesHook 異步函數數組要求按順序調用
AsyncSeriesBailHook 可中斷的異步函數鏈
AsyncSeriesWaterfallHook 異步串行瀑布鉤子函數
因此說這幾種compose掌握了,基本compose的各類類型就徹底解決了,面試官跟你聊這些,你均可以秒殺他了,通常他只會考慮咱們前言裏面那兩種compose。
爲了代碼好理解,我作了很微小的改動。
串行同步執行,不關心返回值
class SyncHook {
constructor(name){
this.tasks = [];
this.name = name;
}
tap(task){
this.tasks.push(task);
}
call(){
this.tasks.forEach(task=>task(...arguments));
}
}
let queue = new SyncHook('name');
queue.tap(function(...args){ console.log(args); });
queue.tap(function(...args){ console.log(args); });
queue.tap(function(...args){ console.log(args); });
queue.call('hello');
// 打印
// ["hello"]
// ["hello"]
// ["hello"]
複製代碼
看完上面的函數,有人就會說,compose函數去哪了,咱們不是要組合函數嗎,上面這是什麼東西?
咱們能夠看到tap是註冊函數,call是調用函數,也就是說,compose函數自己結合了tap註冊的功能和call調用的功能。因此咱們也能夠把這個 SyncHook 改裝 成compose就是
function compose(...fns) {
return (...args) => fns.forEach(task=>task(...args))
}
複製代碼
這個區別頗有意思的,其實就是面向對象和函數式編程的一個區別,tapable是用面向對象的思想,咱們上面的compose是函數式的思想,一個是以類爲中心利用數據和函數去組合功能,一個是以函數爲中心組合功能。
下面的函數也是一個道理,自己代碼都很是簡單,一看就懂了,我就不一一轉換成compose了。
串行同步執行,bail是保險絲的意思,有一個返回值不爲null則跳過剩下的邏輯
class SyncBailHook {
constructor(name){
this.tasks = [];
this.name = name;
}
tap(task){
this.tasks.push(task);
}
call(){
let i= 0,ret;
do {
ret=this.tasks[i++](...arguments);
} while (!ret);
}
}
let queue = new SyncBailHook('name');
queue.tap(function(name){
console.log(name,1);
return 'Wrong';
});
queue.tap(function(name){
console.log(name,2);
});
queue.tap(function(name){
console.log(name,3);
});
queue.call('hello');
// 打印
// hello 1
複製代碼
串行同步執行,Waterfall是瀑布的意思,前一個訂閱者的返回值會傳給後一個訂閱者
class SyncWaterfallHook {
constructor(name){
this.tasks = [];
this.name = name;
}
tap(task){
this.tasks.push(task);
}
call(){
let [first,...tasks] = this.tasks;
tasks.reduce((ret,task) => task(ret) , first(...arguments));
}
}
let queue = new SyncWaterfallHook(['name']);
queue.tap(function(name,age){
console.log(name, age, 1);
return 1;
});
queue.tap(function(data){
console.log(data , 2);
return 2;
});
queue.tap(function(data){
console.log(data, 3);
});
queue.call('hello', 25);
// 打印
// hello 25 1
// 1 2
// 2 3
複製代碼
串行同步執行,Loop是循環往復的意思,訂閱者返回true表示繼續列表循環,返回undefined表示結束循環
class SyncLoopHook{
constructor(name) {
this.tasks=[];
this.name = name;
}
tap(task) {
this.tasks.push(task);
}
call(...args) {
this.tasks.forEach(task => {
let ret = true;
do {
ret = task(...args);
}while(ret == true || !(ret === undefined))
});
}
}
let hook = new SyncLoopHook('name');
let total = 0;
hook.tap(function(name){
console.log('react',name)
return ++total === 3? undefined :'繼續學';
})
hook.tap(function(name){
console.log('node',name)
})
hook.tap(function(name){
console.log('node',name)
})
hook.call('hello');
// 打印3次react hello,而後打印 node hello,最後再次打印node hello
複製代碼
並行異步執行,和同步執行的最大區別在於,訂閱者中能夠存在異步邏輯。就是個併發異步函數而已,沒順序要求
promise實現
class AsyncParallelHook{
constructor(name) {
this.tasks=[];
this.name = name;
}
tapPromise(task) {
this.tasks.push(task);
}
promise() {
let promises = this.tasks.map(task => task());
// Promise.all全部的Promsie執行完成會調用回調
return Promise.all(promises);
}
}
let queue = new AsyncParallelHook('name');
console.time('cast');queue.tapPromise(function(name){
return new Promise(function(resolve,reject){
setTimeout(function(){
console.log(1);
resolve();
},1000)
});
});
queue.tapPromise(function(name){
return new Promise(function(resolve,reject){
setTimeout(function(){
console.log(2);
resolve();
},2000)
});
});
queue.tapPromise(function(name){
return new Promise(function(resolve,reject){
setTimeout(function(){
console.log(3);
resolve();
},3000)
});
});
queue.promise('hello').then(()=>{
console.timeEnd('cast');
})
// 打印
// 1
// 2
// 3
複製代碼
異步串行的鉤子,也就是異步函數要求按順序調用
promise實現
class AsyncSeriesHook {
constructor(name) {
this.tasks = [];
this.name = name
}
promise(...args) {
let [first, ...others] = this.tasks;
return others.reduce((p, n) => { // 相似redux源碼
return p.then(() => {
return n(...args);
});
}, first(...args))
}
tapPromise(task) {
this.tasks.push(task);
}
}
let queue=new AsyncSeriesHook('name');
console.time('cost');
queue.tapPromise(function(name){
return new Promise(function(resolve){
setTimeout(function(){
console.log(1);
resolve();
},1000)
});
});
queue.tapPromise(function(name,callback){
return new Promise(function(resolve){
setTimeout(function(){
console.log(2);
resolve();
},2000)
});
});
queue.tapPromise(function(name,callback){
return new Promise(function(resolve){
setTimeout(function(){
console.log(3);
resolve();
},3000)
});
});
queue.promise('hello').then(data=>{
console.log(data);
console.timeEnd('cost');
});
// 打印
// 1
// 2
// 3
複製代碼
串行異步執行,bail是保險絲的意思,任務若是return,或者reject,則阻塞了
這裏的實現有一絲絲技巧,就是如何打斷reduce,能夠看一個簡單案例
const arr = [0, 1, 2, 3, 4]
const sum = arr.reduce((prev, curr, index, currArr) => {
prev += curr
if (curr === 3) currArr.length = 0
return prev
}, 0)
console.log(sum) // 6
複製代碼
這就是打斷reduce的辦法就 --- 用一個if判斷
promise實現
class AsyncSeriesBailHook {
constructor(name){
this.tasks = [];
this.name = name
}
tapPromise(task){
this.tasks.push(task);
}
promise(...args){
const [first,...others] = this.tasks;
return new Promise((resolve, reject) => {
others.reduce((pre, next, index, arr) => {
return pre
.then(() => { if((arr.length !== 0)) return next(...args)})
.catch((err=>{
arr.splice(index, arr.length - index);
reject(err);
})).then(()=>{
(arr.length === 0) && resolve();
})
}, first(...args))
})
}
}
let queue=new AsyncSeriesBailHook('name');
console.time('cast');
queue.tapPromise(function(...args){
return new Promise(function(resolve){
setTimeout(function(){
console.log(1);
resolve();
},1000)
});
});
queue.tapPromise(function(...args){
return new Promise(function(resolve, reject){
setTimeout(function(){
console.log(2);
reject(); // 使用reject那麼就會直接跳出後面的邏輯
},1000)
});
});
queue.tapPromise(function(...args){
return new Promise(function(resolve){
setTimeout(function(){
console.log(3);
resolve();
},1000)
});
});
queue.promise('hello').then( data => {
console.log(data);
console.timeEnd('cast');
});// 打印
// 1
// 2
複製代碼
上面的實現也可使用下面的代碼,原理跟koa相似
AsyncSeriesWaterfallHook
串行異步執行,Waterfall是瀑布的意思,前一個訂閱者的返回值會傳給後一個訂閱者
promise實現
class AsyncSeriesWaterfallHook {
constructor(){
this.name= name;
this.tasks = [];
}
tapPromise(name,task){
this.tasks.push(task);
}
promise(...args){
const [first,...others] = this.tasks;
return others.reduce((pre, next) => {
return pre.then((data)=>{
return data ? next(data) : next(...args);
})
},first(...args))
}
}
複製代碼
本文結束!歡迎點贊!😺