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. 操作系统-进程-线程
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等其他地方。
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