preface

By accident, I came across an article writing the compose function “Thanks to the compose function, which makes my code 💩 gradually beautiful ~”. I have asked many interviewees about this proposition in the interview before, and I have quite experienced it. Just in time to talk

I will not directly ask you if you know the compose function. In general, it is stated on the resume that you are familiar with the React technology stack (Reudx, React-router, etc.), but I will ask if you know the implementation principle of redux middleware. So the question is essentially what do we write for the synchronized compose function, what is synchronous compose?

For example, compose is a list of tasks, such as the following task queue.

let tasks = [step1, step2, step3, step4]
Copy the code

Each step is a step, which is executed step by step to the end. This is compose. Compose is an important utility function in functional programming

  • The first function is multivariate (takes multiple arguments), and all subsequent functions are cellular (takes one argument).
  • Executed sequentially from right to left
  • All functions are executed synchronously (more on asynchrony)

That is to implement the following:

(... args) => step1(step2(setp3(step4(... args))))Copy the code

How elegant implementation, a reduce function can, redux middleware source code is roughly the same way to achieve

function compose(... funcs) { return funcs.reduce((a, b) => (... args) => a(b(... args))) }Copy the code

Some of you may not understand this code, it’s normal, let’s analyze it briefly

const a = () => console.log(1); const b = () => console.log(2); const c = () => console.log(3); compose(a, b, c)(); // Print 3, 2, 1 respectivelyCopy the code

So let’s look at what happens when we compose,

Compose (a, b), compose(... args) => a(b(... The args)); Compose (k, c) is equivalent to (... args) => k(c(... Compose (a,b,c), compose(... args) => a(b(c(... args)))Copy the code

If you still don’t understand it, don’t worry, there’s an iterative way to implement the compose function, so it’s pretty easy. Redux is a little bit more concise.

Then, if they answer and have used koA framework, I will ask what is the principle of KOA middleware and can I write one? How is it different from the synchronous compose function.

The difference is that these functions are asynchronous, so the reduce method above doesn’t apply, for example

const a =  () => setTimeout(()=>console.log(1), 1000);
const b =  () => setTimeout(()=>console.log(2), 500);
const c =  () => setTimeout(()=>console.log(3), 2000);
Copy the code

Obviously, we need an asynchronous compose function to solve the problem, this can also be extended as a wechat interview question, called lazyMan, a very famous question, you can search, asynchronous compose in koA source code has been implemented, let’s see how the framework is implemented:

Suppose there are three asynchronous functions fn1, fn2, and fn3, which are implemented as follows

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]
Copy the code

The next argument is a feature of koa. For example, fn1 calls next and fn2 executes it. Fn2 calls next and fn3 executes it.

This is how KOA handles asynchrony, using the Next approach (there is also a Promise approach described later).

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);Copy the code

All right, so many preambles… Let’s get down to business

Webpack Tapable library

This library is the core of webPack’s hook functions, so why mention it? It’s the ultimate solution for the various compose functions, and we can learn a lot about how the code is encapsulated

For example, for example, compose: “Compose”, “compose”, “compose”, “compose”, “compose”, “compose”, “compose”, “compose”, “compose”

  • Classification:

  • Sync* (Sync version, compose) :

    • SyncHook (serial synchronous execution, not caring about the return value)
    • SyncBailHook (executes synchronously in serial, if the return value is not NULL, the remaining functions do not execute)
    • SyncWaterfallHook (serial synchronous execution, the return value of the former function as the parameter of the latter function, as we have previously redux middleware principle, iterative implementation, simpler)
    • SyncLoopHook (serial synchronous execution where subscribers return true to continue with subsequent functions and undefine to not execute subsequent functions)
  • Async* (asynchronous version, compose) :

  • AsyncParallelHook does not care about the return value, it is just a concurrent asynchronous function, no order requirement

  • AsyncSeriesHook An array of asynchronous functions requires that they be called in sequence

  • AsyncSeriesBailHook Interruptible chain of asynchronous functions

  • AsyncSeriesWaterfallHook Asynchronous serial waterfall hook function

Therefore, once you have mastered these kinds of compose, all types of basic compose will be completely solved. You can kill the interviewer when he talks about these things with you. Generally, he will only consider the two kinds of compose in our preface.

I’ve made very small changes to make the code easier to understand.

Sync * type of Hook

SyncHook

Serial synchronous execution, regardless of the return value

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'); // print // ["hello"] // ["hello"] // ["hello"]Copy the code

Now, if you look at this function up here, you might say, “Well, where’s the compose function,” and we’re going to compose the function, what’s this thing up here?

We can see that tap is a registration function, and call is a call function, in other words, the compose function itself is a combination of the tap registry function and the call function. So we can also change this SyncHook into something called compose

function compose(... fns) { return (... args) => fns.forEach(task=>task(... args)) }Copy the code

For example, for example, “Compose” is used for composing functions, and “compose” is used for composing functions. For example, “Compose” is used for composing functions, and “compose” is used for composing functions.

For example, for example, compose, for example, compose, for example, compose.

SyncBailHook

Serial synchronous execution, bail is the meaning of the fuse, if a return value is not NULL, the rest of the logic is skipped

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'); // Print // hello 1Copy the code

SyncWaterfallHook

Run in serial synchronization, where Waterfall means, the return value from the previous subscriber is passed to the next subscriber

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); // Print // hello 25 1 // 1 2 // 2 3Copy the code

SyncLoopHook

The subscriber returns true to continue the list Loop, and undefined to end the Loop

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 :' continue learning '; }) hook.tap(function(name){ console.log('node',name) }) hook.tap(function(name){ console.log('node',name) }) hook.call('hello'); // Print the React Hello three times, then print the Node Hello, and finally print the Node Hello againCopy the code

Async * type of Hook

AsyncParallelHook

The big difference between parallel asynchronous execution and synchronous execution is that there can be asynchronous logic in the subscribers. It’s just a concurrent asynchronous function, no order requirement

Promise to realize

class AsyncParallelHook{ constructor(name) { this.tasks=[]; this.name = name; } tapPromise(task) { this.tasks.push(task); } promise() { let promises = this.tasks.map(task => task()); Return Promise. All (promises); // Promise. } } 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'); }) // Print // 1 // 2 // 3Copy the code

AsyncSeriesHook

Asynchronous serial hooks, that is, asynchronous functions require calls in sequence

Promise to realize

class AsyncSeriesHook { constructor(name) { this.tasks = []; this.name = name } promise(... args) { let [first, ...others] = this.tasks; Return others.reduce((p, n) => {return p.hen (() => {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'); }); Print // 1 // 2 // 3Copy the code

AsyncSeriesBailHook

Serial asynchronous execution, the bail is a fuse, and if the task is returned, or rejected, it is blocked

The implementation here has a little bit of skill, is how to interrupt reduce, can you see a simple case

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
Copy the code

That’s the way to interrupt reduce — with an if

Promise to realize

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 (reject); reject (reject); reject (reject); }); 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'); }); // Print // 1 // 2Copy the code

AsyncSeriesWaterfallHook

Executed asynchronously in serial, where Waterfall means, the return value from the previous subscriber is passed to the next subscriber

Promise to realize

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)) } }Copy the code

End of article! Welcome to like! 😺