終極compose函數封裝方案!

前言

無心中在掘金看到一篇寫compose函數的文章《感謝 compose 函數,讓個人代碼屎山💩逐漸美麗了起來~》,以前這個命題我面試的時候問過不少面試者,還挺有體會的。正好談一談node

我不會直接問你知道compose函數嗎,通常簡歷上寫熟悉react技術棧(reudx,react-router等等),我會問知道redux中間件的實現原理嗎?這個問題其實本質上是問同步的compose函數怎麼寫,什麼是同步compose呢?react

compose就是執行一系列的任務(函數),好比有如下任務隊列(數組裏都是函數)webpack

let tasks = [step1, step2, step3, step4]
複製代碼

每個step都是一個步驟,按照步驟一步一步的執行到結尾,這就是一個composecompose在函數式編程中是一個很重要的工具函數,在這裏實現的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的Tapable庫

這個庫是webpack實現鉤子函數的核心庫,爲何要提到它呢,由於它就是各類compose函數的終極解決方案,並且咱們還能夠好好學學這些大佬寫的代碼是如何封裝的

咱們把全部的compose作一個分類:大致是分同步和異步的

  • 分類:

  • Sync*(同步版本compose): 

    • SyncHook (串行同步執行, 不關心返回值)
    • SyncBailHook (串行同步執行,若是返回值不是null,則剩下的函數不執行)
    • SyncWaterfallHook(串行同步執行,前一個函數的返回值做爲後一個函數的參數,跟咱們以前將的redux中間件原理是一個道理,迭代實現,更簡單)
    • SyncLoopHook (串行同步執行,訂閱者返回true表示繼續執行後面的函數,返回undefine表示不執行後面的函數)
  • Async*(異步版本compose):

  • AsyncParallelHook 不關心返回值,併發異步函數而已,沒順序要求

  • AsyncSeriesHook 異步函數數組要求按順序調用

  • AsyncSeriesBailHook 可中斷的異步函數鏈

  • AsyncSeriesWaterfallHook 異步串行瀑布鉤子函數

因此說這幾種compose掌握了,基本compose的各類類型就徹底解決了,面試官跟你聊這些,你均可以秒殺他了,通常他只會考慮咱們前言裏面那兩種compose。

爲了代碼好理解,我作了很微小的改動。

Sync*型Hook

SyncHook

串行同步執行,不關心返回值

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了。

SyncBailHook

串行同步執行,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
複製代碼

SyncWaterfallHook

串行同步執行,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
複製代碼

SyncLoopHook

串行同步執行,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
複製代碼

Async*型Hook

AsyncParallelHook

並行異步執行,和同步執行的最大區別在於,訂閱者中能夠存在異步邏輯。就是個併發異步函數而已,沒順序要求

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
複製代碼

AsyncSeriesHook

異步串行的鉤子,也就是異步函數要求按順序調用

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
複製代碼

AsyncSeriesBailHook

串行異步執行,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))
  }
}
複製代碼

本文結束!歡迎點贊!😺

相關文章
相關標籤/搜索