一、概要
- JavaScript是一个单线程的脚本语言
执行顺序:主任务(同步任务) -> 微任务 -> 宏任务
在当前的微任务没有执行完成时,是不会执行下一个宏任务的
1.面试题
setTimeout(_ => console.log(4))//宏
new Promise(resolve => {
resolve()
console.log(1)//直接执行
}).then(_ => {
console.log(3)//微
})
console.log(2)//直接执行
//输出:
//1 -> 2 -> 3 -> 4
setTimeout(() => console.log(1));//宏
setImmediate(() => console.log(2));//宏
process.nextTick(() => console.log(3));//微
Promise.resolve().then(() => console.log(4));//微
(() => console.log(5))();//直接执行
//输出
//5 -> 3 -> 4 -> 1-> 2
2.复杂的任务处理
setTimeout(() => {
console.log('set1')
new Promise((resolve, reject) => {
console.log('pr1')
resolve()
}).then( () => {
console.log('then1')
})
});
setTimeout(() => {
console.log('set2')
});
new Promise((resolve, reject) => {
console.log('pr2')
resolve()
}).then( () => {
console.log('then2')
})
new Promise((resolve, reject) => {
console.log('pr3')
resolve()
setTimeout(() => {
console.log('set3')
});
}).then( () => {
console.log('then3')
})
setTimeout(() => {
console.log('set4')
new Promise((resolve, reject) => {
console.log('pr4')
resolve()
}).then( () => {
console.log('then4')
})
});
setTimeout(() => {
console.log('set5')
});
console.log('1')
// node输出:pr2 -> pr3 -> 1 -> then2 -> then3 -> set1 -> pr1 -> set2 -> set3 -> set4 -> pr4 -> set5 -> then1 -> then4
// Chrome输出:pr2 -> pr3 -> 1 -> then2 -> then3 -> set1 -> pr1 -> then1 -> set2 -> set3 -> set4 -> pr4 -> then4 -> set5
二、libuv

1.特点
负责各种回调函数的执行时间,毕竟异步任务最后还是要回到主线程,一个个排队执行
- 同步任务总是比异步任务更早执行
- 异步任务: 本次循环 | 次轮循环(循环:事件循环(
event loop))- Node 规定,
process.nextTick和Promise的回调函数,追加在本轮循环,即同步任务一旦执行完成,就开始执行它们。而setTimeout、setInterval、setImmediate的回调函数,追加在次轮循环。
2.执行顺序
- 执行同步任务
- 发出异步请求
- 规划定时器生效时间
- 执行process.nextTick()等等
- 执行事件循环
2.1.事件循环
- timers
- I/O callbacks
- idle, prepare
- poll
- check
- close callbacks

2.2.事件点解释
2.2.1. timers
定时器阶段,处理setTimeout()和setInterval()的回调函数。在这个阶段,主线程会检查当前时间是否满足定时器条件,满足则执行回调函数,否则进入下一阶段。
2.2.2. I/O callbacks
除了以下操作的回调函数,其他的回调函数都在这个阶段执行。
· setTimeout()和setInterval()的回调函数
· setImmediate()的回调函数
· 用于关闭请求的回调函数,比如socket.on('close', ...)
2.2.3. idle, prepare
该阶段只供 libuv 内部调用,可以忽略。
2.2.4. Poll
这个阶段是轮询时间,用于等待还未返回的 I/O 事件,比如服务器的回应、用户移动鼠标等等。
这个阶段的时间会比较长。如果没有其他异步任务要处理(比如到期的定时器),会一直停留在这个阶段,等待 I/O 请求返回结果。
2.2.5. check
该阶段执行setImmediate()的回调函数。
2.2.6. Close callbacks
该阶段执行关闭请求的回调函数,比如socket.on('close', ...)。
2.2.7. Demo
const fs = require('fs');
const timeoutScheduled = Date.now();
// 异步任务一:100ms 后执行的定时器
setTimeout(() => {
const delay = Date.now() - timeoutScheduled;
console.log(`${delay}ms`);
}, 100);
// 异步任务二:文件读取后,有一个 200ms 的回调函数
fs.readFile('test.js', () => {
const startCallback = Date.now();
while (Date.now() - startCallback < 200) {
// 什么也不做
}
});
// 执行顺序:
// timer(条件不符合) -> readFile(大约100ms) -> timer(条件不符合) -> readFile回调函数(200ms,执行到一半100ms后) -> timer(条件符合,但是readFile回调函数还未执行结束,等待执行结束) -> readFile回调函数执行结束 -> timer执行 -> 输出200+ms
三、同步任务
JavaScript是单线程,默认主任务执行。比如dom加载、css加载等等。
1.click事件
类似于**dispatchEvent**,同步任务执行
document.body.addEventListener('click', _ => console.log('click'))
document.body.click()
document.body.dispatchEvent(new Event('click'))
console.log('done')
// > click
// > click
// > done
2.MutationObserver监听
连续多次触发,只会在最后一次触发回调
new MutationObserver(_ => {
console.log('observer')
// 如果在这输出DOM的data-random属性,必然是最后一次的值
console.log('num = ' + document.body.getAttribute('data-num'));
}).observe(document.body, {
attributes: true
})
document.body.setAttribute('data-num', Math.random())
document.body.setAttribute('data-num', Math.random()*10)
document.body.setAttribute('data-num', Math.random()*100)
document.body.setAttribute('data-num', Math.random()*10000)
// 输出:
// ovserver
// random = 3782.6350323922743(说明只执行了最后一次的回调函数)
四、宏任务
1.setTimeout
2.setInterval
3.I/O
4.setImmediate
该特性是非标准的,请尽量不要在生产环境中使用它
| 方法 | 浏览器 | Node |
|---|---|---|
| setTimeout | ✅ | ✅ |
| setInterval | ✅ | ✅ |
| setImmediate | ❌ | ✅ |
| requestAnimationFrame | ✅ | ❌ |
requestAnimationFrame姑且也算是宏任务吧,requestAnimationFrame在MDN的定义为,下次页面重绘前所执行的操作,而重绘也是作为宏任务的一个步骤来存在的,且该步骤晚于微任务的执行。
告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。
5.Demo
setTimeout(() => console.log('1'));
setImmediate(() => console.log('2'));
console.log('3');
new Promise((re, rj) => {
console.log('4')
re()
}).then(()=> {
console.log('5')
})
//浏览器: 3 -> 4 -> 5 -> 2 -> 1
//node: 3 -> 4 -> 5 -> 1 -> 2
五、微任务
| 任务 | 浏览器 | Node |
|---|---|---|
| process.nextTick | ❌ | ✅ |
| MutationObserver | ✅ | ❌ |
| Promise.then catch finally | ✅ | ✅ |
1.Promise.then
2.process.nextTick()
2.1.问题
class Lib extends require('events').EventEmitter {
constructor () {
super()
this.emit('init')
}
}
const lib = new Lib()
lib.on('init', _ => {
// 这里将永远不会执行
console.log('init!')
})
// 由于构造方法(实例化对象)是同步执行,new之后回调就跟着执行了,但是监听还未执行,导致不会执行
2.2.改造
通过nextTick,将任务转为微任务,主任务队列为空后立即执行
class Lib extends require('events').EventEmitter {
constructor () {
super()
process.nextTick(_ => {
this.emit('init')
})
}
}
const lib = new Lib()
lib.on('init', _ => {
// 这里将永远不会执行
console.log('init!')
})
// 输出
// init!
3.MutationObserver
listen for attribute changes on the dom.
4.参考文档
[动画]jakearchibald.com/2015/tasks-…
[官网]nodejs.org/en/docs/gui…