Promise chaining - should all functions need to be

2019-08-22 23:08发布

问题:

Assuming I have three asynchronous functions to be chained together, func1 --> func2 --> func3, it seems there are two ways to do it.

Option 1: promisify the first function, func1p = Promise.promisify(func1), leave the other two alone, and then create a chain like this:

func1p
  .then(func2)
  .then(func3)
  .catch(err)

Option 2: promisify all three functions, func1p = Promise.promisify(func1), func2p = Promise.promisify(func2), func3p = Promise.promisify(func3) and then create a chain like this:

func1p
  .then(func2p)
  .then(func3p)
  .catch(err)

According to MDN Web Docs, the then function (always) returns a new promise. So it seems to me it's not necessary to promisify func2 and func3 because they would have already been 'promisified' by the then function of the func1p. In other words, option 1 should be enough.

I've tested with both options and both seemed to work and gave the same result to me. My concern, however, is if there's any difference in performance, overhead etc. between the two and if so, which one is better.

My questions:

  • Is it a good practice to always promisify all functions and so I should use option 2 instead, or it's the other way around?
  • If option 1 is preferred, is it still true if func2 and func3 are to be created from the beginning?

EDIT: Here are my code (I used Express JS and request-promise)

EDIT2: When I asked this question I assumed my functions are asynchronous and the question is about what I should do with them. I didn't expect it to turn into a discussion about whether my functions are actual asynchronous or not. Imho they may or may not be but there is always the possibility that they will be asynchronous the next time and when I'm not sure, I'd rather assume that they are to be safe. So just to be clear, I edited my code to indicate that func1, func2, func3 are indeed asynchronous in this context.

 function func1 (filePath1) {
    fs.readFile(filePath1, (err, data1) => {
        if (err) throw err;
        console.log(data1);
    });
 }

 function func2 (filePath2) {
    fs.readFile(filePath2, (err, data2) => {
        if (err) throw err;
        console.log(data2);
    });
 }     

 function func3 (filePath3) {
    fs.readFile(filePath3, (err, data3) => {
        if (err) throw err;
        console.log(data3);
    });
 }

回答1:

The question has become a bit broad because you're still learning a lot of this stuff. :-) Here are the three parts I see that I think I can now reasonably answer:

  1. When should I use Promise.promisify vs. just using then? What do they do differently from one another?

  2. Can I use synchronous functions with then?

  3. How should I write asynchronous functions so that I can use them with promises?

1. When should I use Promise.promisify vs. just using then? What do they do differently from one another?

Promise.promisify is a function provided by Bluebird and is not part of the standard JavaScript Promise object. Its job is to create a wrapper function around a standard Node-callback-style function* that provides a promise instead. It accepts a function and returns a new function wrapped around it.

then is a standard feature of promises that hooks up a callback to the promise's resolution (and optionally a handler to its rejection). It accepts a function and returns a new promise that will resolve or reject depending on what the function you give it does.

They're completely unrelated, other than both involving promises. They do completely different things. The only reason for using Promise.promisify is if you have to deal with legacy Node-style-callback functions (like those in the majority of the Node API, since it predates promises) and want to use promises with them. In contrast, you use then any time you use promises.

2. Can I use synchronous functions with then?

Yes. There's no particular reason to, but you can. The function you pass then gets called with the resolution value of the promise (or its rejection value if you pass the callback as the second argument) as its only argument. If your then callback returns a promise, then makes the promise it creates resolve or reject based on the promise the function returns; if the then callback returns a non-promise value, the promise then creates is resolved with that value.

3. How should I write asynchronous functions so that I can use them with promises?

Make your function return a promise.

Looking at your func1, for instance, it doesn't return a promise and in fact it doesn't work properly:

// Your `func1` from the question
function func1 (filePath1) {
    fs.readFile(filePath1, (err, data1) => {
        if (err) throw err;
        console.log(data1);
    });
}

The reason it doesn't work properly is that it throws from the readFile callback. But throws in that callback are not handled by anything usefully.

To write that function to be used with promises, you'd have it return a promise. You can do that by writing it like this:

// `func1` updated to use promises, and also to accept options
function func1(filePath1, options) {
    return new Promise((resolve, reject) => {
        fs.readFile(filePath1, options, (err, data1) => {
            if (err) {
                reject(err);
            } else {
                resolve(data1);
            }
        });
    });
}

...or, if you're using Bluebird, by simply using Promise.promisify, since all your function does is call readFile:

const func1 = Promise.promisify(fs.readFile);

If you're not using Bluebird, or if you want to promisify entire APIs (like the whole of fs) in one go, you might look at the promisify package instead.


* A standard "Node-callback-style function" is one that

  1. Accepts arguments where the last in the list is a callback function
  2. oes its work asynchronously, then
  3. Calls the callback with an initial argument which is either an error or null, followed (if it's null) by the result of the asynchronous call.

Let's look at an example: fs.readFile: It accepts the path of the file to read, an optional options object, and as the last argument a callback to call with the results. When it calls the callback, if there was an error, it passes that error as the first argument and no second argument at all. If there wasn't an error, it calls the callback with two arguments: null, and the file data.

So there's a pattern there:

  • The last argument to the API function is the callback
  • The first argument to the callback is an error or null

Promise.promisify puts a promise-enabled wrapper around any function that works in that way.

So: When should you use it? Any time you want to use promises with a Node-callback-style function.