参考链接:
1. 单线程的js
js作为主要运行在浏览器的脚本语言,js主要用途之一是操作DOM。
在js高程中举过一个栗子,如果js同时有两个线程,同时对同一个dom进行操作,这时浏览器应该听哪个线程的,如何判断优先级?
为了避免这种问题,js必须是一门单线程语言,并且在未来这个特点也不会改变。
2. 执行栈和任务队列
因为js是单线程语言,当遇到异步任务(如ajax操作等)时,不可能一直等待异步完成,再继续往下执行,在这期间浏览器是空闲状态,显而易见这会导致巨大的资源浪费。
(1)执行栈
当执行某个函数、用户点击一次鼠标,Ajax完成,一个图片加载完成等事件发生时,只要指定过回调函数,这些事件发生时就会进入任务队列中,等待主线程读取,遵循先进先出原则。
执行任务队列中的某个任务,这个被执行的任务就称为执行栈。
(2)主线程
要明确的一点是,主线程跟执行栈是不同概念,主线程规定现在执行执行栈中的哪个事件。
主线程循环:即主线程会不停的从执行栈中读取事件,会执行完所有栈中的同步代码。
当遇到一个异步事件后,并不会一直等待异步事件返回结果,而是会将这个事件挂在与执行栈不同的队列中,我们称之为任务队列(Task Queue)。
当主线程将执行栈中所有的代码执行完之后,主线程将会去查看任务队列是否有任务。如果有,那么主线程会依次执行那些任务队列中的回调函数。
(3)同步任务与异步任务
- 同步和异步任务分别进入不同的执行”场所”,同步的进入主线程,异步的进入Event Table并注册其回调函数
- 当指定的事情完成时,Event Table会将这个回调函数移入Event Queue。
- 主线程内的任务执行完毕为空,会去Event Queue读取对应的回调函数,进入主线程执行。
- 上述过程会不断重复,也就是常说的Event Loop(事件循环)。
纯同步任务:
console.log('start')
console.log('end')
复制代码
上边的执行结果大家肯定都明白,先输出start,再输出end,这一段代码会进入同步队列,顺序执行。
同步+异步:
console.log('start')
setTimeout(function() {
console.log('setTimeout')
}, 0)
console.log('end')
复制代码
注意:
注意:setTimeout的延迟时间表示的是xxx时间后将回调函数添加到异步队列中,而不是xxx时间后执行回调函数,所以它的定时并不精确。假设setTimeout规定2秒后执行,但同步队列中有一个函数,执行花了很长时间,甚至花了1秒。那么这时setTimeout中的回调也会等上至少1秒之后,同步任务都执行完了,再去执行。这时候的setTimeout回调执行的时机就会超过2秒,也就是至少3秒。
这样的情况,先执行同步队列任务,函数调用栈执行到setTimeout时,setTimeout会在规定的时间点将回调函数放入异步队列,等待同步队列的任务被执行完,立即执行setTimeout回调,所以结果是:start、end、setTimeout。
3. 宏任务与微任务
微任务和宏任务皆为异步任务,它们都属于一个队列,主要区别在于他们的执行顺序,Event Loop的走向和取值。那么他们之间到底有什么区别呢?
其中宏任务(task)
包括:
- script(整体代码)
- setTimeout, setInterval, setImmediate(属于node),
- I/O
- UI rendering
ajax请求不属于宏任务,js线程遇到ajax请求,会将请求交给对应的http线程处理,一旦请求返回结果,就会将对应的回调放入宏任务队列,等请求完成执行。
微任务(jobs)
包括:
- process.nextTick(属于node)
- Promise
- Object.observe(已废弃)
- MutationObserver(html5新特性)
注意:这里的宏/微任务大多都表示的都是对应类型操作的回调函数,而Promise的微任务对应的不是其直接的回调函数,而是Promise.then回调
示例1:
console.log('start')
setTimeout(function() {
console.log('timeout')
}, 0)
new Promise(function(resolve) {
console.log('promise')
resolve()
}).then(function() {
console.log('promise resolved')
})
console.log('end')
复制代码
执行过程解析:
- 建立执行上下文,进入执行栈开始执行代码,打印start
- 往下执行,遇到setTimeout,将回调函数放入宏任务队列,等待执行
- 继续往下,有个new
Promise,其回调函数并不会被放入其他任务队列,因此会同步地执行,打印promise,但是当resolve后,.then会把其内部的回调函数放入微任务队列 - 执行到了最底部的代码,打印出end。这时,主执行栈清空了,开始寻找微任务队列里有没有可执行代码
- 发现了微任务队列中有之前放进去的代码,执行打印出promise resolved,第一次循环结束
- 再开始第二次循环,从宏任务开始,检查宏任务队列是否有可执行代码,发现有一个,打印timeout
所以,打印顺序是:start–>promise–>end–>promise resolved–>timeout
示例2:
console.log('第一次循环主执行栈开始')
setTimeout(function() {
console.log('第二次循环开始,宏任务队列的第一个宏任务执行中')
new Promise(function(resolve) {
console.log('宏任务队列的第一个宏任务的微任务继续执行')
resolve()
}).then(function() {
console.log('第二次循环的微任务队列的微任务执行')
})
}, 0)
new Promise(function(resolve) {
console.log('第一次循环主执行栈进行中...')
resolve()
}).then(function() {
console.log('第一次循环微任务,第一次循环结束')
setTimeout(function() {
console.log('第二次循环的宏任务队列的第二个宏任务执行')
})
})
console.log('第一次循环主执行栈完成')
复制代码
- 第一次循环
1:进入执行栈执行代码,打印第一次循环主执行栈开始
2:遇到setTimeout,将回调放入宏任务队列等待执行
3:promise声明过程是同步的,打印第一次循环主执行栈进行中…,resolve后遇到.then,将回调放入微任务队列
4:打印第一次循环主执行栈完成
5:检查微任务队列是否有可执行代码,有一个第三步放入的任务,打印第一次循环微任务,第一次循环结束,第一次循环结束,同时遇到setTimeout,将回调放入宏任务队列
- 第二次循环
1:从宏任务入手,检查宏任务队列,发现有两个宏任务,分别是第一次循环第二步和第一次循环第五步被放入的任务,先执行第一个宏任务,打印第二次循环开始,宏任务队列的第一个宏任务执行中
2:遇到promise声明语句,打印宏任务队列的第一个宏任务继续执行,这时候又被resolve了,又会将.then中的回调放入微任务队列,这是这个宏任务队列中的第一个任务还没执行完
3:第一个宏任务中的同步代码执行完毕,检查微任务队列,发现有一段第二步放进去的代码,执行打印第二次循环的微任务队列的微任务执行,此时第一个宏任务执行完毕
4:开始执行第二个宏任务,打印第二次循环的宏任务队列的第二个宏任务执行,所有任务队列全部清空,执行完毕
示例3:
这里的await和await下面的代码可看作是:
new Promise(console.log(‘async2’)).then(()=>{console.log(‘async1 end’)})
复制代码
核心思想:优先执行第一次事件循环的宏任务,执行过程中遇到微任务扔到微任务队列,第一次循环宏任务执行完后,执行第一次循环宏任务过程中遇到的所有微任务。然后按此规则再执行下一次循环的宏任务。。。(若微任务执行中遇到宏任务也将其扔到宏任务对列)