1. 单线程的JavaScript
-
JavaScript是单线程的语言这,由它的用途决定的,作为浏览器的脚本语言,主要负责和用户交互,操作DOM。
-
假如JavaScript是多线程的,有两个线程同时操作一个DOM节点,一个负责删除DOM节点,一个在DOM节点上添加内容,浏览器该以哪个线程为标准呢?
-
所以,JavaScript的用途决定它只能是单线程的,过去是,将来也不会变。
-
HTML5的WebWorker允许JavaScript主线程创建多个子线程,但是这些子线程完全受主线程的控制,且不可操作DOM节点,所以JavaScript单线程的本质并没有发生改变。
2. 同步任务和异步任务
-
JavaScript是单线程语言,就意味着任务需要排队执行,只有前一个执行完成,后一个才可以执行。
-
如果前一个任务非常耗时呢?比如操作IO设备、网络请求等,后面的任务就会被阻塞,页面就会被卡住,甚至崩溃,用户体验非常差。
-
如果JavaScript的主线程在遇到这些耗时的任务时,将其挂起,先执行后面的任务,等挂起的任务有结果以后再回头执行,这样就可以解决耗时任务阻塞主线程的问题了。
-
于是,所有的任务就可以分为两种,同步任务和异步任务,同步任务放在主线程中执行,异步任务被挂起,不进入主线程执行(让主线程阻塞等待),当其有结果了,再放入主线程中执行。
3. 任务队列和Event Loop
3.1 任务队列
-
任务队列是一个事件队列,也可以理解成消息队列,当挂起的异步任务就绪以后就会在任务队列中放置相应的事件,表示该任务可以进入主线程中执行了。
-
任务队列中的事件,除了IO设备的事件,还有网络请求,鼠标点击、滚动等,只要为事件指定过回调函数,这些事件发生时就会进入任务队列,等待主线程来读取,然后执行相应的回调函数。
-
回调函数其实就是被挂起来的异步任务,比如:Ajax请求,请求成功或失败以后执行的回调函数就是异步任务。
-
任务队列是一个先进先出的数据结构,排在前面的事件,只要主线程一空,就会优先被读取。
3.2 Event Loop
- 主线程从任务队列读取事件,这个过程是循环不断的,所以JavaScript这种运行机制又称为Event Loop(事件循环)
4. 宏任务和微任务
异步任务可进一步划分为宏任务和微任务,相应的任务队列也有两种,分别为宏任务队列和微任务队列。
4.1 宏任务
- setTimeout、setInterval、setImmediate会产生宏任务
4.2 微任务
- requestAnimationFrame、IO、读取数据、交互事件、UI render、Promise.then、MutationObserve、process.nextTick会产生微任务
4.3 浏览器中的JavaScript脚本执行过程
4.3.1 过程描述
a. JavaScript脚本进入主线程, 开始执行
b. 执行过程中如果遇到宏任务和微任务,分别将其挂起,只
有当任务就绪时将事件放入相应的任务队列c. 脚本执行完成,执行栈清空
d. 去微任务队列依次读取事件,并将相应的回调函数放入执行栈运行,如果执行过程中遇到宏任务和微任务,处理方式同 b, 直到微任务队列为空
e. 浏览器执行渲染动作, GUI渲染线程接管,直到渲染结束
f. JS线程接管,去宏任务队列依次读取事件,并将相应的回调函数放入执行栈, 开始下一个宏任务的执行,过程为b -> c -> d -> e
-> f, 如此循环g. 直到执行栈、宏任务队列、微任务队列都为空,脚本执行结束
4.3.2 示例
// 脚本
console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
const p = new Promise((resolve) => {
setTimeout(() => {
console.log(3)
resolve()
}, 1000)
console.log(4)
})
p.then(() => {
console.log(5)
})
console.log(6)
-
a. 脚本放入执行栈开始实行
-
b. 执行到console.log(1), 输入1
-
c. 执行到setTimeout,遇到宏任务,将其挂起,由于延时 0ms,将在 4ms后在宏任务队列产生一个定时事件, 我们叫定时A
-
d. 程序继续向下执行,执行new Promise(),并运行其参数,遇到第二个定时任务(宏任务),叫它定时B,并将其挂起,执行console.log(4), 输出4
-
e. 遇到微任务p.then(), 将其挂起
-
f. 向下执行遇到console.log(6), 输出6
-
g. 执行栈清空,读取微任务队列,发现为空,因为p.then()含没有就绪,它的就绪依赖与第一个定时任务(定时A)的执行
-
h. 执行栈为空,微任务队列为空,执行浏览器的渲染动作
-
i. 读取宏任务队列,读取第一个就绪的宏任务,为定时任务A,将其回调函数放入执行栈开始执行,执行console.log(2), 输入2
-
j. 执行栈清空,微任务队列为空,渲染
-
k. 开始执行下一个就绪的宏任务,定时任务B,并将其回调函数放入执行栈执行,执行console.log(3), 输出3,并执行resolve(), p.then()就绪,在微任务队列放入相应的事件
-
o. 执行栈清空,读取微任务队列,发现不为空,读取第一个就绪的事件,并将其对应的回调函数放入执行栈执行,执行console.log(5), 输出5
-
p. 执行栈清空,微任务队列为空,渲染,然后发现宏任务队列为空,本次脚本执行彻底结束
输出结果为: 1 4 6 2 3 5
async function async1 () {
console.log('async1_1')
await async2()
console.log('async1_2')
}
async function async2 () {
console.log('async2')
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
async1()
new Promise(resolve => {
console.log('promise executor')
resolve()
}).then(() => {
console.log('promise then')
})
console.log('script end')
函数前加async,实际上返回的是一个promise,比如这里的async2函数,返回的是一个立即resoved promise
await会将后面的同步代码执行完成(async2),然后让出线程,将异步任务(Promise.then)挂起,这里的立即resolved promise,所以会在微任务队列添加一个事件,且排在下面的Promise.then之前
输出结果: script start => async1_1 => async2 => promise executor => script
end => async1_2 => promise then => setTimeout总结
-
如果把JavaScript脚本也当作初始的宏任务,那么JavaScript在浏览器端的执行过程就是这样:
-
先执行一个宏任务, 然后执行所有的微任务
-
再执行一个宏任务,然后执行所有的微任务
-
…
-
如此反复,执行执行栈和任务队列为空
-