async和await、浏览器的事件循环和Node事件循环

1.异步函数 async function

  • async关键字用于声明一个异步函数:
    • async是asynchronous单词的缩写,异步、非同步
    • sync是synchornous单词的缩写,同步、同时;

1.1 异步函数的基本使用

// await/async
async function foo(){

}
const foo2= async ()=>{

}

class Foo{
  // * 类中有一个函数也是异步函数,在前面加 async即可
  async bar(){

  }
}

1.2 异步函数的执行流程

如果异步函数内部没有什么特殊的东西,和普通函数的执行是一样的(按顺序执行,从上往下)。默认情况下也是会被同步执行。

async function foo(){
  console.log("foo function start");

  console.log("内部代码执行1");
  console.log("内部代码执行2");
  console.log("内部代码执行3");
  console.log("内部代码执行4");
}

// * 异步函数内部如果没有什么特殊的东西,和普通函数的执行是一样的。
console.log("script start");
foo()
console.log("script end");

1.3 异步函数和普通函数的区别

1.3.1 返回值是一个Promise对象(无异常)
  • 情况一:异步函数也可以有返回值,但是异步函数的返回值会被包裹到Promise.resolve中
  • 情况二:如果我们的异步函数返回的是Promise对象,Promise.resolve状态会由Promise对象决定
  • 情况三:如果我们的异步函数返回的是一个对象并且实现了then方法,Promise.resolve状态会由对象的then方法决定
async function foo(){
  console.log("foo function start");

  console.log("中间代码");

  console.log("foo function end");
  // * 1. 返回一个普通的值(基本类型、对象)
  // return 123
  // * 2. 返回一个对象,对象实现了then方法,会先执行then方法,根据then方法决定返回的Promise的状态
  // return {
  //   then(resolve,reject){
  //     resolve(123)
  //   }
  // }

  // * 3 返回的是一个Promise对象,由新的Promise决定返回的Promise的状态
   
  return new Promise((resolve,reject)=>{
      resolve("哈哈哈哈")
  })
}

const promise=foo();

// * 异步函数返回的值会作为 then的回调函数的第一个参数的值
promise.then(res=>{ //* promise.then什么时候会被执行,当foo函数返回一个值时
  console.log("promise then function start :",res);
})

// * 异步函数的返回值一定是一个Promise
1.3.2 异步函数的异常

异步函数的异常会被作为返回的Promise的reject的值,在后面执行的过程中可以使用catch方法进行捕获。

async function foo(){
  console.log("foo function start~");

  console.log("中间代码~");
  // * 异步函数的异常,会被作为异步函数返回的Promise的reject的值
  throw new Error("err message")
  console.log("foo function end~");
}
// * Promise发生异常或错误,会执行reject方法
foo().catch(err=>{
  console.log("coderwhy;",err);
})

console.log("后续还有代码~");
1.3.3 不可以使用await关键字

2.await关键字

  • async函数另外一个特殊之处就是可以在它内部使用await关键字,而普通函数中是不可以的。

  • await关键字有什么特点呢?

    • 通常使用await后面接一个表达式,这个表达式会返回一个Promise
    • 那么await会等到Promise状态变为fulfilled状态,之后继续执行异步函数。
  • 如果await后面是一个普通的值,那么直接返回这个值

  • 如果await后面是一个对象(实现了then方法),那么会根据then方法调用来决定后续的值。

  • 如果await后面的表达式,返回的Promise的rejected状态,那么会将这个reject结果作为函数的Promise的reject的值。

2.1 await后面的表达式

2.1.1 表达式(这个表达式返回Promise)
function requestData(){
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      resolve(222)
    },2*1000)
  })
}
async function foo(){
  // * 一般情况下 await后面是接一个表达式,表达式会返回一个Promise:await 表达式(Promise)
 const res=await requestData();
 //* 它会等待Promise对象执行了resolve时,等待给的对应的结果
//  * 它的值是resolve的值
// * 如果一直还没等到结果,后面代码会一直不执行
console.log("-----后续代码---------");
  
const res2=await requestData();

console.log("res2后面的代码:",res);
}

foo()
2.1.2 普通的值

普通的值会立即返回。

async function foo(){
  let result=await {name:"wjy",age:28}
  console.log("result:",result);
}
foo()
2.1.3 接一个对象(实现了then方法)

会立即调用then方法,由then方法调用来决定后续的值。

  • 如果then方法里面调用了resolve方法,则返回的是resolve的值。
  • 如果then方法里面调用了reject方法,则返回的是函数的Promise对象的reject的值
async function foo(){
  let result =await {
    name:"wjy",
    then:function(resolve,reject){
     reject(this.name)
    }
  }
}

foo().catch(err=>{
  console.log("err:",err);
})
2.1.4 如果await 后的表达式返回的是Promise的rejected状态

那么这个reject结果会作为函数的Promise的reject的值。

/**
 * *  如果await后的表达式返回的是  Promise对象的rejected状态,会将reject的值作为函数的Promise对象的reject的值
 */
