JS异步进阶
- 之前讲解JS异步,在于初阶的应用
- 本章在于JS异步的原理和进阶
提问
- 请描述event loop(事件循环/事件轮询)的机制,可画图
- 什么是宏任务和微任务,两者有什么区别
- Promise有哪三种状态?如何变化
- promise then和catch的连接
// 第一题
Promise.resolve().then(() => {
console.log(1)
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3)
})
// 1 3
// 第二题
Promise.resolve().then(() => {
console.log(1)
throw new Error('erro1')
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3)
})
// 1 2 3
// 第三题
Promise.resolve().then(() => {
console.log(1)
throw new Error('erro1')
}).catch(() => {
console.log(2)
}).catch(() => { // 注意这里是 catch
console.log(3)
})
// 1 2
- async/await 语法问题
async function fn() {
return 100
}
(async function () {
const a = fn() // ?? // promise对象
const b = await fn() // ?? // 100
})()
(async function () {
console.log('start')
const a = await 100
console.log('a', a)
const b = await Promise.resolve(200)
console.log('b', b)
const c = await Promise.reject(300)
console.log('c', c)
console.log('end')
})() // 执行完毕,打印出那些内容?//start a,100 b,200
- Promise 和 setTimeout 顺序
console.log(100)
setTimeout(() => {
console.log(200)
})
Promise.resolve().then(() => {
console.log(300)
})
console.log(400)
// 100 400 300 200
- async/await的执行顺序问题
async function async1 () {
console.log('async1 start') //2
await async2() // 这一句会同步执行,返回 Promise ,其中的 `console.log('async2')` 也会同步执行
console.log('async1 end') //6 await 后面的都作为回调内容---微任务
}
async function async2 () {
console.log('async2') //3
}
console.log('script start') //1
setTimeout(function () { // 异步,宏任务
console.log('setTimeout') //8
}, 0)
async1()
// 初始化 promise 时,传入的函数会立刻被执行
new Promise (function (resolve) {
console.log('promise1') // 4
resolve()
}).then (function () { // 异步,微任务
console.log('promise2') //7
})
console.log('script end') //5
// 同步代码执行完之后,屡一下现有的异步未执行的,按照顺序
// 1. async1 函数中 await 后面的内容 —— 微任务
// 2. setTimeout —— 宏任务
// 3. then —— 微任务
// 同步代码执行完毕 (event loop - call stack 被清空)
// 执行微任务
// (尝试触发DOM渲染)
// 触发Event Loop,执行宏任务
event loop
- JS是单线程运行的
- 异步要基于回调来实现
- event loop就是异步回调的实现原理
JS如何执行
- 从前到后,一行一行执行
- 如果某一行执行报错,则停止下面代码的执行
- 先把同步代码执行完,再执行异步
示例
console.log('Hi')
setTimeout(function cb1() {
console.log('cb1') // cb 即 callback
}, 5000)
console.log('Bye')
总结event loop的过程
- 同步代码,一行一行放在Call Stack调用栈执行
- 遇到异步,会先“记录”下,等待时机(定时、网络请求等)
- 时机到了,就会移动到Callback Queue
- 如Call Stack为空(即同步代码执行完)Event Loop开始工作
- 轮询查找Callback Queue,如有则移动到Call Stack执行
- 然后继续轮询查找(永动机一样)
DOM事件和event loop
- JS是单线程的
- 异步(setTimeOut,ajax等)使用回调,基于event loop
- Dom事件不是异步,但DOM事件也使用回调,基于event loop
//DOM 事件,也用 event loop
<button id="btn1">提交</button>
<script>
console.log('Hi')
$('#btn1').click(function (e) {
console.log('button clicked')
})
console.log('Bye')
</script>
promise进阶
三种状态
- pending resolved rejected
- pending 》resolved 或 pending 》rejected
- 变化不可逆
// 刚定义时,状态默认为 pending
const p1 = new Promise((resolve, reject) => {
})
console.log('p1',p1) // pending
// 执行 resolve() 后,状态变成 resolved
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
})
})
console.log('p2',p2) // pending - 开始打印时
setTimeout(() => console.log('p2-setTimeout',p2)) //resolved
// 执行 reject() 后,状态变成 rejected
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject()
})
})
console.log('p3',p3) // pending - 开始打印时
setTimeout(() => console.log('p3-setTimeout',p3)) //rejected
状态的表现和变化
- pending状态,不会触发then和catch
- resolved状态,会触发后续的then回调函数
- rejected状态,会触发后续的catch回调函数
const p1 = Promise.resolve(100)
console.log('p1',p1)
const p2 = Promise.reject('err')
console.log('p2',p2)
const p1 = Promise.resolve(100) //resolved
//console.log('p1',p1)
p1.then(data => {
console.log('data',data)
}).catch(err => {
console.error('err',err)
})
const p2 = Promise.reject('err') //rejected
//console.log('p2',p2)
p2.then(data => {
console.log('data2',data)
}).catch(err => {
console.error('err2',err)
})
then和catch对状态的影响(then和catch改变状态)
then catch 会继续返回 Promise ,此时可能会发生状态变化!!!
- then正常返回resolved,里面有报错则返回rejected
- catch正常返回resolved,里面有报错则返回rejected
const p1 = Promise.resolve().then(() => {
return 100
})
console.log('p1',p1) //resolved
const p2 = Promise.resolve().then(() => {
throw new Error('then error')
})
console.log('p2',p2) //rejected
const p1 = Promise.resolve().then(() => {
return 100
})
console.log('p1',p1) //resolved 触发后续then回调
p1.then(() => {
console.log('123')
})
const p2 = Promise.resolve().then(() => {
throw new Error('then error')
})
console.log('p2',p2) //rejected 触发后续catch回调
p2.then(() => {
console.log('456')
}).catch(err => {
console.error('err100',err)
})
const p3 = Promise.reject('my error').catch((err) => {
console.error('err)
})
console.log('p3',p3) //resolved 注意!触发then函数
p3.then(() => {
console.log(100)
})
const p4 = Promise.reject(('my error').catch((err) => {
throw new Error('catch err')
})
console.log('p4',p4) //rejected 触发后续catch回调
p4.then(() => {
console.log(200)
}).catch(()=> {
console.error('some err')
})
// then() 一般正常返回 resolved 状态的 promise
Promise.resolve().then(() => {
return 100
})
// then() 里抛出错误,会返回 rejected 状态的 promise
Promise.resolve().then(() => {
throw new Error('err')
})
// catch() 不抛出错误,会返回 resolved 状态的 promise
Promise.reject().catch(() => {
console.error('catch some error')
})
// catch() 抛出错误,会返回 rejected 状态的 promise
Promise.reject().catch(() => {
console.error('catch some error')
throw new Error('err')
})
// 第一题
Promise.resolve().then(() => {
console.log(1) //1
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3) //3
})
// 第二题
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
console.log(1) //1
throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise
console.log(2) //2
}).then(() => {
console.log(3) //3
})
// 第三题
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
console.log(1) //1
throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise
console.log(2) //2
}).catch(() => {
console.log(3)
})
Promise总结
- 三种状态,状态的表现和变化
- then和catch对状态的影响
- then和catch的链式调用
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
}
const src1 = 'http://www.imooc.com/static/img/index/logo_new.png'
const src2 = 'https://avatars3.githubusercontent.com/u/9583120'
(async function () {
// 注意:await 必须放在 async 函数中,否则会报错
// 加载第一张图片
const img1 = await loadImg(src1)
console.log(img1)
// 加载第二张图片
const img2 = await loadImg(src2)
console.log(img2)
})()
会报
因为src2后面没加分号,和后面的()连在一起被当做函数,类似图示alert也会弹出弹框,解决办法:(async前面加!号
const src1 = 'http://www.imooc.com/static/img/index/logo_new.png'
const src2 = 'https://avatars3.githubusercontent.com/u/9583120'
!(async function () {
// 注意:await 必须放在 async 函数中,否则会报错
// 加载第一张图片
const img1 = await loadImg(src1)
console.log(img1)
// 加载第二张图片
const img2 = await loadImg(src2)
console.log(img2)
})()
//用同步的方式,编写异步。
async function loadImg1() {
const src1 = 'http://www.imooc.com/static/img/index/logo_new.png'
const img1 = await loadImg(src1)
return img1
}
async function loadImg2() {
const src2 = 'https://avatars3.githubusercontent.com/u/9583120'
const img2 = await loadImg(src2)
return img2
}
!(async function () {
// 注意:await 必须放在 async 函数中,否则会报错
try {
// 加载第一张图片
const img1 = await loadImg1() //await后面不仅可以加promise对象也可以加async函数
console.log(img1)
// 加载第二张图片
const img2 = await loadImg2()
console.log(img2)
} catch (ex) {
console.error(ex)
}
})()
async-await和Promise有什么关系
- async/await是消灭异步回调的终极武器
- 但和Promise并不互斥
- 反而,两者相辅相成
- 执行async函数,返回的是Promise对象,如果函数内没返回 Promise ,则自动封装成Promise对象
async function fn1() {
return new Promise(200)
}
const res1 = fn1()//执行async函数,返回的是一个Promise对象
res1.then(data => {console.log('data',data)}) //200
async function fn2() {
return 100 // 相当于 Promise.resolve(100)
}
console.log( fn2() ) //Promise对象
- await 相当于Promise的then,await 后面可以加promise对象、值、async函数的执行结果
// await 后面跟 Promise 对象:会阻断后续代码,等待状态变为 resolved ,才获取结果并继续执行
// await 后续跟非 Promise 对象:会直接返回
!(async function () {
const p1 = Promise.resolve(100)
const data = await p1 // await 相当于Promise的then
console.log('data',data) // 100
})()
!(async function () {
const data1 = await 400 // await Promise.resolve(400)
console.log('data1',data1)
})()
!(async function () {
const data2 = await fn1()
console.log('data2',data2)
})()
!(async function () {
const p1 = new Promise(() => {})
await p1
console.log('p1') // 不会执行
})()
!(async function () {
const p4 = Promise.reject('some err') //rejected状态
const res = await p4 // await -> then
console.log(res) // 不会执行
})()
- try…catch可捕获异常,代替了Promise的catch
!(async function () {
const p4 = Promise.reject('some err') //rejected状态
try {
const res = await p4
console.log(res)
} catch (ex) {
console.error(ex) // try...catch 相当于 promise catch
}
})()
//总结
async 封装 Promise
await 处理 Promise 成功
try...catch 处理 Promise 失败
async/await是语法糖,异步的本质还是回调函数
- async/await是消灭异步回调的终极武器
- JS是单线程,还得是有异步,还得是基于event loop
- async/await只是一个语法糖,但这颗糖真香
//只要遇到了 await ,后面的代码都相当于放在 callback 里
async function async1 () {
console.log('async1 start') // 2
await async2() //undefined
// await 的后面都可以看做是calllback里的内容,即异步
// 类似,event loop,setTimeout(cb1)
// setTimeout(function() { console.log('async1 end') })
// Promise.resolve().then(() => { console.log('async1 end') } ) //微任务/宏任务
console.log('async1 end') // 5 关键在这一步,它相当于放在 callback 中,最后执行
}
async function async2 () {
console.log('async2') // 3
}
console.log('script start') //1
async1() //立马执行async1函数体
console.log('script end') //4
// 同步代码已经执行完(event loop)
async function async1 () {
console.log('async1 start') // 2
await async2()
// 下面三行都是异步回调 callback的内容
console.log('async1 end') // 5
await async3()
console.log('async1 end 2')} //7
async function async2 () {
console.log('async2') // 3
}
async function async3 () {
console.log('async3') // 6
}
console.log('script start') //1
async1()
console.log('script end') //4
// 同步代码已经执行完(event loop)
for…of
- for…in(以及forEach for) 是常规的同步遍历
- for…of常用于异步的遍历
// 定时算乘法
function multi(num) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(num * num)
}, 1000)
})
}
// // 使用 forEach ,是 1s 之后打印出所有结果,即 3 个值是一起被计算出来的
// function test1 () {
// const nums = [1, 2, 3];
// nums.forEach(async x => {
// const res = await multi(x);
// console.log(res);
// })
// }
// test1();
// 使用 for...of ,可以让计算挨个串行执行
async function test2 () {
const nums = [1, 2, 3];
for (let x of nums) {
// 在 for...of 循环体的内部,遇到 await 会挨个串行计算
const res = await multi(x)
console.log(res)
}
}
test2()
微任务microTask和宏任务macroTask
- 什么是宏任务,什么是微任务
- event loop和DOM渲染
- 微任务和宏任务的区别
console.log(100)
setTimeout(() => {
console.log(200)
})
Promise.resolve().then(() => {
console.log(300)
})
console.log(400)
// 100 400 300 200
宏任务和微任务
- 宏任务:setTimeout,setInterval,Ajax,DOM事件
- 微任务:Promise async/await
- 微任务执行时机比宏任务要早(先记住)
event loop和DOM渲染
- 再次回归一遍event loop的过程
- JS是单线程的,而且和DOM渲染共用一个线程
- JS执行的时候,得留一些时机供DOM渲染
- 回顾event loop过程(增加DOM渲染时机)
1.每次Call Stack清空(即每次轮询结束),即同步任务执行完,或者说异步代码推到Call Stack执行结束
2.都是DOM重新渲染的机会,DOM结构如有改变则重新渲染,(不一定非得渲染,就是给一次 DOM 渲染的机会!!!)
3.然后再去触发下一次Event Loop
const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container')
.append($p1)
.append($p2)
.append($p3)
console.log('length', $('#container').children().length )
alert('本次 call stack 结束,DOM 结构已更新,但尚未触发渲染')
// (alert 会阻断 js 执行,也会阻断 DOM 渲染,便于查看效果)
// 到此,即本次 call stack 结束后(同步任务都执行完了),浏览器会自动触发渲染,不用代码干预
// 微任务:DOM渲染前触发
Promise.resolve().then(() => {
console.log('length1',$('#container').children().length) //3
alert('Promise then') //DOM渲染了吗?NO
})
// 宏任务:DOM渲染后触发
setTimeout(() => {
console.log('length2',$('#container').children().length) //3
alert('setTimeout') //DOM渲染了吗?yes
})
宏任务和微任务的区别
- 宏任务:DOM渲染后触发,如setTimeout
- 微任务:DOM渲染前触发,如Promise
从event loop解释,为何微任务执行更早
- 宏任务(浏览器规定的)
- 微任务(ES6语法规定的)
微任务:ES 语法标准之内,JS 引擎来统一处理。即,不用浏览器有任何关于,即可一次性处理完,更快更及时。
宏任务:ES 语法没有,JS 引擎不处理,浏览器(或 nodejs)干预处理。