preface

The basic use of Promise can be found in Teacher Ruan Yifeng’s introduction to ECMAScript 6.

Let’s talk about something else.

The callback

When we talk about promises, we usually start with callbacks or callback hell, so what’s the downside of using callbacks?

1. Callback nesting

With callbacks, we would most likely write the business code as follows:

doA( function(){
    doB();

    doC( function(){ doD(); } ) doE(); }); doF();Copy the code

Of course, this is a simplified form. After some simple thinking, we can determine the order of execution as follows:

doA()
doF()
doB()
doC()
doE()
doD()
Copy the code

However, in actual projects, the code will be more chaotic. In order to troubleshoot problems, we need to bypass many eyesores and constantly jump between functions, making the difficulty of troubleshooting problems doubled.

The problem, of course, is that nesting is so incompatible with linear thinking that we have to spend more time thinking about the actual order of execution. Nesting and indentation are just red herring in the process.

Of course, it’s not the worst thing that goes against people’s linear thinking. In fact, we add all kinds of logical judgments to our code, like in the example above, doD() must be completed after doC(), and what if doC() fails? Are we trying doC() again? Or do you just go to some other error handler? When we add these judgments to the process, the code quickly becomes too complex to maintain and update.

2. Inversion of control

When we write code normally, we have control over our code. However, when we use a callback, whether the callback will continue to execute depends on the API using the callback, as in:

// Whether the callback function is executed depends on the Buy module
import {buy} from './buy.js';

buy(itemData, function(res) {
    console.log(res)
});
Copy the code

There is no problem with the FETCH API that we often use, but what if we use a third-party API?

When you call a third party’s API, will the other party execute your callback more than once because of some error?

To avoid this problem, you can add judgments to your own callback function, but what if the callback is not executed because of some error? What if the callback sometimes executes synchronously and sometimes asynchronously?

Let’s summarize the situation:

  1. The callback function is executed multiple times
  2. The callback function did not execute
  3. Callbacks are sometimes executed synchronously and sometimes asynchronously

In each of these cases, you’ll probably have to do something in the callback, and every time the callback is executed, you’ll have to do something, which leads to a lot of duplicate code.

The callback hell

Let’s start with a simple example of callback hell.

Now to find the largest file in a directory, the procedure should be:

  1. withfs.readdirGet a list of files in the directory;
  2. Loop through the file usingfs.statObtaining File Information
  3. Compare to find the largest file;
  4. Invokes the callback with the file name parameter of the largest file.

The code is:

var fs = require('fs');
var path = require('path');

function findLargest(dir, cb) {
    // Read all files in the directory
    fs.readdir(dir, function(er, files) {
        if (er) return cb(er);

        var counter = files.length;
        var errored = false;
        var stats = [];

        files.forEach(function(file, index) {
            // Read file information
            fs.stat(path.join(dir, file), function(er, stat) {

                if (errored) return;

                if (er) {
                    errored = true;
                    return cb(er);
                }

                stats[index] = stat;

                // Count the number of files, read 1 file information, count minus 1, when 0, it is finished reading, then perform the final comparison operation
                if (--counter == 0) {

                    var largest = stats
                        .filter(function(stat) { return stat.isFile() })
                        .reduce(function(prev, next) {
                            if (prev.size > next.size) return prev
                            return next
                        })

                    cb(null, files[stats.indexOf(largest)])
                }
            })
        })
    })
}
Copy the code

Use mode:

// Find the largest file in the current directory
findLargest('/'.function(er, filename) {
    if (er) return console.error(er)
    console.log('largest file was:', filename)
});
Copy the code

You can copy the above code into a file such as index.js, and then execute Node index.js to print out the name of the largest file.

After this example, let’s talk about some other problems with callback hell:

1. Hard to reuse

Once the order of callbacks is determined, it is difficult to reuse some of them.

For example, if you want to read file information from fs.stat, this code will be reused because the callback refers to the outer variable, which will need to be extracted from the outer layer.

2. The stack information is disconnected

As we know, the JavaScript engine maintains a stack of execution contexts that are created and pushed onto the stack when a function executes, and then pushed off the stack when the function completes execution.

If function B is called from function A, JavaScript pushes the execution context of function A onto the stack, then pushes the execution context of function B onto the stack, and then pushes the execution context of function A off the stack when function A is finished.

The advantage of this is that if we interrupt code execution, we can retrieve the entire stack and get whatever information we want from it.

Asynchronous callback function is not the case, however, such as the execution of fs, readdir, actually is to add a callback function to task queue, continue to execute code, until after completion of the main thread, can choose the task has been completed, from the task queue and add it to the stack and the stack, only that an execution context, if the correction error, There is no way to get information on the stack where the asynchronous operation was called, and it is not easy to determine where an error occurred.

Also, because it is asynchronous, you cannot catch errors directly with a try catch statement.

(Promise didn’t fix that, though.)

3. Use outer variables

