理解Javascript的异步

前言

本文2925字,阅读大约须要10分钟。javascript

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

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

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

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

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

这里就是Javascript异步的用武之地了,咱们能够经过异步操做(好比回调函数,promise和async/await)来执行长时间的网络请求而不阻塞主线程。promise

虽说了解这些全部的概念不必定让你马上成为一名出色的Javascript开发者,但了解异步会对你颇有帮助。浏览器

话很少说,正文开始:)bash

同步的代码是怎么执行的

在深刻研究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();
复制代码

上述代码的执行栈

上述代码的执行栈

因此这里发生了什么呢?

当代码被执行时,首先一个全局执行上下文(这里用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中)的一部分。

Javascript运行环境概述

Javascript运行环境概述

事件循环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');
复制代码

事件循环

事件循环

代码开始执行,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中同步和异步代码是怎么执行,以及一些其它的概念(包括执行栈,事件循环,微任务队列,消息队列等)。

以上。


能力有限,水平通常,欢迎勘误,不胜感激。

订阅更多文章可关注公众号「前端进阶学习」,回复「666」,获取一揽子前端技术书籍

前端进阶学习
相关文章
相关标签/搜索