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! 😺