When multiple asynchronous computations are performed simultaneously, as in this case, the order in which the file is read through is unpredictable, variables in the outer scope must be used, such as count, ERrored, stats, etc., which is not only difficult to write, but also if you ignore the file read error and do not record the error status. It will then read other files, causing unnecessary waste. In addition, outer variables may also be accessed and modified by other functions in the same scope, which is prone to misoperation.

The reason I mention callback hell alone is to say that nesting and indentation are just a shorthand for callback hell, and the problems it causes go beyond the readability of nesting.

Promise

Promise solved most of these problems.

1. Nesting problem

Here’s an example:

request(url, function(err, res, body) {
    if (err) handleError(err);
    fs.writeFile('1.txt', body, function(err) {
        request(url2, function(err, res, body) {
            if (err) handleError(err)
        })
    })
});
Copy the code

After using Promise:

request(url)
.then(function(result) {
    return writeFileAsynv('1.txt', result)
})
.then(function(result) {
    return request(url2)
})
.catch(function(e){
    handleError(e)
});
Copy the code

For the example of reading the largest file, we use promise to simplify:

var fs = require('fs');
var path = require('path');

var readDir = function(dir) {
    return new Promise(function(resolve, reject) {
        fs.readdir(dir, function(err, files) {
            if (err) reject(err);
            resolve(files)
        })
    })
}

var stat = function(path) {
    return new Promise(function(resolve, reject) {
        fs.stat(path, function(err, stat) {
            if (err) reject(err)
            resolve(stat)
        })
    })
}

function findLargest(dir) {
    return readDir(dir)
        .then(function(files) {
            let promises = files.map(file= > stat(path.join(dir, file)))
            return Promise.all(promises).then(function(stats) {
                return { stats, files }
            })
        })
        .then(data= > {

            let largest = data.stats
                .filter(function(stat) { return stat.isFile() })
                .reduce((prev, next) = > {
                    if (prev.size > next.size) return prev
                    return next
                })

            return data.files[data.stats.indexOf(largest)]
        })

}
Copy the code

2. Reverse control and reverse control

When we talked about using third-party callback apis, we might encounter the following problems:

  1. The callback function is executed multiple times
  2. The callback function did not execute
  3. Callbacks are sometimes executed synchronously and sometimes asynchronously

For the first problem, the Promise can only resolve once, and the rest of the calls are ignored.

For the second problem, we can solve it using the promise.race function:

function timeoutPromise(delay) {
    return new Promise( function(resolve,reject){
        setTimeout( function(){
            reject( "Timeout!"); }, delay ); }); }Promise.race( [
    foo(),
    timeoutPromise( 3000 )
] )
.then(function(){}, function(err){});
Copy the code

And the third question, why do you do it synchronously sometimes and asynchronously sometimes?

Let’s look at an example:

varcache = {... };function downloadFile(url) {
      if(cache.has(url)) {
            // If cache exists, this is synchronous call
           return Promise.resolve(cache.get(url));
      }
     return fetch(url).then(file= > cache.set(url, file)); // This is an asynchronous call
}
console.log('1');
getValue.then((a)= > console.log('2'));
console.log('3');
Copy the code

In this example, it prints 1, 2, 3 with cahCE, and 1, 3, 2 with no cache.

However, if the synchronous and asynchronous code is used as an internal implementation, only the interface is exposed to the external call, the caller can not determine whether it is asynchronous or synchronous state, affecting the maintainability and testability of the program.

Simply put, synchronous and asynchronous coexistence does not guarantee consistency of program logic.

Promise solves this problem, though. Here’s an example:

var promise = new Promise(function (resolve){
    resolve();
    console.log(1);
});
promise.then(function(){
    console.log(2);
});
console.log(3);

/ / 1 2 3
Copy the code

Even if the Promise object goes into the Resolved state immediately, calling the resolve function synchronously, the methods specified in the then function will still be done asynchronously.

The PromiseA+ specification also specifies:

In practice, ensure that the onFulfilled and onRejected methods are executed asynchronously and should be executed in a new execution stack after the event loop in which the THEN method is called.

Promise anti-patterns

1. Promise nesting

// bad
loadSomething().then(function(something) {
    loadAnotherthing().then(function(another) {
        DoSomethingOnThem(something, another);
    });
});
Copy the code
// good
Promise.all([loadSomething(), loadAnotherthing()])
.then(function ([something, another]) { DoSomethingOnThem(... [something, another]); });Copy the code

2. Broken Promise chains

// bad
function anAsyncCall() {
    var promise = doSomethingAsync();
    promise.then(function() {
        somethingComplicated();
    });

    return promise;
}
Copy the code
// good
function anAsyncCall() {
    var promise = doSomethingAsync();
    return promise.then(function() {
        somethingComplicated()
    });
}
Copy the code

3. A chaotic collection

// bad
function workMyCollection(arr) {
    var resultArr = [];
    function _recursive(idx) {
        if (idx >= resultArr.length) return resultArr;

        return doSomethingAsync(arr[idx]).then(function(res) {
            resultArr.push(res);
            return _recursive(idx + 1);
        });
    }

    return _recursive(0);
}
Copy the code

