首先我们一开始先思考几道题,在思考题目的时候可以先想一下该题考查的是什么知识点,然后我们再来通过题目理解该知识点
js异步/事件循环/Promise
我们可以先看几个面试题,留着疑问去学习:
- 同步和异步的区别?
- 手写Promise加载一张图片?
- 前端使用异步的场景有哪些?
- 请描述event loop(事件循环/事件轮询)的机制,可画图
- 什么是宏任务?什么是微任务,两者有什么区别?
- Promise有哪三种状态?如何变化?
知识点:
- 单线程和异步
- 应用场景
- callback hell 和Promise
- event loop
- promise进阶
- async/await 的使用
- 微任务/宏任务的 认知和区别
单线程和异步
因为js的单线程才有的异步
-
JS是单线程语言,只能同时做一件事儿
比方说,我们弄一个ajax请求,一个定时器先等待一秒钟, 如果是同步的话,这里在加载图片或者ajax过程中就卡住了, 鼠标点击不了,拖不动,js也不执行,这就是单线程语言的本质。 复制代码
-
浏览器和nodejs已支持js启动进程,如Web Worker;但也不能改变js是单线程这种本质
-
因为JS可修改DOM结构,所以js的DOM渲染共用同一个线程。js执行过程中DOM必须停止,DOM渲染过程中js必须停止
所以,当我们遇到等待(网络请求,定时任务)的时候不能卡住, 所以就需要异步,解决js单线程的问题,回调callback函数形式 同步异步的例子:异步可以开一个定时器或者请求数据, 同步的话可以放一个alert(。。),会发现alert后面的代码被堵塞了。 复制代码
异步和同步:
- 基于JS是单线程语言
- 异步不会阻塞代码执行
- 同步会阻塞代码执行
应用场景
- 网络请求,如 ajax,图片加载
- 定时任务,如 setTimeout
网络请求:
console.log('start')
$.get('./detail.json',function(data1){
console.log(data1)
})
console.log('end')
复制代码
图片加载:
console.log('start')
let img = document.createElement('img')
img.onload = function(){
console.log('loaded')
}
img.src = 'https://juejin.cn/xxx.png'
console.log('end')
复制代码
定时器~
callback hell回调地狱
老式写法嵌套一层又一层:
很复杂,于是产生了peomise:
peomise解决了回调地狱:也就是解决callback嵌套的问题;
手写Promise加载一张图片
function loadImg(src){
return new Promise(
// 一开始的时候 是pending状态
(resolve,reject) => {
// resolve,reject这两个也是函数,一个成功的执行,一个失败的执行
const img = document.createElement('img')
img.onload = () => {
resolve(img)// 这里是resolved状态
}
img.onerror = () => {
reject(new Error(`图片加载失败${src}`))// 这里是reject状态
}
img.src = src
}
)
}
loadImg(url)// 返回的是一个promise对象,所以可以then
const url1 = 'https://img.mukewang.com/5a9fc8070001a82402060220-140-140.jpg'
const url2 = 'https://img3.mukewang.com/5a9fc8070001a82402060220-100-100.jpg'
loadImg(url1).then(img1 => {
console.log(img1.width)
return img1 // 返回一个普通对象
}).then(img1 => { // 这里的参数是一个普通对象
console.log(img1.height)
return loadImg(url2) // 这里返回一个Promise实例
}).then(img2 => {
console.log(img2.width)
return img2
}).then(img2 => {
console.log(img2.height)
}).catch(ex => console.error(ex))
复制代码
Promise
(一)Promise的状态
- promise有三种状态
- 状态的表现和变化
- then和catch对状态的影响
三种状态和变化:
pending resovled rejected
在过程中还没结果 已经解决了 已经失败了
pending-> resolved或pending->rejected
变化不可逆
复制代码
// 代码演示:
const p1 = new Promise((resolve,reject) =>{})
console.log('p1',p1) // p1 Promise{<pending>}
const p2 = new Promise((resolve,reject) =>{
setTimeout(()=>{
resolve()
})
})
console.log('p2',p2) // p2 Promise{<pending>} 依然是一个pending
// 但我们点开以后,里面的PromiseStatus是一个resolved
// 因为这里是一个异步,先打印,后执行完毕,所以一开始还是一个pending
setTimeout(()=>{console.log('p2-setTimeout',p2)})
// p2-setTimeout Promise{<resolved>:undefined} 我们在异步里打印的话能打印出它真实的状态
const p3 = new Promise((resolve,reject) =>{
setTimeout(()=>{
reject()
})
})
console.log('p3',p3) // p3 Promise{<pending>},点开后,里面的PromiseStatus是一个rejected
setTimeout(()=>{console.log('p3-setTimeout',p3)})
// p3-setTimeout Promise{<rejected>:undefined}
复制代码
- pengding状态,不会触发then和catch
- resolved状态,会触发后续的then回调函数
- rejected状态,会触发后续的catch回调函数
// 现在我们想直接拿到一个resolved
const p1 = Promise.resolve(100) // resolved
console.log('p1',p1)//p1 Promise{<resolved>:100}
p1.then(data => {
// resolve只会走.then回调
console.log('data',data) // data 100
}).catch(err => {
console.error('err',err) // resolve不会走这边
})
const p2 = Promise.reject('err') // rejected
// 因为这里不是.catch去接收信息,所以内部抛出错误,直接rejected状态
console.log('p2',p2)//p2 Promise{<rejected>:'err'} 然后报错!
p2.then(data => {
console.log('data2',data) // rejected不会走这边
}).catch(err => {
// rejected只会走.catch回调
console.error('err2',err) // err2 'err'
})
复制代码
(二)Promise的then和catch如何影响状态的变化
- then正常返回resolved,若里面有报错,则返回rejected
- catch正常也返回resolved,若里面有报错,则返回rejected
这两句话虽然很简单,但我们还是从下面的代码中去理解一下:
// then() 一般正常返回 resolved 状态的 promise
Promise.resolve().then(() => {
// resolved会触发then回调
return 100
})
// then() 里抛出错误,会返回 rejected 状态的 promise
Promise.resolve().then(() => {
throw new Error('err')
})
// catch() 不抛出错误,会返回 resolved 状态的 promise
Promise.reject().catch(() => {
// rejected会触发catch回调
console.error('catch some error')
})
// catch() 抛出错误,会返回 rejected 状态的 promise
Promise.reject().catch(() => {
console.error('catch some error')
throw new Error('err')
})
复制代码
async/await
- 一开始因为有异步回调callback hell(嵌套写法)
- 然后我们使用Promise then catch链式调用,但也是基于回调函数的(链式写法)
- async/await是同步语法,彻底消灭回调函数(同步写法)
// 我们继续看之前写的这个方法
function loadImg(src) {
const promise = new Promise((resolve, reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
reject(new Error(`图片加载失败 ${src}`))
}
img.src = src
})
return promise
}
// 注意,在控制台中输入
alert
(
)
也是会触发这个方法的,
所以我们在日常书写js中,写自调用的时候如果因为这个符号的问题报错,我们可以在前面加一个!号:
async function loadImg3(){
const src3 = 'http://www.imooc.com/static/img/index/logo_new.png'
const img3 = await loadImg(src3)
return img3
}
!(async function loadImg1() {
const src1 = 'http://www.imooc.com/static/img/index/logo_new.png'
const img1 = await loadImg(src1) // promise对象
const src2 = 'http://www.imooc.com/static/img/index/logo_new.png'
const img2 = await loadImg(src2)
const img3 = await loadImg3() // async函数
console.log(img1, img2,'同步写法')
})()
// await不仅后面可以追加 还可以追加promise对象
复制代码
async/await 和 Promise的关系
- async/await 是消灭异步回调的终极武器
- 和Promise并不互斥
- 两者相辅相成
// async函数 返回的是Promise对象
async function fn1() {
return 100
//如果返回的是一个值的话,它会封装成一个Promise对象去返回
// return Promise.resolve(100) 两者返回的都是Promise对象
// 如果返回的是一个Promise对象的话,它会原封不动的去返回
}
const res1 = fn1()
console.log(res1) // Promise{<resolved>:100} -> 执行async函数返回的是一个Promise对象
// await后跟promise,值,async函数 这三种情况都可以
!(async function(){
const p1 = Promise resolve(300)
const data = await p1 // await 相当于Promise then
const data1 = await 400 // await 会把400 封装成Promise对象去执行Promise.then
const data2 = await fn1() // async函数
console.log('data',data,data1,data2) // data 300 400 100
})()
// try...catch...相当于Promise的catch
!(async function (){
const p4 = Promise.reject('err1') // rejected
try {
const res = await p4
console.log(res)
// 这里肯定走不进来,仅为await相当于then,只执行resolve状态的
} catch (ex){
console.error(ex) // err1
}
})()
复制代码
总结,两者相辅相成的原因:
- 执行async函数,返回的是Promise对象
- await相当于Promise的then
- try…catch 可捕获异常,替代了Promise的catch
异步的本质
event loop
- 场景题-promise then 和 catch 的连接
2. 场景题- async/await 语法
3. 场景题- promise/setTimeout 的顺序
4. 场景题- 外加async/await 的顺序问题(思考输出什么)
event loop (事件循环/事件轮询)
- js是单线程运行的
- 异步要基于回调来实现
- event loop就是异步回调的实现原理
JS如何执行?
- 从前到后,一行一行执行
- 如果某一行执行报错,则停止下面代码的执行
- 先把同步代码执行完,再执行异步(通过回调实现异步)
讲event loop之前先把这段代码看熟了
console.log('hi')
setTimeout(function cd1(){
consol.log('cd1')
},5000)
console.log('bye')
复制代码
然后这张图是我们执行代码的流程图示
- 同步代码的执行。首先,我们第一行代码console.log(‘Hi’)会被推入调用栈(1)中,然后调用栈执行这行代码,会在控制台(2)输出处展示Hi,执行完后调用栈(1)会把第一行代码弹出调用栈
- 异步代码的处理。接着第二行代码setTimeout会被放入执行栈中,内部有一个函数cb1.但因为这里是一个异步,会把timer(cb1)放到WebAPIs(3)中,执行栈弹出setTimeout,5秒钟后会把timer(cb1)放到函数队列中。
- 剩余同步代码的执行。然后第三部分代码console.log(‘Bye’)被推入调用栈中,调用栈执行该行代码,在控制台输出,执行完毕后弹出调用栈。
- 继续处理异步代码。但这个时候第二部分代码还在WebAPI中;因为这个时候我们的调用栈被清空了,里面没有任何事件了,这个时候会启动我们的event loop机制,一旦我们的同步代码被执行完,调用栈被清空,我们的event loop机制会立马启动。event loop会一遍一遍的循环,会从函数队列中去寻找有没有函数需要执行;
- 异步代码的执行。等到事件触发的时候,事件会从WebAPI弹到函数队列;eventloop就转呀转发现函数队列中有事件,这个时候eventloop就立马把这个函数拿到执行栈中执行。执行栈会对 timer(cb1)进行分析,分为函数cb1和console.log两部分;先把内部的console执行了,在控制台输出打印内容,然后清掉console的执行,这个时候其实函数内只有一行打印代码,所以相当于该函数执行完了;所以接着把函数cb1弹出执行栈。整个代码执行完毕。
总结
- 同步代码,一行一行放在CallStack执行
- 遇到异步,会先“记录”下,弹到WebAPIs,等待时机(定时,网络请求等)
- 时机一到,就会移动到Callback Queue中
- 如果Call Stack 为空(即同步代码执行完),Event Loop开始工作
- Event Loop轮询查找Callback Queue,如果有事件,则移动到Call Stack执行
- Event Loop继续轮询查找(像永动机一样)
DOM事件和event loop
只要用了回调就基于event loop
- JS是单线程
- DOM事件也是基于event loop的,因为其实也使用回调(比如什么时候用户点击啦什么的)
- 异步(setTimeout,ajax等等)也是基于event loop的
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END