Koa compose source code reading and learning

Source code warehouse: koa-compose

preface

Do a topic before the beginning of the article. Give a function array and encapsulate a function to execute the functions in the function array in turn

function func1() {
  console.log(1)
}
function func2() {
  console.log(2)
}
function func3() {
  console.log(3)
}
const arr=[func1,func2,func3]
Write one compose Function when we call compose When, execute in sequence func1,func2,func3´╝îPrint out 1, 2, 3, 4
function compose(){
  //your code goes here...
}

We quickly came up with the idea of using a loop to iterate through the data and execute in turn

function compose() {
  for (let item of arr) {
    item()
  }
}
compose()
//Printout:
//1
//2
//3

Of course, this is not the answer we want. We want the function to execute func3(func2(func1())). You can write code like this

//Method 1: use reduce with concise code
function compose1() {
  return arr.reduce((prev, curr) => (...args) => curr(prev(...args)))
}

// Method 2: you can also use loop traversal assignment
function compose2(){
  let prev
  for(let i=0;i<arr.length;i++){
    prev=arr[i](prev)
  }
  return prev || function(){}
}

Let's change it and pass in parameters to the function. The topic becomes as follows

function func1(next){
  console.log(1)
  next()
  console.log(2)
}
function func2(next){
  console.log(3)
  next()
  console.log(4)
}
function func3(next){
  console.log(5)
  next()
  console.log(6)
}
const arr=[func1,func2,func3]
Write one compose Function when we call composeSync When, print out 1,3,5,6,4,2
function composeSync(){
  //your code goes here...
}

Let's first analyze the problem. Each function has a next parameter, and next is a function. Because of the order of printouts, next is the next item in the array. That is, the compose function needs to concatenate each item in the arr array and pass the latter item as a parameter to the current item for execution, so the first half will output 1, 3, 5 And because they are all synchronized code, the following code will not be executed until next() is executed, so 6, 4, 2 are output. After analysis, we can start to write code. The easiest way to think of is recursion

//Method 1: use recursion
const composeSync1=function(){
  function dispatch(index){
    if(index===arr.length) return ;
    return arr[index](()=>dispatch(index+1))
  }
  return dispatch(0)
}
//Method 2: use loops (generally, those that can use recursion can use loops)
const composeSync2=function(){
  let prev=()=>{ }
  for(let i=arr.length-1;i>=0;i--){
    prev=arr[i].bind(this,prev)
  }
  return prev()
} 
composeSync1()
composeSync2()

//Printout:
//1
//3
//5
//6
//4
//2

Unconsciously, we have basically implemented the onion model, which can be used as long as it is slightly improved (fault-tolerant processing, asynchronous processing, etc.). In the next step, we add error handling and asynchronous processing. You can refer to the source code for this

//Fault tolerance: judge whether arr is an array, whether each item of arr is a function, and whether the array length is greater than 0, try Catch() Lower bridging item
//Async: async Await

Koa compose parsing

Let's take it literally. Compose means composition. Its function is to implement the onion model and manage all middleware. koa compose is a middleware of koa. It mainly implements the onion model. From the above questions, we can vaguely think that compose is the prototype of the onion model. Each function in the array is a middleware. The execution mechanism of the onion model and the management mode of the middleware. So what is an onion model? What is middleware?

Onion model and Middleware

  • Onion model: a mechanism for serial processing of data. Similar to an onion, it is wrapped by layers of Middleware (processing data).
  • Middleware: functions, classes, and methods for processing data are scattered in various parts of the model.
    Two stolen pictures:

Source code analysis

'use strict'

/**
 * Expose compositor.
 */

module.exports = compose

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

Let's ignore the comments first. The actual code is less than 20 lines, which is very concise. First, judge whether the middleware passed in is an array and cycle to judge whether each item is a function. Then, in the return function, pass in the parameters context (context object) and next (the function to be executed in the next step, that is, the next item of middleware compared with the current item). The dispatch function is defined to recursively encapsulate func1, func2, and func3 into a func3(func2(func1)) structure. First of all, we need to judge the boundary, compare the length of index and middleware, and define an index to judge whether the current middleware has existed. Finally, return the current item, and pass the next item as a parameter to the current item, so that all middleware can be nested.

Manually implement an onion model

The implementation steps and ideas are the topics we just started to do. Step by step, we can implement a simple version of the onion model. Finally, post the code

const app = { middlewares: [] };
app.use = (fn) => {
   app.middlewares.push(fn);
};

app.compose = function() {
  // Your code goes here
  function dispatch(index){
    if(index===app.middlewares.length) return ;
    const fn=app.middlewares[index];
    return fn(()=>dispatch(index+1))
  }
  dispatch(0);
}
app.use(next => {
   console.log(1);
   next();
   console.log(2);
});
app.use(next => {
   console.log(3);
   next();
   console.log(4);
});
app.use(next => {
   console.log(5);
   next();
   console.log(6);
});
app.compose();

reference

Posted by zak on Tue, 31 May 2022 11:54:48 +0530