async function foo(){
  let result=await new Promise((resolve,reject)=>{
    reject("error message");
  });
  console.log("result:",result);
}
foo().catch(err=>{
  console.log("err123:",err);
})

3. 进程和线程

线程和进程是操作系统中的两个概念:

  • 进程(process):计算机已经运行的程序,是操作系统管理程序的一种方式。
  • 线程(thread):操作系统能够运行运算调度的最小单位,通常情况下它被包含在进程中。

听起来很抽象,这是还是给出我的解释:

  • 进程:我们可以认为,启动一个应用程序,就会默认启动一个进程(也可能是多个进程)
  • 线程:每一个进程中,都会启动至少一个线程用来执行程序中的代码,这个线程被称之为 主线程
  • 所以我们也可以说 进程是线程的容器。

再用一个形象的例子来解释:

  • 操作系统类似于一个大工厂
  • 工厂中里有很多车间,这个车间就是进程。
  • 每个车间可能有一个以上的工人在工厂,这个工人就是线程。

4. 操作系统-进程-线程

image-20220518235159892.png

5. 操作系统的工作方式

操作系统是如何做到同时让多个进程(边听歌、边写代码、边查阅资料)同时工作呢?

  • 这是因为CPU的运算速度非常快,它可以快速的在多个进程之间迅速的切换
  • 当我们进程中的线程获取到时间片时,就可以快速执行我们编写的代码
  • 对于用户来说是感受不到这种快速的切换的。

6.浏览器中的JavaScript线程

  • 我们经常会说 JavaScript是单线程的,但是JavaScript的线程应该有自己的容器进程浏览器或者Node

  • 浏览器是一个进程吗?它里面只有一个线程吗?

    • 目前多数的浏览器其实都是多进程的,当我们打开一个tab页面时就会开启一个新的进程,这是为了防止一个页面卡死而造成所有页面无法响应,整个浏览器需要强制退出。
    • 每个进程中又有很多的线程,其中包括执行JavaScript代码的线程
  • javascript的代码执行是在一个单独的线程中执行的

    • 这就意味着JavaScript的代码,在同一个时刻只能做一件事
    • 如果这件事是非常耗时的,就意味着当前的线程会被阻塞
  • 所以真正耗时的操作,实际上并不是由javaScript线程在执行的

    • 浏览器的每个进程都是多线程的,那么其他线程可以来完成这个耗时的操作
    • 比如网络请求定时器,我们只需要在特定的时候执行应该有的回调即可。

7.浏览器的事件循环

如果在执行JavaScript代码的过程中,有异步操作呢?

  • 中间我们插入了一个setTimeout的函数调用
  • 这个函数会被放入到调用栈中,执行会立即结束,并不会阻塞后续代码的执行

事件循环大概是这样的:

  • js线程执行不耗时的任务,将耗时的任务交给浏览器的其他线程执行,耗时任务执行完后,会将对应的回调函数放入到事件队列中。
  • js引擎会查看事件队列有没有回调函数,如果有,会依次取出执行。
console.log("script start");

// 业务代码 

// * setTimeout是一个全局函数,我们可以直接调用它
// * setTimeout其实是一个同步函数,传入的回调函数是一个异步函数

// * 开始计时操作(相关的代码),由浏览器的其他线程来完成,(如果由js线程来完成的话,后续的代码会被阻塞。)

// * 浏览器本身维护着一个队列(队列又分为宏任务和微任务 queue)(队列是一个数据结构,线性结构,先进先出)
/**
 * * 事件队列(默认里面是没有东西的,定时器达到一秒钟之后会将回调函数放入到事件队列中,当js引擎发现事件队列中有东西的时候,
 * * 会将这个回调函数取出来,开始执行这个回调函数)
 * * js引擎会不断从事件队列里面不断取我们的回调函数,依次执行的。
 * * js线程、其他线程、事件队列形成了一个环
 */
setTimeout(()=>{

},1000)

console.log("后续代码~");


console.log("script end");

8.宏任务和微任务

但是事件循环中并非只维护着一个队列,事实上是有两个队列:

  • 宏任务队列(macrotask queue):ajax、setTimeout、setInterval、DOM监听、UI Rendering等
  • 微任务队列(microtask queue):Promise.then回调、Mutation Observer API、queueMicrotask()等

那么事件循环对于两个队列的优先级是怎么样的呢?

  • main script中的代码优先执行(编写的顶层script代码)
  • 执行任何一个宏任务之前(不是队列,是一个宏任务),都会先查看微任务队列中是否有任务需要执行
    • 也就是宏任务执行之前,必须保证微任务队列是空的。
    • 如果不为空,那么就优先执行微任务队列中的任务(回调)。

9.面试题

9.1 Promise面试题

setTimeout(function(){
  console.log("setTimeout1");
  new Promise(function (resolve){
    resolve();
  }).then(function(){
    new Promise(function(resolve){
      resolve();
    }).then(function(){
      console.log("then4");
    })
    console.log("then2");
  })
})


new Promise(function(resolve){
  console.log("promise1");
  resolve()
}).then(function(){
  console.log("then1");
})

setTimeout(function(){
  console.log("setTimeout2");
})

console.log(2);