You can write it as:

function workMyCollection(arr) {
    return Promise.all(arr.map(function(item) {
        return doSomethingAsync(item);
    }));
}
Copy the code

If you must execute in a queue, you can write:

function workMyCollection(arr) {
    return arr.reduce(function(promise, item) {
        return promise.then(function(result) {
            return doSomethingAsyncWithResult(item, result);
        });
    }, Promise.resolve());
}
Copy the code

4.catch

// bad
somethingAync.then(function() {
    return somethingElseAsync();
}, function(err) {
    handleMyError(err);
});
Copy the code

If somethingElseAsync throws an error, it cannot be caught. You can write it as:

// good
somethingAsync
.then(function() {
    return somethingElseAsync()
})
.then(null.function(err) {
    handleMyError(err);
});
Copy the code
// good
somethingAsync()
.then(function() {
    return somethingElseAsync();
})
.catch(function(err) {
    handleMyError(err);
});
Copy the code

Traffic light problem

Title: red light once every three seconds, green light once every second, yellow light once every two seconds; How do I make three lights turn on again and again? (Implemented with Promse)

Three lighting functions already exist:

function red(){
    console.log('red');
}
function green(){
    console.log('green');
}
function yellow(){
    console.log('yellow');
}
Copy the code

Using THEN and recursion:

function red(){
    console.log('red');
}
function green(){
    console.log('green');
}
function yellow(){
    console.log('yellow');
}

var light = function(timmer, cb){
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            cb();
            resolve();
        }, timmer);
    });
};

var step = function() {
    Promise.resolve().then(function(){
        return light(3000, red);
    }).then(function(){
        return light(2000, green);
    }).then(function(){
        return light(1000, yellow);
    }).then(function(){
        step();
    });
}

step();
Copy the code

promisify

Sometimes we need to transform the API of the callback syntax into the Promise syntax, for which we need a promisify method.

The last argument is passed to the callback function, and the first argument is an error message (null if there is no error), so we can write a simple method called promisify:

function promisify(original) {
    return function (. args) {
        return new Promise((resolve, reject) = > {
            args.push(function callback(err, ... values) {
                if (err) {
                    return reject(err);
                }
                returnresolve(... values) }); original.call(this. args); }); }; }Copy the code

See ES6-PromisIf for complete information

Limitations of Promises

1. Get eaten by mistake

So the first thing we need to understand is, what is error eaten, does it mean that the error message is not printed?

No, here’s an example:

throw new Error('error');
console.log(233333);
Copy the code

In this case, because of throw error, the code is blocked and 233333 is not printed. Here’s another example:

const promise = new Promise(null);
console.log(233333);
Copy the code

The above code still blocks execution because if a Promise is used in an invalid way and an error blocks the normal Promise construction, the result is an exception that runs out immediately, rather than a rejected Promise.

But here’s another example:

let promise = new Promise((a)= > {
    throw new Error('error')});console.log(2333333);
Copy the code

This time, 233333 will be printed as normal, indicating that errors inside the Promise will not affect the code outside the Promise, which is often referred to as “error eating.”

This isn’t the only limitation of promises, try.. The same goes for catch, which also catches an exception and simply eats the error.

Because errors are eaten, errors in the Promise chain are easy to ignore, which is why it is generally recommended to add a catch function at the end of the Promise chain, because for a Promise chain without an error handler, any errors will be propagated through the chain. Until you register your error handlers.

2. A single value

Promise can only have one completion value or one reason for rejection. However, in real use, multiple values are often needed to be transferred. The general approach is to construct an object or array, and then pass, and after obtaining this value in THEN, the operation of value assignment will be conducted.

To be honest, there is no good way to do this. The suggestion is to use ES6’s deconstructive assignment:

Promise.all([Promise.resolve(1), Promise.resolve(2)])
.then(([x, y]) = > {
    console.log(x, y);
});
Copy the code

3. Unable to cancel

Once a Promise is created, it executes immediately and cannot be canceled.

4. The pending status cannot be known

When you are in a pending state, you have no way of knowing what stage of progress you are currently in (just started or about to complete).

reference

  1. JavaScript You Didn’t Know By Volume
  2. N uses of Promise
  3. JavaScript Promise mini-book
  4. Promises/A + specification
  5. How Promise is used
  6. Promise Anti-patterns
  7. An interview question about the Promise application

ES6 series

ES6 directory address: github.com/mqyqingfeng…

ES6 series is expected to write about 20 chapters, aiming to deepen the understanding of ES6 knowledge points, focusing on the block-level scope, tag template, arrow function, Symbol, Set, Map and Promise simulation implementation, module loading scheme, asynchronous processing and other contents.

If there is any mistake or not precise place, please be sure to give correction, thank you very much. If you like or are inspired by it, welcome star and encourage the author.