前端异步事件详解
js 事件机制
javascript 的单线程
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以JavaScript只能是单线程
其他的辅助线程
除了单线程作为主线程之外,还存在着其他线程,例如,处理ajax请求的线程,处理点击事件的线程等等,我们不做区分,暂时管它们叫做辅助线程,也被称为js的 Event Loop
同步与异步
由上可以得出结论,主线程里面的代码就是同步执行的,辅助线程的代码就是异步执行的
主线程同步
主线程运行变量赋值、循环等操作的代码就是同步代码,特点是直接运行,不需要等待,运行的也相对快。
异步代码主要场景
- click、mouseover等回调函数
- ajax 请求
- script 的refer、async 异步加载
- setTimeout、setInterval
- promise.then
- 模块化异步加载场景等
执行机制
浏览器解析js代码时候,如果碰到异步,会将回调函数代码插入Event Loop 任务队列去,当执行引擎空闲时候,从任务队列里面取出来执行,遵循先入先出的原则。
setTimeout(() => {
console.log(1)
})
setTimeout(() => {
console.log(2)
})
// log: 1 2
碰到 promise 情况稍稍有些不一样,promise.then 会另外开辟一个任务队列,而且优先执行这个队列,这种异步情况就叫做微任务,队列就叫微任务队列,上面的setTimeout 响应的就叫 宏任务
setTimeout(() => {
console.log(1)
})
new Promise((resolve) => {
resolve()
}).then(() => {
console.log(2)
})
console.log(3)
// log: 3 2 1
理解了这些,看下下面的代码
setTimeout(() => {
console.log(1)
new Promise((resolve) => {
resolve()
}).then(() => {
console.log(2)
})
})
new Promise((resolve) => {
resolve()
}).then(() => {
console.log(3)
setTimeout(() => {
console.log(4)
})
})
分析: 宏任务里面有微任务,微任务里面有宏任务,首先第一个打印1的setTimeout 会入宏任务队列, 微任务一结束,打印出,3最先打印出来,然后打印4的setTimeout 会在后面入宏任务队列,然后来执行宏任务1,打印出来1,寻找当前线程的微任务2, 打印出来2,最后执行宏任务4 ,将4打印处理
//log: 3 1 2 4
接下来看以下代码
console.log('1');
setTimeout(function () {
console.log('2');
Promise.resolve().then(function () {
console.log('3');
})
new Promise(function (resolve) {
console.log('4');
resolve();
}).then(function () {
console.log('5')
})
})
Promise.resolve().then(function () {
console.log('6');
})
new Promise(function (resolve) {
console.log('7');
resolve();
}).then(function () {
console.log('8')
})
setTimeout(function () {
console.log('9');
Promise.resolve().then(function () {
console.log('10');
})
new Promise(function (resolve) {
console.log('11');
resolve();
}).then(function () {
console.log('12')
})
})
最终输出:
// 1 7 6 8 2 4 3 5 9 11 10 12
流程如下:(括号内为输出)
- 主线程(1)
- 主线程(promise 直接执行传入函数)(7)
- 主线程的微任务队列(6,8)
- 宏任务队列0
- 宏任务队列0
- 宏任务队列[0]的微任务队列(3,5)
- 宏任务队列1
- 宏任务队列[1]的微任务队列(11,12)
值得注意的是: 宏任务队列可以有多个,微任务队列在当前线程里只能有一个,可以看成是当前线程结束后的一个回调函数组。
宏任务和微任务
宏任务和微任务的关系
就拿医院挂号排队为例,宏任务就是取号排队的事件,微任务就是可能有个病人他看病过程需要拍x光、去验血,排在后面的一位只能等,这时候拍x光和验血就组成了这个病人(宏任务)的微任务队列。只有当上一个病人x光和验血都进行完之后,才能进行下一个病人诊断。
宏任务 macro-task
宏任务就是指的是浏览器dom对象原生的方法属性和基于此的封装带来的异步执行。
宏任务的来源:
- xmlHttpRequest
- addEventListener
- setTimeout
- setInterval
- setImmediate(node.js)
- I/O(上传文件,解析文件需要)
- requestAnimationFrame(根据浏览器刷新频率更新操作)
- 框架的render
微任务 micro-task(Job)
微任务也是先出后进的队列,不同的是它来源于JavaScript,所以导致当前线程只有一个微任务队列,类似于 dom 的 onload 的概念
微任务来源:
- Promise
- node 的 process.nextTick
- Object.observe
无阻塞 I/O Event Loop 机制
图中的event loop中我们假设有A、B、C三个等待执行的命令队列,其中A和B都会在其执行的过程中触发I/O操作(图中右侧红色圆角矩形框,具体I/O操作可举例为“读取数据库数据”)。以A触发自身的I/O操作为例,常规的动态语言可能都会停住整个队列,等待I/O回馈后,才结束中断、继续运行下去。如果遇到I/O很耗时的情况,进程就会白白等待而浪费不少时间。为了解决此问题,NodeJS采用了event loop机制,将所有I/O操作都扔到线程池去处理,从而不再阻塞命令队列的进一步执行操作。因此从上图可以看到,即使A触发了自身的I/O,也不会阻塞队列的下一个命令B的执行。