javascript: 一门单线程非阻塞脚本语言。
单线程和任务队列
- 单线程就意味着,所有任务需要排队,前一个任务结束,才会执行有一个任务。如果前一个任务耗时很长,有一个任务就不得不一直等待。
- 如果排队是因为计算量过大,CPU忙不过来,也可以说得过去,但是绝大多数时间,cpu是闲着的,因为IO设备(输入输出)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行!
- js语言设计者意识到,这是主线程完全可以不管IO设备 ,挂起处于等待中的任务,先运行排在后边的任务,等到IO设备反悔了结果,再回过头把挂起的任务继续执行下去。
- 于是,所有的任务可以分为两种,一种是同步任务,另外一种是异步任务。同步任务指的是,在主线程上,排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程,而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
javascript事件循环
- 同步任务: 直接通过主线程执行,如script代码
- 异步任务:进入Event Table,并注册回调函数--->Event Queue,等主线程的执行栈为空时候,读取Event Queue里面的函数,进入主线程,如setTimeout() 、promise.then()等
javascript是单线程的,但是浏览器是多线程的,典型的浏览器有如下线程:
- javascript引擎线程
- 界面渲染线程
- 浏览器事件触发线程
- Http请求线程
关于javascript的单线程
- 应用场景决定了javascript的单线程特性,假如javascript是多线程,同时进行:一个线程对某一个dom进行添加属性操作,另一个线程对该线程进行删除操作,那么浏览器该听哪一个。这就决定了javascript必须是单线程。
- web worker: 是一个多线程。它出现的目的是当浏览器有大量密集的计算或者响应很长时间的运算的时候,页面出现卡顿,可以起一个worker子线程,主线程和worker线程互不干预,这样页面就可以进行点击之类的操作,但这个子线程不能操作DOM元素。
任务队列
js中,有两类任务队列,宏任务(macro tasks)和微任务(micro tasks)。宏任务队列可以有多个,微任务队列只有一个。
- 宏任务: script(全局任务),setTimeout,setInterval,setImmediate(node),,I/O,UI rendering.
- 微任务: process.nextTick(node.js中进程相关的对象),Promise, Object, Observer,MutationObserver.
宏任务(macro tasks)和微任务(micro taks)
macrotaks和microtask表示异步任务的两种分类。
宏任务: js内部(任务队列)的任务,严格按照时间顺序执行压栈和执行。如setTimeout/setInterval/setImmediate/MessageChannel等
微任务:通常来说,就是需要在当前任务执行结束后立即执行的任务,例如需要对一系列的任务做出回应,或者是需要异步的执行任务,而又不需要分配一个新的任务,这样便可以减小一点性能的开销。
在挂起任务时,js引擎将所有任务按照类别分到这两个队列中,首先在macrotask的队列(这个队列也被叫做task queue)中取出第一个任务,执行完毕后取出microtask队列中的所有任务顺序执行;之后再取macrotask任务,周而复始,直到两个队列的任务都执行完。
宏任务和微任务的关系:
宏任务与微任务属性机制
宏任务
# | 浏览器 | node |
---|---|---|
setTimeout | ✓ | ✓ |
setInterval | ✓ | ✓ |
setImmediate | ✓ | ✓ |
requestAnimationFrame | ✓ | × |
微任务
# | 浏览器 | node |
---|---|---|
process.nextTick | × | ✓ |
MutationObserver | ✓ | × |
Promise.then catch finally | ✓ | ✓ |
运行机制:
- 在执行栈中执行一个宏任务
- 执行过程中遇到微任务,将微任务添加到为任务队列中
- 当前宏任务执行完毕,立即执行微任务队列中的任务
- 当前微任务队列中的任务执行完毕,检查渲染,GUI线程接管渲染
- 渲染完毕后,js线程接管,开启下一次事件循环,执行下一次宏任务(从任务队列中按顺序取)
看下面的代码
scss
<script>
console.log(1)
setTimeout(function() {
console.log(2)
process.nextTick(() => {
console.log(3)
})
new Promise(resolve=> {
console.log(4)
resolve()
}).then(()=> {
console.log(5)
})
})
process.nextTick(()=> {
console.log(6)
})
new Promise(resolve=> {
console.log(7)
resolve()
}).then(()=>{
console.log(8)
})
setTimeout(()=> {
console.log(9)
process.nextTick(()=> {
console.log(10)
})
new Promise(resolve=> {
console.log(11)
resolve()
}).then(()=>{
console.log(12)
})
})
</script>
- 首先浏览器执行js进入第一个宏任务进入主线程,直接打印console.log(1)
- 遇到 setTimeout 分发到宏任务Event Queue中
- 遇到 process.nextTick 丢到微任务Event Queue中
- 遇到 Promise, new Promise 直接执行 输出 console.log(7);
- 执行then 被分发到微任务Event Queue中
- 第一轮宏任务执行结束,开始执行微任务 打印 6,8
- 第一轮微任务执行完毕,执行第二轮宏事件,执行setTimeout
- 先执行主线程宏任务,在执行微任务,打印'2,4,3,5'
- 在执行第二个setTimeout,同理打印 '9,11,10,12'
- 整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。
注意:以上是在浏览器环境下执行的数据,只作为宏任务和微任务的分析,我在node环境下测试打印出来的顺序为:1,7,6,8,2,4,9,11,3,10,5,12。node环境执行结果和浏览器执行结果不一致的原因是:浏览器的Event loop是在HTML5中定义的规范,而node中则由libuv库实现。libuv库流程大体分为6个阶段:timers,I/O callbacks,idle、prepare,poll,check,close callbacks,和浏览器的microtask,macrotask那一套有区别。
注意事项:
- 先是宏任务-->微任务-->宏任务-->微任务一直循环下去;
- script代码为第一层宏任务,如果有setTimeout,setInterval,则他们的回调函数会成为第二层的宏任务,
- promise.then()和process.nextTick()是微任务,在执行完该一层的宏任务后执行,且process.nextTick()优先于promise.then();
小结:
- macrotask(按优先级顺序排列): script(你的全部JS代码,"同步代码"), setTimeout, setInterval, setImmediate, I/O,UI rendering
- microtask(按优先级顺序排列):process.nextTick,Promises(这里指浏览器原生实现的 Promise), Object.observe, MutationObserver
- JS引擎首先从macrotask queue中取出第一个任务,执行完毕后,将microtask queue中的所有任务取出,按顺序全部执行;
- 然后再从macrotask queue(宏任务队列)中取下一个,执行完毕后,再次将microtask queue(微任务队列)中的全部取出;
- 循环往复,直到两个queue中的任务都取完。
提别强调:
队列的优先级执行顺序为: 先执行同步和立即执行任务>microtask>macrotask
** 官方文档:Javascript Event Loop