理解Javascript的异步

前言

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

总括: 本文梳理了异步代码和同步代码执行的区别,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();

上述代码的执行栈

所以这里发生了什么呢?

当代码被执行时,首先一个全局执行上下文(这里用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运行环境概述

事件循环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」,获取一揽子前端技术书籍

原文地址:https://www.cnblogs.com/jztan/p/12344927.html

时间: 2024-11-10 16:04:02

理解Javascript的异步的相关文章

理解Javascript的异步等待

目前async / await特性并没有被添加到ES2016标准中,但不代表这些特性将来不会被加入到Javascript中.在我写这篇文章时,它已经到达第三版草案,并且正迅速的发展中.这些特性已经被IE Edge支持了,而且它将会到达第四版,届时该特性将会登陆其他浏览器 -- 为加入该语言的下一版本而铺路(也可以看看:TC39进程). 我们听说特性已经有一段时间了,现在让我们深入它,并了解它是如何工作的.为了能够了解这篇文章的内容,你需要对promise和生成器对象有深厚的理解.这些资源或许可以

JavaScript异步编程(一) 深入理解JavaScript事件

JavaScript异步编程 深入理解JavaScript事件 ?事件的调度 JavaScript事件处理器在线程空闲之前不会运行 线程的阻塞 var start = new Date(); // setTimeout和setInterval的计时精度比期望值差 setTimeout(function(){ var end = new Date(); console.log('Time elapsed', end - start, 'ms'); }, 500); while(new Date -

全面理解Javascript中Promise

全面理解Javascript中Promise 最近在学习Promise的时候,在网上收集了一些资料,发现很多的知识点不够系统,所以小编特意为大家整理了一些自认为 比较好的文章,供大家更好地学习js中非常有趣的Promise Promise概念 2015 年 6 月,ECMAScript 6 的正式版 终于发布了. ECMAScript 是 JavaScript 语言的国际标准,javascript 是 ECMAScript 的实现.ES6 的目标,是使得 JavaScript 语言可以用来编写大

重新理解javascript回调函数

把函数作为参数传入到另一个函数中.这个函数就是所谓的回调函数 经常遇到这样一种情况,某个项目的A层和B层是由不同的人员协同完成.A层负责功能funA,B层负责funcB.当B层要用到某个模块的数据,于是他对A层人员说,我需要你们提供满足某种需求的数据,你给我提供一个接口. A层的人员说:我给你提供数据,怎么展示和处理则是B的事情. 当然B层不可能为你每个需求都提供一个数据接口,B给A提供一个通过的接口.B得到数据,然后B写函数去展示. 即,你需要和其他人合作,别人提供数据,而你不需要关注别人获取

深入理解JavaScript系列(44):设计模式之桥接模式

介绍 桥接模式(Bridge)将抽象部分与它的实现部分分离,使它们都可以独立地变化. 正文 桥接模式最常用在事件监控上,先看一段代码: addEvent(element, 'click', getBeerById); function getBeerById(e) { var id = this.id; asyncRequest('GET', 'beer.uri?id=' + id, function(resp) { // Callback response. console.log('Requ

理解 Javascript 的单线程,着实不易

理解 Javascript 的单线程,着实不易. 比如,对于 C 系编程来说,写个 while(true) 不是什么令人紧张的事情.在 nodejs 中似乎也不会出什么意外,因为还有消息机制可以操控--这只是你对 nodejs 的幻觉,或者说,这只是我对 nodejs 的幻觉.若子进程写成这样的话: process.on('message', function() { while(true); }); process.on('SIGINT', function() { console.log('

理解javascript中的回调函数(callback)【转】

在JavaScrip中,function是内置的类对象,也就是说它是一种类型的对象,可以和其它String.Array.Number.Object类的对象一样用于内置对象的管理.因为function实际上是一种对象,它可以"存储在变量中,通过参数传递给(别一个)函数(function),在函数内部创建,从函数中返回结果值". 因为function是内置对象,我们可以将它作为参数传递给另一个函数,延迟到函数中执行,甚至执行后将它返回.这是在JavaScript中使用回调函数的精髓.本篇文

(转)深入理解JavaScript 模块模式

深入理解JavaScript 模块模式 (原文)http://www.cnblogs.com/starweb/archive/2013/02/17/2914023.html 英文:http://www.adequatelygood.com/2010/3/JavaScript-Module-Pattern-In-Depth 模块模式是JavaScript一种常用的编码模式.这是一般的理解,但也有一些高级应用没有得到很多关注.在本文中,我将回顾基础知识,浏览一些不错的高级技巧,甚至我认为是原生基础的

理解javascript 回调函数

原文:理解javascript 回调函数 ##回调函数定义 百度百科:回调函数 回调函数就是一个通过函数指针调用的函数.如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数.回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应. 在JavaScript中,回调函数具体的定义为:函数A作为参数(函数引用)传递到另一个函数B中,并且这个函数B执行函数A.我们就说函数A叫做回调函数.如