Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.2k views
in Technique[技术] by (71.8m points)

javascript - JS - chain async methods in sequence w/o callback or modification

I'm trying to add a "default callback" to a prototype that will assign a callback function (in the form of a promise) to an async method if none is provided.

The goal is to have a class's chain of async methods run synchronously

Item.async1().async2()....asyncN()

Mind you, the async functions themselves expect a callback but they're not passed as arguments in the function call (which tells me that the class needs a default behavior when the callback lookup fails)

Spec states that I cannot directly modify the behavior or side effects of the prototype methods. I can add prototype methods. We have no visibility into how these prototype methods are implemented.

TLDR: Without modifying the prototype methods, how can you chain an N number of async methods and ensure they run sequentially?

BTW: Promisifying the prototype methods in question would be helpful if I wanted to implement the promisified versions, but it looks like we're constrained to the original function calls

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Well, I wasn't going to answer - but I was challenged.

It's quite easy to utilize the built in abilities of promises in order to get this sort of queuing for free. Here is how this conversion will work:

  • We convert the callback API to promises on a new object with a promise subclass.
  • We add all the promised methods to the subclass itself - so it chains.
    • We tell the subclass to execute all the methods in a then, so they'll queue as then is the queueing mechanism promises have.

Note: The promisify and promisifyAll methods I write here - you should grab off NPM - lots of good and fast usages that take a promise constructor.

First, we need a method that converts a callback API to promises:

// F is a promise subclass
function promisify(fn) { // take a function
    return function(...args) {  // return a new one with promises
      return new F((resolve, reject) => { // that returns a promise
         // that calls the original function and resolves the promise
         fn.call(this, ...args, (err, data) => err ? reject(err) : resolve(data));
      });
    };
  } 

Now, let's promisify the whole object:

  function promisifyAll(obj) {
    const o = {};
    for(const prop in obj) {
      if(!(obj[prop].call)) continue; // only functions
      o[prop] = promisify(obj[prop]).bind(obj);
    }
    return o;
  }

So far, nothing new, lots of NPM libraries do this - now for the magic of promises - let's create a method that executes functions on an original object in a then:

function whenReadyAll(obj) {
    const obj2 = {}; // create a new object
    for(const prop in obj) { // for each original object
       obj2[prop] = function(...args) { 
         // return a function that does the same thing in a `then`
         return this.then(() => obj[prop](...args));
       };
    }
    return obj2;
  }

Now, let's wrap things up

function liquidate(obj) {
  const promised = promisifyAll(obj); // convert the object to a promise API
  class F extends Promise {} // create a promise subclass
  Object.assign(F.prototype, whenReadyAll(promised)); // add the API to it
  return promised; // return it
  // previous code here
}

And that's it, if we want the example to be self contained (again, promise and promisifyAll are provided by a library usually):

function liquidate(obj) {
  const promised = promisifyAll(obj);
  class F extends Promise {}
  Object.assign(F.prototype, whenReadyAll(promised)); // add the API  
  return promised;
  function whenReadyAll(obj) {
    const obj2 = {};
    for(const prop in obj) {
       obj2[prop] = function(...args) { 
         return this.then(() => obj[prop](...args));
       };
    }
    return obj2;
  }
  function promisifyAll(obj) {
    const o = {};
    for(const prop in obj) {
      if(!(obj[prop].call)) continue; // only functions
      o[prop] = promisify(obj[prop]).bind(obj);
    }
    return o;
  }
  function promisify(fn) {
    return function(...args) { 
      return new F((resolve, reject) => {
         fn.call(this, ...args, (err, data) => err ? reject(err) : resolve(data));
      });
    };
  } 
}

Or with a library that does promisify:

function liquidate(obj) { // 14 LoC
  class F extends Promise {} 
  const promised = promisifyAll(obj, F); // F is the promise impl
  Object.assign(F.prototype, whenReadyAll(promised)); // add the API  
  return promised;
  function whenReadyAll(obj) {
    const obj2 = {};
    for(const prop in obj) {
       obj2[prop] = function(...args) { 
         return this.then(() => obj[prop](...args));
       };
    }
    return obj2;
  }
}

And what's an answer without a demo:

var o = {  // object with a delay callback method
  delay(cb) { 
    console.log("delay"); 
    setTimeout(() => cb(null), 1000); 
  }
};
var o2 = liquidate(o); // let's liquidate it
// and we even get `then` for free, so we can verify this works
var p = o2.delay().then(x => console.log("First Delay!")).
                   delay().
                   then(x => console.log("Second Delay!"));

// logs delay, then First Delay! after a second, 
// then delay and then Second Delay! after a second

Copy paste this to your friendly neighborhood console and see for yourself :)


To prove this preserves state on the original object (it's easy to modify it not to if that's a requirement) let's add an i variable and increment it in delay and see that things work:

var o = {  // object with a delay callback method
  delay(cb) { 
    console.log("delay", this.i++); 
    setTimeout(() => cb(null), 1000); 
  },
  i: 0
};
var o2 = liquidate(o); // let's liquidate it
// and we even get `then` for free, so we can verify this works
var p = o2.delay().then(x => console.log("First Delay!")).
                   delay().
                   then(x => console.log("Second Delay!", o.i));
//logs:
// delay 0
// First Delay!
// delay 1
// Second Delay! 2

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...