Callback hell
For JavaScript programmers, handling callbacks is homely, but handling callbacks with too deep levels is not that beautiful. The following sample code snippet uses three-layer callbacks, and then replenishes the brain for more layers of scenes. It is simply sour. This is the legendary callback hell.
getDirectories(function(dirs) { getFiles(dirs[0], function(files) { getContent(files[0], function(file, content) { console.log('filename:', file); console.log(content); }); }); }); function getDirectories(callback) { setTimeout(function() { callback(['/home/ben']); }, 1000);} function getFiles(dir, callback) { setTimeout(function() { callback([dir + '/test1.txt', dir + '/test2.txt']); }, 1000)} function getContent(file, callback) { setTimeout(function() { callback(file, 'content'); }, 1000)}Solution
There are many asynchronous solutions in the ecosystem that can handle the problem of callback hell, such as bluebird, Q, etc. This article focuses on the support for asynchronous programming in the ECMAScript 6/7 specification.
ES6 Promise
Promise is a solution for asynchronous programming and a powerful tool to solve the callback hell problem.
Promise was accepted mainstreamly in the JavaScript ecosystem in 2007 when the Dojo framework added dojo.Deferred's functionality. With the popularity of dojo.Deferred, in 2009, Kris Zyp proposed the CommonJS Promises/A specification. Subsequently, a large number of Promise implementations appeared in the ecosystem, including Q.js, FuturesJS, etc. Of course, the popularity of Promise is largely due to the existence of jQuery, but jQuery does not fully comply with the CommonJS Promises/A specification. Then as you can see, the ES 6 specification includes Promise.
Promise is described in MDN as follows:
The Promise object is a proxy for the return value, which may not be known when the promise object is created. It allows you to specify a handling method for success or failure of an asynchronous operation. This allows the asynchronous method to return a value like a synchronous method: the asynchronous method returns a containing the original return value
The following code is an example in the "Callback Hell" section implemented through Promise. The code does not look very concise, but it has significantly improved compared to traditional hierarchical callbacks, and the code is more maintainable and readable.
getDirectories().then(function(dirs) { return getFiles(dirs[0]);}).then(function(files) { return getContent(files[0]);}).then(function(val) { console.log('filename:', val.file); console.log(val.content);}); function getDirectories() { return new Promise(function (resolve, reject) { setTimeout(function() { resolve(['/home/ben']); }, 1000); });} function getFiles(dir) { return new Promise(function (resolve, reject) { setTimeout(function() { resolve([dir + '/test1.txt', dir + '/test2.txt']); }, 1000); });} function getContent(file) { return new Promise(function (resolve, reject) { setTimeout(function() { resolve({file: file, content: 'content'}); }, 1000); });}ES6 Generator
The implementation of Promise is not simple enough, we still need a better choice, and co is one of the choices. co is an asynchronous flow controller based on Generator. Before understanding co, you need to understand Generator first. Students who are familiar with C# should all understand that the C# 2.0 version introduces the yield keyword to iterate the generator. ES 6 Generator is similar to C#, and also uses yield syntax sugar, and implements a state machine internally. For specific usage, please refer to the MDN document function* section, and refer to the AlloyTeam team Blog for in-depth understanding of Generator. Use co to cleverly combine ES6 Generator and ES6 Promise to make asynchronous calls more harmonious.
co(function* (){ var dirs = yield getDirectories(); var files = yield getFiles(dirs[0]); var contentVal = yield getContent(files[0]); console.log('filename:', contentVal.file); console.log(contentVal.content);});co is very clever, and its core code can simplify the following example. The general idea is to use recursive traversal generator until the state is completed, of course co does more.
runGenerator(); function* run(){ var dirs = yield getDirectories(); var files = yield getFiles(dirs[0]); var contentVal = yield getContent(files[0]); console.log('filename:', contentVal.file); console.log(contentVal.content);} function runGenerator(){ var gen = run(); function go(result){ if(result.done) return; result.value.then(function(r){ go(gen.next(r)); }); } go(gen.next());}ES7 Async/Await
ES6 Generator is really good, but it’s a pity that I need the support of a third-party library. The good news is that ES 7 will introduce the Async/Await keyword to perfectly solve the problem of asynchronous calls. Well, .net is one step ahead, and .net framework 4.5 is already the first to support it.
The future code will be written like this:
run();async function run() { var dirs = await getDirectories(); var files = await getFiles(dirs[0]); var contentVal = await getContent(files[0]); console.log('filename:', contentVal.file); console.log(contentVal.content);}in conclusion
From the classic callback asynchronous programming method, to the improvement of asynchronous programming by the ES6 Promise specification, to the elegant processing of co-combined with ES Generator, and finally the perfect ending of ES7 async/await, we can understand why ECMAScript has these features and what problems it solves, and see more clearly the development of JavaScript asynchronous programming.