js异步 从callback 到 async
js异步
因为js是单线程的语言,所以为了不使页面卡顿,暂停。js引入了异步机制。
问:那么js为什么是单线程的呢?
答:因为js是可以操作dom元素的,所以js就必须单线程的。否则页面渲染就会出问题。
es6以前的处理方式
众所周知,es6以前的是通过回掉函数callback来处理异步的逻辑的。这种处理方式正常情况下 没有什么问题,但是如果碰到多个函数层层嵌套 就会出现回掉地狱。形成一个类似于金字塔的样子,虽然执行上并没有问题,但是非常不利于维护和阅读。想想如果 产品经理忽然说需求变了 需要你在b里面在加一段逻辑,再调用一下e函数,然后按照a,b,e,c,d的顺序执行。(这个时候 如果不小心删掉了一个大括号,那么开发人员的心态就会崩溃,怀疑自我。想转行!)(以下代码瞎写的,表达个意思而已,懂吧)
function a(){
function b(){
function c(){
function D(){
console.log('a,b,c,d 都按顺序执行了')
}
}
}
}
es6的promise
所以es6的promise应运而生。promise的产生是为了让我们脱离回掉地狱。过上链式调用的好日子。这样代码阅读起来会舒服很多很多。如上面那段逻辑 我们就可以改写为
function a() {
return new Promise((resolve,reject)=>{
return 'a'
})
}
function b() {
return new Promise((resolve,reject)=>{
return 'b'
})
}
function c() {
return new Promise((resolve,reject)=>{
return 'c'
})
}
function d() {
return new Promise((resolve,reject)=>{
return 'd'
})
}
a().then(()=>{
return b();
}).then(()=>{
return c();
}).then(()=>{
return d()
}).then(()=>{
console.log('a,b,c,d 都按顺序执行了')
})
这样的话如果 你要在b 后面加一个e函数 你就可以直接在中间插一段就行了。代码会非常的整齐,逻辑看上去都清晰很多。
promise 有三种状态,pending, resolved,rejected. 刚定义出来的promise 就是pending状态。 Pending 可以转为resolved 也可以转为rejected 过程不可逆。resolved的状态被then接收,rejected状态可以被catch接收。(这里说的resolved也就是fulfilled的意思)
es2017的async
虽然 promise把异步变成了链式调用,但是async 能让我们用同步的语法去写异步的逻辑。这个就很强大,让你写代码的时候 不需要考虑什么同步异步,达到润物无声的高级感。
我们都知道async 是promise的语法糖,那么怎么理解语法糖这个意思呢,就是说是 他本质上还是promise 的逻辑,但是形式上不一样而已。那怎么证明async不是新的类型呢。可以定义一个async函数,然后直接打印这个函数 就会发现他执行后返回值还是一个promise对象。如下
async function a(){} a();
/*Promise {<fulfilled>: undefined}
__proto__: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: undefined*/
async 既然是promise的语法糖,那promise有的功能 它也都有。promises使用时最常用的then函数,在async里面对标的就是await函数。所以 await不能单独调用哦,必须放在async里面。使用方法如下
async function a(){return 'helloWorld'}
async function b() {
console.log("*****")
let hello=await a();
console.log(hello);
console.log("888")
}
b();
//*****
//helloWorld
//888
那promise 对应的catch函数,在这里需要用其它语言的写法,try catch来处理。
也就是说 要用try来包裹await函数,在catch里写上异常处理逻辑。
这里有个较为常见的面试题。
问:我们知道await函数执行过程中,如果中断了,是不会继续往下走的。那如果有的报错,我们其实是希望他提示一下,但是不要中断我的操作,那我们要怎么做呢?
答:将要处理的这段异步代码用try catch包裹起来,在catch里面加上错误提示,再将try里面包裹的 await后面的代码 再写一份到catch中,让其运行。
或者,我们可以用async和promise相结合的方式,用promise的catch里面写出错后的逻辑。
具体代码可以大概概括如下(表达个意思,不是复制出去就能执行的)
async function a(){return 'helloWorld'}
async function b() {
console.log("我是执行异步前的正常逻辑");//1
try {
let hello=await a();
console.log("我是执行异步后的其它代码")//2
}catch (e) {
console.error(e)//错误提示
console.log("我是执行异步后的其它代码")//2语句复制过来的
}
}
//或与promise结合
async function b() {
console.log("我是执行异步前的正常逻辑");//1
a().then(()=>{
console.log("我是执行异步后的其它代码")//2
}).catch((e)=>{
console.error(e)//错误提示
console.log("我是执行异步后的其它代码")//2语句复制过来的
})
}
异步的处理机制-事件循环
说到异步 那不能提到异步的处理机制-事件循环(event-loop)
具体流程 我用下面的简图表示出来了(可能图不太标准 ,主要看文字)
就是说 一段代码下来,
1.我们首先判断当前语句是否是同步的正常语句,是的话,我们就推入运行栈中 运行,运行完,将这个语句推出栈。如果碰到异步的语法,我们先把他们放到消息队列里等待着,继续往下走。
2.如果同步代码已经全部执行完毕,那么运行栈暂时清空,此时,事件循环就好像一个哨兵一样,发现了这个消息,他去消息队列里找我们刚刚暂存在里异步函数,然后继续重复第一步,判断这个异步函数中的语句 是否是同步的。。
3.事件循环这个哨兵就像永动机一样,不需要休息,一直存在,一直循环监听。就实现了我们的异步逻辑实现。
宏任务与微任务
宏任务指的是浏览器指定的一些异步函数,比如点击事件,定时器事件,ajax事件
微任务指的是js指定的异步函数,比如promise和async await。
问:为什么要区分宏任务和微任务
答:因为宏任务和微任务 在事件循环中执行的时机是不一样的。(可能会遇到一些面试题里会问这个)
问:宏任务和微任务哪个先执行
答:微任务先执行,宏任务后执行。微任务在dom渲染前执行,宏任务在dom渲染后后执行。(这个就不要问为什么了,人家浏览器就这么定了)