理解Javascript的异步

更新日期: 2020-02-22阅读: 1.5k标签: 异步

总括: 本文梳理了异步代码和同步代码执行的区别,Javascript的事件循环,任务队列微任务队列等概念。


未曾失败的人恐怕也未曾成功过。

Javascript是单线程的编程语言,单线程就是说同一时间只能干一件事。放到编程语言上来说,就是说Javascript引擎(执行Javascript代码的虚拟机)同一时间只能执行一条语句。

单线程语言的好处是你只管写不用担心并发问题。但这也意味着无法在不阻塞主线程的情况下去执行一些诸如网络请求的长时间操作。

设想下如果我们从某个接口请求一些数据,然后服务器需要一些时间才能将数据返回,此时就会阻塞主线程页面处于无响应的状态。

这里就是Javascript异步的用武之地了,我们可以通过异步操作(比如回调函数,promise和async/await)来执行长时间的网络请求而不阻塞主线程。

虽然说了解这些所有的概念不一定让你立刻成为一名出色的Javascript开发者,但了解异步会对你很有帮助。

话不多说,正文开始:)

同步的代码是怎么执行的

在深入研究Javascript的异步之前,我们先来看下同步的代码是如何在Javascript引擎中执行的。看例子:

const second = () => {
  console.log('Hello there!');
}
const first = () => {
  console.log('Hi there!');
  second();
  console.log('The End');
}
first();

要想理解上面的代码是如何在Javascript引擎中被执行的,我们必须要去理解Javascript的执行上下文和执行栈

执行上下文

所谓的执行上下文是Javascript代码执行环境中的一个抽象的概念。Javascript任何代码都是在执行上下文中执行的。

函数内部的代码会在函数执行上下文中执行,全局的代码会在全局执行上下文中执行,每一个函数都有自己的执行上下文。

执行栈

顾名思义执行栈是一种后进先出(LIFO)的栈结构,它用来存储在代码执行阶段创建的所有的执行上下文。

基于单线程的原因,Javascript只有一个执行栈,因为是基于栈结构所以只能从栈的顶层添加或是删除执行上下文。

让我们回到上面的代码,尝试理解Javascript引擎是如何去执行它们的。

const second = () => {
  console.log('Hello there!');
}
const first = () => {
  console.log('Hi there!');
  second();
  console.log('The End');
}
first();


<div align="center">上述代码的执行栈</div>

所以这里发生了什么呢?

当代码被执行时,首先一个全局执行上下文(这里用main()表示)被创建然后压到执行栈的顶端。当执行到first()这一行代码,它的执行上下文被压到执行栈的顶端。

紧接着,console.log('Hi there!');的函数执行上下文被压到执行栈的顶端,执行结束后该执行上下文从执行栈弹出。然后调用second()函数,该函数的执行上下文被压到执行栈的顶端。

然后执行console.log('Hello there!');,对应的函数执行上下文被压入执行栈,执行结束被弹出,然后second()函数执行结束,执行上下文被弹出。

console.log(‘The End’)执行,函数执行上下文被压入执行栈,执行结束被弹出,此时first()函数执行结束,对应执行上下文被弹出。

整个程序执行结束,全局执行上下文(main())被弹出。

异步代码是怎么执行的

现在我们已经对同步代码的执行有了一个基本的认知,下面让我们看下异步代码是如何执行的:

阻塞

假设我们用同步的方式去发起一个图片请求或是一个普通的网络请求,例子如下:

const processImage = (image) => {
  /**
  * doing some operations on image
  **/
  console.log('Image processed');
}
const networkRequest = (url) => {
  /**
  * requesting network resource
  **/
  return someData;
}
const greeting = () => {
  console.log('Hello World');
}
processImage(logo.jpg);
networkRequest('www.somerandomurl.com');
greeting();

请求图片或是网络请求是需要花费时间的,因此当我们调用processImage()的时候,花费的时间取决于图片的大小。

当processImage()函数执行结束,响应的执行上下文从执行栈中弹出,然后调用networkRequest()函数,对应执行上下文被压入执行栈,该函数同样需要花费一些时间才能结束。

