Home > Article > Web Front-end > JavaScript asynchronous evolution history
The most basic asynchronous calling method in JS is callback, which passes the callback function callback to the asynchronous API. After the asynchronous completion, the browser or Node Notify the JS engine to call the callback. For simple asynchronous operations, using callback is sufficient. But with the emergence of interactive pages and Node, the disadvantages of the callback solution began to emerge. The Promise specification was born and incorporated into the ES6 specification. Later, ES7 incorporated async functions into the standard based on Promise. This is the history of JavaScript asynchronous evolution.
Usually, the code is executed from top to bottom. If there are multiple tasks, they must be queued. The previous task will be completed before the next task will be executed. This execution mode is called synchronous. Novices can easily confuse synchronization in computer language with synchronization in daily language. For example, synchronization in "synchronizing files to the cloud" refers to "keeping... consistent". In computers, synchronization refers to the mode in which tasks are executed sequentially from top to bottom. For example:
A(); B(); C();
In this code, A, B, and C are three different functions, and each function is an unrelated task. In synchronous mode, the computer will perform task A, then task B, and finally task C. In most cases, sync mode is fine. But if task B is a long-running network request, and task C happens to be displaying a new page, it will cause the web page to freeze.
A better solution is to divide task B into two parts. One part performs tasks requested by the network immediately, and the other part performs tasks after the request comes back. This pattern in which one part is executed immediately and the other part is executed in the future is called asynchronous.
A(); // 在现在发送请求 ajax('url1',function B() { // 在未来某个时刻执行 }) C(); // 执行顺序 A => C => B
In fact, the JS engine does not directly process network requests. It just calls the browser's network request interface, and the browser sends network requests and monitors the returned data. The essence of JavaScript's asynchronous capabilities is the multi-threading capabilities of the browser or Node.
The function that will be executed in the future is usually called callback. Using the asynchronous mode of callback solves the blocking problem, but it also brings some other problems. At the beginning, our functions were written from top to bottom and executed from top to bottom. This "linear" mode is very consistent with our thinking habits, but now it is interrupted by callback! In the above piece of code, now it skips task B and executes task C first! This kind of asynchronous "non-linear" code will be more difficult to read than synchronous "linear" code, and therefore more likely to breed bugs.
Try to judge the execution order of the following code. You will have a deeper understanding that "non-linear" code is more difficult to read than "linear" code.
A(); ajax('url1', function(){ B(); ajax('url2', function(){ C(); } D(); }); E(); // A => E => B => D => C
In this code, the order of execution from top to bottom is disrupted by Callback. Our line of sight when reading the code is A => B => C => D => E, but the execution order is A => E => B => D => C. This is not The bad things about linear code.
By advancing the tasks executed after ajax, it is easier to understand the execution order of the code. Although the code looks ugly due to nesting, the order of execution is now "linear" from top to bottom. This technique is very useful when writing multiple nested code.
A(); E(); ajax('url1', function(){ B(); D(); ajax('url2', function(){ C(); } }); // A => E => B => D => C
The previous code only handles the success callback and does not handle the exception callback. Next, add the exception handling callback, and then discuss the issue of "linear" execution of the code.
A(); ajax('url1', function(){ B(); ajax('url2', function(){ C(); },function(){ D(); }); },function(){ E(); });
After adding the exception handling callback, the success callback function B and the exception callback function E of url1 are separated. This "non-linear" situation appears again.
In node, in order to solve the "non-linear" problem caused by abnormal callbacks, an error-first strategy has been formulated. The first parameter of callback in node is specifically used to determine whether an exception occurs
A(); get('url1', function(error){ if(error){ E(); }else { B(); get('url2', function(error){ if(error){ D(); }else{ C(); } }); } });
At this point, the "nonlinear" problem caused by callback has basically been solved. Unfortunately, using callback nesting, layers of if else and callback functions, once the number of nested layers increases, it is not very convenient to read. In addition, once an exception occurs in callback, the exception can only be handled within the current callback function.
In the history of asynchronous evolution of JavaScript, a series of libraries have emerged to solve the shortcomings of callback, and Promise became the final winner and was successfully introduced into ES6. It will provide a better "linear" way of writing and solve the problem that asynchronous exceptions can only be caught in the current callback.
Promise is like an intermediary that promises to return a trustworthy asynchronous result. First, Promise signs an agreement with the asynchronous interface. When successful, the resolve function is called to notify Promise. When an exception occurs, reject is called to notify Promise. On the other hand, Promise and callback also sign an agreement, and Promise will return a trusted value to the callback registered in then and catch in the future.
// 创建一个 Promise 实例(异步接口和 Promise 签订协议) var promise = new Promise(function (resolve,reject) { ajax('url',resolve,reject); }); // 调用实例的 then catch 方法 (成功回调、异常回调与 Promise 签订协议) promise.then(function(value) { // success }).catch(function (error) { // error })
Promise 是个非常不错的中介,它只返回可信的信息给 callback。它对第三方异步库的结果进行了一些加工,保证了 callback 一定会被异步调用,且只会被调用一次。
var promise1 = new Promise(function (resolve) { // 可能由于某些原因导致同步调用 resolve('B'); }); // promise依旧会异步执行 promise1.then(function(value){ console.log(value) }); console.log('A'); // A B (先 A 后 B) var promise2 = new Promise(function (resolve) { // 成功回调被通知了2次 setTimeout(function(){ resolve(); },0) }); // promise只会调用一次 promise2.then(function(){ console.log('A') }); // A (只有一个) var promise3 = new Promise(function (resolve,reject) { // 成功回调先被通知,又通知了失败回调 setTimeout(function(){ resolve(); reject(); },0) }); // promise只会调用成功回调 promise3.then(function(){ console.log('A') }).catch(function(){ console.log('B') }); // A(只有A)
介绍完 Promise 的特性后,来看看它如何利用链式调用,解决异步代码可读性的问题的。
var fetch = function(url){ // 返回一个新的 Promise 实例 return new Promise(function (resolve,reject) { ajax(url,resolve,reject); }); } A(); fetch('url1').then(function(){ B(); // 返回一个新的 Promise 实例 return fetch('url2'); }).catch(function(){ // 异常的时候也可以返回一个新的 Promise 实例 return fetch('url2'); // 使用链式写法调用这个新的 Promise 实例的 then 方法 }).then(function() { C(); // 继续返回一个新的 Promise 实例... }) // A B C ...
如此反复,不断返回一个 Promise 对象,再采用链式调用的方式不断地调用。使 Promise 摆脱了 callback 层层嵌套的问题和异步代码“非线性”执行的问题。
Promise 解决的另外一个难点是 callback 只能捕获当前错误异常。Promise 和 callback 不同,每个 callback 只能知道自己的报错情况,但 Promise 代理着所有的 callback,所有 callback 的报错,都可以由 Promise 统一处理。所以,可以通过catch来捕获之前未捕获的异常。
Promise 解决了 callback 的异步调用问题,但 Promise 并没有摆脱 callback,它只是将 callback 放到一个可以信任的中间机构,这个中间机构去链接我们的代码和异步接口。
异步(async)函数是 ES7 的一个新的特性,它结合了 Promise,让我们摆脱 callback 的束缚,直接用类同步的“线性”方式,写异步函数。
声明异步函数,只需在普通函数前添加一个关键字 async 即可,如async function main(){} 。在异步函数中,可以使用await关键字,表示等待后面表达式的执行结果,一般后面的表达式是 Promise 实例。
async function main{ // timer 是在上一个例子中定义的 var value = await timer(100); console.log(value); // done (100ms 后返回 done) } main();
异步函数和普通函数一样调用 main() 。调用后,会立即执行异步函数中的第一行代码 var value = await timer(100) 。等到异步执行完成后,才会执行下一行代码。
除此之外,异步函数和其他函数基本类似,它使用try...catch来捕捉异常。也可以传入参数。但不要在异步函数中使用return来返回值。
var timer = new Promise(function create(resolve,reject) { if(typeof delay !== 'number'){ reject(new Error('type error')); } setTimeout(resolve,delay,'done'); }); async function main(delay){ try{ var value1 = await timer(delay); var value2 = await timer(''); var value3 = await timer(delay); }catch(err){ console.error(err); // Error: type error // at create (<anonymous>:5:14) // at timer (<anonymous>:3:10) // at A (<anonymous>:12:10) } } main(0);
异步函数也可以被当作值,传入普通函数和异步函数中执行。但是在异步函数中,使用异步函数时要注意,如果不使用await,异步函数会被同步执行。
async function main(delay){ var value1 = await timer(delay); console.log('A') } async function doAsync(main){ main(0); console.log('B') } doAsync(main); // B A
这个时候打印出来的值是 B A。说明 doAsync 函数并没有等待 main 的异步执行完毕就执行了 console。如果要让 console 在 main 的异步执行完毕后才执行,我们需要在main前添加关键字await。
async function main(delay){ var value1 = await timer(delay); console.log('A') } async function doAsync(main){ await main(0); console.log('B') } doAsync(main); // A B
由于异步函数采用类同步的书写方法,所以在处理多个并发请求,新手可能会像下面一样书写。这样会导致url2的请求必需等到url1的请求回来后才会发送。
var fetch = function (url) { return new Promise(function (resolve,reject) { ajax(url,resolve,reject); }); } async function main(){ try{ var value1 = await fetch('url1'); var value2 = await fetch('url2'); conosle.log(value1,value2); }catch(err){ console.error(err) } } main();
使用Promise.all的方法来解决这个问题。Promise.all用于将多个Promise实例,包装成一个新的 Promis e实例,当所有的 Promise 成功后才会触发Promise.all的resolve函数,当有一个失败,则立即调用Promise.all的reject函数。
var fetch = function (url) { return new Promise(function (resolve,reject) { ajax(url,resolve,reject); }); } async function main(){ try{ var arrValue = await Promise.all[fetch('url1'),fetch('url2')]; conosle.log(arrValue[0],arrValue[1]); }catch(err){ console.error(err) } } main();
目前使用 Babel 已经支持 ES7 异步函数的转码了,大家可以在自己的项目中开始尝试。
以上就是JavaScript 异步进化史的内容,更多相关内容请关注PHP中文网(www.php.cn)!