queueMicrotask(()=>{
  console.log("queueMicrotask1");
})

new Promise(function(resolve){
  resolve();
}).then(function(){
  console.log("then3");
})

promise1

2

then1

queueMicrotask1

then3

setTimeout1

then2

then4

setTimeout2

9.2 promise async await 面试题

async function async1(){
  console.log("asycn1 start");
  await async2();
  console.log("async1 end");
}

async function async2(){
  console.log("async2");
}

console.log("script start");

setTimeout(function(){
  console.log("setTimeout");
},0)

async1();

new Promise(function(resolve){
  console.log("promise1");
  resolve()
}).then(function(){
  console.log("promise2");
})

console.log("script end");
  • await后面的代码会被包裹在then方法中的
  • 异步函数会返回一个promise对象

script start

asycn1 start

async2

promise1

script end

async1 end

promise2

setTimeout

9.3 Promise较难面试题

如果返回的是一个普通的值,不会推迟。

如果返回的是一个thenable对象,会被推迟一个微任务。

如果返回的是一个Promise.resolve,然后再then会推迟两个微任务。

Promise.resolve().then(()=>{
  console.log(0);
  // * 直接return一个值,相当于resolve(4)
  // return 4;

  // * return thenable对象 then方法不会直接执行,会放在下一个微任务的后面,
  // return {
  //   then:function(resolve,reject){
  //     resolve(4)
  //   }
  // }
  // * return 一个Promise对象,不是普通的对象会往后退一步,.then再退一步,总共退两步
  return Promise.resolve(4)
}).then((res)=>{ 
  console.log(res);
})

Promise.resolve().then(()=>{
  console.log(1);
}).then(()=>{
  console.log(2);
}).then(()=>{
  console.log(3);
}).then(()=>{
  console.log(5);
}).then(()=>{
  console.log(6);
})

10.Node事件循环

浏览器中的EventLoop是根据HTML5的规范来实现的,不同的浏览器可能会有不同的实现,而Node中是由libuv实现的。

这里我们来给出一个Node的架构图:

  • 我们会发现libuv中主要维护一个EventLoop和worker threads(线程池)
  • EventLoop负责调用系统的一些其他操作:文件的IO、Network、child-processes等

libuv是一个多平台的专注于异步IO的库,它最初是为Node开发的,但是现在也被使用到Luvit、Julia、pyuv等其他地方。

image-20220519160150824.png

11.Node事件循环的阶段

我们最前面就强调过,事件循环像是一个桥梁,是连接着应用程序的JavaScript和系统调用之间的通道。

  • 无论是我们的文件IO、数据库、网络IO、定时器、子进程,在完成对应的操作后,都会将对应的结果和回调函数放到事件循环(任务队列)中。

  • 事件循环会不断的从任务队列中取出对应的事件(回调函数)来执行。

  • 但是一次完整的事件循环Tick分成很多个阶段:

  • 定时器(Timers):本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。

  • 待定回调(Pending Callback):对某些系统操作(如TCP错误类型)执行回调,比如TCP连接时接收到ECONNREFUSED。

  • idle, prepare:仅系统内部使用。

  • 轮询(Poll):检索新的 I/O 事件;执行与 I/O 相关的回调;

  • 检测(check):setImmediate() 回调函数在这里执行。

  • 关闭的回调函数:一些关闭的回调函数,如:socket.on('close', ...)。

12.Node的宏任务和微任务

我们会发现一次事件循环的Tick来说,Node事件循环更复杂,它也分为微任务和宏任务。

  • 宏任务(macrotask):setTimeout、setInterval、IO事件、setImmediate、close事件
  • 微任务(microtask);Promise.then回调、process.nextTick、queueMicrotask

但是,Node的事件循环中不只是微任务队列和宏任务队列:

  • 微任务队列
    • next tick queue:process.nextTick
    • other queue:Promise.then()、queueMicrotask
  • 宏任务队列
    • timer queue:setTimeout、setInterval
    • poll queue:IO事件
    • check queue:setImmediate
    • close queue:close事件

13.Node事件循环的顺序

所以,在每一次事件循环的tick中,会按照如下顺序来执行代码:

  • next tick microtask queue
  • other microtask queue
  • timer queue
  • poll queue
  • check queue
  • close queue

14. Node面试题

async function async1(){
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}

async function async2(){
  console.log("async2");
}

console.log("scirpt start");

setTimeout(function(){
  console.log("setTimeout0");
},0);

setTimeout(function(){
  console.log("setTimeout2");
},300 )

setImmediate(()=>console.log("setImmediate"));

process.nextTick(()=>console.log("nextTick1"))

async1();

process.nextTick(()=>console.log("nextTick2"))

new Promise(function(resolve){
  console.log("promise1");
  resolve();
  console.log("promise2");
}).then(function(){
  console.log("promise3");
})

console.log("script end");

scirpt start

async1 start

async2

promise1

promise2

script end

nextTick1

nextTick2

async1 end

promise3

setTimeout0

setImmediate

setTimeout2

15.总结

async和await、浏览器事件循环和Node的事件循环.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355

推荐阅读更多精彩内容