networkRequest()函数执行结束,调用greeting(),然后里面只有一行console.log('Hello World'),`console.log()函数通常执行会很快,因此greeting()会很快执行完然后返回结果。

可以发现,我们必须等函数(比如processImage,networkRequest函数)执行结束才能调用下一个函数。这意味着这些函数调用的时候会阻塞主线程,造成主线程不能执行其他代码,这是我们所不希望的。

所以怎么解决这个问题呢?

最简单的解决办法就是使用异步的回调函数,有了异步的回调函数就不会阻塞主线程,看例子:

const networkRequest = () => {
  setTimeout(() => {
    console.log('Async Code');
  }, 2000);
};
console.log('Hello World');
networkRequest();

这里我们使用了setTimeout方法去模拟网络请求函数。

请注意:setTimeout不是Javascript引擎提供的,而是web api(浏览器中)和C/C++ API(nodejs中)的一部分。


<div align="center">Javascript运行环境概述</div>

事件循环Web API消息队列/任务队列并不是Javascript引擎的一部分而是浏览器的Javascript运行环境或是Nodejs的Javascript运行环境的一部分,在Nodejs中,Web API被C/C++ API替代。

回到上面的代码,看看异步的代码是如何执行的:

const networkRequest = () => {
  setTimeout(() => {
    console.log('Async Code');
  }, 2000);
};
console.log('Hello World');
networkRequest();
console.log('The End');


<div align="center">事件循环</div>

代码开始执行,console.log(‘Hello World’)函数的执行上下文首先被压入执行栈,执行结束后被弹出,然后调用networkRequest(),对应的函数执行上下文被压入执行栈。

紧接着 setTimeout() 函数被调用,对应的函数执行上下文被压入执行栈。

setTimeout有两个参数:1. 回调函数;2. 时间(以毫秒ms为单位);3. 附加参数(会被传到回调函数里面)

setTimeout() 函数会在web API运行环境中进行一个2s的倒计时,这个时候 setTimeout() 函数就已经执行完了,执行上下文从执行栈中弹出。再然后console.log('The End')函数被执行,进入执行栈,结束后弹出执行栈。

这时候倒计时到期,setTimeout()的回调函数被推到消息队列中,但回调函数不会立即执行,这是事件循环开始的地方。

事件循环

事件循环的工作就是去查看执行栈,确定执行栈是否为空,如果执行栈为空,那么就去检查消息队列,看看消息队列中是否有待执行的回调函数。它按照类似如下的方式来被实现:

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

在这里,执行栈已经为空,消息队列包含一个setTimeout函数的回调函数,因此事件循环把回调函数的执行上下文压入执行栈的顶端。

然后console.log(‘Async Code’)函数的执行上下文被压入执行栈,结束后从执行栈弹出。这时候回调函数执行结束,对应的执行上下文也从执行栈中弹出。

DOM事件

消息队列(也叫任务队列)中也会包含来自DOM事件(比如点击事件,键盘事件等),看例子:

document.querySelector('.btn').addEventListener('click',(event) => {
  console.log('Button Clicked');
});

对于DOM事件来说,web API中会有一个事件侦听器坚挺某个事件被触发(在这里是click事件),当某个事件被触发时,就会把相应的回调函数放入消息队列中执行。

事件循环再次检查执行栈,如果执行栈为空,就把事件的回调函数推入执行栈。

我们已经了解了异步回调和事件回调是如何执行的,这些回调函数被存储在消息队列中等待被执行。

ES6任务队列和微任务队列

ES6中为promise函数引入了微任务队列(也叫作业队列)的概念。微任务队列消息队列的区别就是优先级上的区别,微任务队列的优先级要高于消息队列。也就是说在微任务队列的promise回调函数会比在消息队列中的回调函数更先执行。

比如:

console.log('Script start');
setTimeout(() => {
  console.log('setTimeout');
}, 0);
new Promise((resolve, reject) => {
  resolve('Promise resolved');
}).then(res => console.log(res))
  .catch(err => console.log(err));
console.log('Script End');

输出:

Script start
Script End
Promise resolved
setTimeout

可以看到promise是在setTimeout之前执行的,因为promise的response被存储在微任务队列中,有比消息队列更高的优先级。

再看另一个例子,有两个promise函数,两个setTimeout函数:

console.log('Script start');
setTimeout(() => {
  console.log('setTimeout 1');
}, 0);
setTimeout(() => {
  console.log('setTimeout 2');
}, 0);
new Promise((resolve, reject) => {
  resolve('Promise 1 resolved');
}).then(res => console.log(res))
  .catch(err => console.log(err));
new Promise((resolve, reject) => {
  resolve('Promise 2 resolved');
}).then(res => console.log(res))
  .catch(err => console.log(err));
console.log('Script End');

输出:

Script start
Script End
Promise 1 resolved
Promise 2 resolved
setTimeout 1
setTimeout 2

可以看到两个promise的回调函数都在setTimeout的回调函数之前运行,因为相比消息队列事件循环会优先处理微任务队列中的回调函数。

当事件循环处理微任务队列中的回调函数的时候另一个promise被resolved了,然后这个promise的回调函数会被添加到微任务队列中。并且它会被优先执行,无论消息队列中的回调函数的执行会花费多长时间,都要排队。

比如:

console.log('Script start');
setTimeout(() => {
  console.log('setTimeout');
}, 0);
new Promise((resolve, reject) => {
  resolve('Promise 1 resolved');
}).then(res => console.log(res));

new Promise((resolve, reject) => {
  resolve('Promise 2 resolved');
}).then(res => {
  console.log(res);
  return new Promise((resolve, reject) => {
    resolve('Promise 3 resolved');
  })
}).then(res => console.log(res));
console.log('Script End');

打印:

Script start
Script End
Promise 1 resolved
Promise 2 resolved
Promise 3 resolved
setTimeout

因此所有在微任务队列的回调函数都会在消息队列的回调函数之前被执行。也就是说,事件循环会先清空微任务队列的回调函数才会去执行消息队列中的回调函数。


结论

我们了解了Javascript中同步和异步代码是怎么执行,以及一些其它的概念(包括执行栈,事件循环,微任务队列,消息队列等)。

原文地址:Understanding Asynchronous JavaScript

公众号:「前端进阶学习」,回复「666」,获取一揽子前端技术书籍

链接: https://www.fly63.com/article/detial/7560

通过alert方法,去理解js中阻塞、局部作用域、同步/异步任务

javascript中alert是Bom中的成员函数,alert对话框是模态的,具有阻塞性质的,不点击是不会执行后续代码的。js的阻塞是指在调用结果返回之前,当前线程会被挂起, 只有在得到结果之后才会继续执行。

如何优化async代码?更好的编写async异步函数

如何优化async代码?更好的编写async函数:使用return Promise.reject()在async函数中抛出异常,让相互之间没有依赖关系的异步函数同时执行,不要在循环的回调中/for、while循环中使用await,用map来代替它

【JS】异步处理机制的几种方式

Javascript语言的执行环境是单线程,异步模式非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。

js异步加载方式有哪些?_详解异步加载js的多种方案

js异步加载又被称为非阻塞加载,浏览器在下载JS的同时,还会进行后续页面处理。那么如何实现js异步加载呢?下面整理了多种实现方案供大家参考。异步加载js方案:Script Dom Element、onload时的异步加载、$(document).ready()、async属性、defer属性、es6模块type=module属性

Nodejs 处理异步(获取异步数据并处理)的方法

回调函数方式:将异步方法如readFile封装到一个自定义函数中,通过将异步方法得到的结果传给自定义方法的回调函数参数。事件驱动方式:使用node events模块,利用其EventEmitter对象

JS常用的几种异步流程控制

JavaScript引擎是基于单线程 (Single-threaded) 事件循环的概念构建的,同一时刻只允许一个代码块在执行,所以需要跟踪即将运行的代码,那些代码被放在一个任务队列 (job queue) 中

前端异步编程之Promise和async的用法

传统的异步解决方案采用回调函数和事件监听的方式,而这里主要记录两种异步编程的新方案:ES6的新语法Promise;ES2017引入的async函数;Generator函数(略)

异步的JavaScript

JS本身是一门单线程的语言,所以在执行一些需要等待的任务(eg.等待服务器响应,等待用户输入等)时就会阻塞其他代码。如果在浏览器中JS线程阻塞了,浏览器可能会失去响应,从而造成不好的用户体验。

js 多个异步的并发控制

请实现如下的函数,可以批量请求数据,所有的URL地址在urls参数中,同时可以通过max参数 控制请求的并发度。当所有的请求结束后,需要执行callback回调。发请求的函数可以直接使用fetch。

解读react的setSate的异步问题

将setState()认为是一次请求而不是一次立即执行更新组件的命令。为了更为可观的性能,React可能会推迟它,稍后会一次性更新这些组件。React不会保证在setState之后,能够立刻拿到改变的结果。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!