event loop(事件循环)
-
概念
- js 是单线程运行的
- 异步要基于回调来实现
- event loop 就是异步回调的实现原理
- DOM 事件也应用回调, 基于 event loop
-
过程
- 同步代码, 一行一行放在 Call Stack 执行
- 遇到异步会先记录下, 期待机会 (定时, 网络申请等)
- 机会到了就挪动到 Callback Queue
- 如 Call Stack 为空 (即同步代码执行完) Event Loop 开始工作
- 轮询查找 Callback Queue, 如有则挪动到 Call Stack 执行
- 而后持续轮询查找
Promise
- 三种状态
- 状态和 then catch
- 罕用 API
先回顾一下 Promise 的根本应用
// 加载图片
function loadImg(src) {
const p = new Promise((resolve, reject) => {const img = document.createElement('img')
img.onload = () => {resolve(img)
}
img.onerror = () => {const err = new Error(` 图片加载失败 ${src}`)
reject(err)
}
img.src = src
}
)
return p
}
const url = 'https://img.mukewang.com/5a9fc8070001a82402060220-140-140.jpg'
loadImg(url).then(img => {console.log(img.width)
return img
}).then(img => {console.log(img.height)
}).catch(ex => console.error(ex))
三种状态
三种状态 pending resolved rejected
(画图示意转换关系,以及转换不可逆)
// 刚定义时,状态默认为 pending
const p1 = new Promise((resolve, reject) => {})
// 执行 resolve() 后,状态变成 resolved
const p2 = new Promise((resolve, reject) => {setTimeout(() => {resolve()
})
})
// 执行 reject() 后,状态变成 rejected
const p3 = new Promise((resolve, reject) => {setTimeout(() => {reject()
})
})
状态和 then catch
状态变动会触发 then catch —— 这些比拟好了解,就不再代码演示了
- pending 不会触发任何 then catch 回调
- 状态变为 resolved 会触发后续的 then 回调
- 状态变为 rejected 会触发后续的 catch 回调
// 间接返回一个 resolved 状态
Promise.resolve(100)
// 间接返回一个 rejected 状态
Promise.reject('some error')
// 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)
}).catch(() => {console.log(2)
}).then(() => {console.log(3)
}) //1 3
// 第二题
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
console.log(1)
throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise
console.log(2)
}).then(() => {console.log(3)
}) //1 2 3
// 第三题
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
console.log(1)
throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise
console.log(2)
}).catch(() => {console.log(3)
}) //1 2
async/await
- Promise 的 then,catch 链式调用, 也是基于回调函数
- 用同步的形式编写异步, 彻底毁灭回调函数
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
}
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()
console.log(img1)
// 加载第二张图片
const img2 = await loadImg2()
console.log(img2)
} catch (ex) {console.error(ex)
}
})()
和 Promise 的关系
- async 函数返回后果都是 Promise 对象(如果函数内没返回 Promise,则主动封装一下)
async function fn2() {return new Promise(() => {})
}
console.log(fn2() )
async function fn1() {return 100}
console.log(fn1() ) // 相当于 Promise.resolve(100)
- await 前面跟 Promise 对象:会阻断后续代码,期待状态变为 resolved,才获取后果并继续执行
- await 后续跟非 Promise 对象:会间接返回
(async function () {const p1 = new Promise(() => {})
await p1
console.log('p1') // 不会执行
})()
(async function () {const p2 = Promise.resolve(100)
const res = await p2
console.log(res) // 100
})()
(async function () {
const res = await 100
console.log(res) // 100
})()
(async function () {const p3 = Promise.reject('some err')
const res = await p3
console.log(res) // 不会执行
})()
- try…catch 捕捉 rejected 状态
(async function () {const p4 = Promise.reject('some err')
try {
const res = await p4
console.log(res)
} catch (ex) {console.error(ex)
}
})()
总结来看:
- async 封装 Promise
- await 解决 Promise 胜利
- try…catch 解决 Promise 失败
异步实质
await 是同步写法,但实质还是异步调用。
async function async1 () {console.log('async1 start')
await async2()
console.log('async1 end') // 要害在这一步,它相当于放在 callback 中,最初执行
}
async function async2 () {console.log('async2')
}
console.log('script start')
async1()
console.log('script end')
即,只有遇到了 await
,前面的代码都相当于放在 callback 里。
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()
宏工作和微工作
- 宏工作:setTimeout setInterval DOM 事件
- 微工作:Promise(对于前端来说)
- 微工作比宏工作执行的更早
console.log(100)
setTimeout(() => {console.log(200)
})
Promise.resolve().then(() => {console.log(300)
})
console.log(400)
// 100 400 300 200
event loop 和 DOM 渲染
再次回顾 event loop 的过程
- 每一次 call stack 完结,都会触发 DOM 渲染(不肯定非得渲染,就是给一次 DOM 渲染的机会!!!)
- 而后再进行 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 完结后(同步工作都执行完了),浏览器会主动触发渲染,不必代码干涉
// 另外,依照 event loop 触发 DOM 渲染机会,setTimeout 时 alert,就能看到 DOM 渲染后的后果了
setTimeout(function () {alert('setTimeout 是在下一次 Call Stack,就能看到 DOM 渲染进去的后果了')
})
宏工作和微工作的区别
- 宏工作:DOM 渲染后再触发
- 微工作:DOM 渲染前会触发
// 批改 DOM
const $p1 = $('<p> 一段文字 </p>')
const $p2 = $('<p> 一段文字 </p>')
const $p3 = $('<p> 一段文字 </p>')
$('#container')
.append($p1)
.append($p2)
.append($p3)
// // 微工作:渲染之前执行(DOM 构造已更新)// Promise.resolve().then(() => {// const length = $('#container').children().length
// alert(`micro task ${length}`)
// })
// 宏工作:渲染之后执行(DOM 构造已更新)setTimeout(() => {const length = $('#container').children().length
alert(`macro task ${length}`)
})
再深刻思考一下:为何两者会有以上区别,一个在渲染前,一个在渲染后?
- 微工作:ES 语法规范之内,JS 引擎来对立解决。即,不必浏览器有任何对于,即可一次性解决完,更快更及时。
- 宏工作:ES 语法没有,JS 引擎不解决,浏览器(或 nodejs)干涉解决。