JavaScript 的并发模型基于“事件循环”。
一、运行时概念
可视化描述Javascript执行引擎的主线程运行的时候,产生堆(heap
)和栈(stack)
,程序中代码依次进入栈中等待执行,若执行时遇到异步方法,该异步方法会被添加到用于回调的队列(queue)中【即JavaScript执行引擎的主线程拥有一个执行栈/堆和一个任务队列
- 栈: 函数调用形成了一个栈帧
function foo(b) {
var a = 10;
return a + b + 11;
}
function bar(x) {
var y = 3;
return foo(x * y);
}
console.log(bar(7)); // 返回 42
当调用 bar 时,创建了第一个帧 ,帧中包含了 bar 的参数和局部变量。当 bar 调用 foo 时,第二个帧就被创建,并被压到第一个帧之上,帧中包含了 foo 的参数和局部变量。当 foo 返回时,最上层的帧就被弹出栈(剩下 bar 函数的调用帧 )。当 bar 返回的时候,栈就空了。
- 堆: 对象被分配在一个堆中,即用以表示一大块非结构化的内存区域。
-
队列: 一个 JavaScript 运行时包含了一个待处理的消息队列。每一个消息都关联着一个用以处理这个消息的函数。
在事件循环期间的某个时刻,运行时从最先进入队列的消息开始处理队列中的消息。为此,这个消息会被移出队列,并作为输入参数调用与之关联的函数。正如前面所提到的,调用一个函数总是会为其创造一个新的栈帧。
函数的处理会一直进行到执行栈再次为空为止;然后事件循环将会处理队列中的下一个消息(如果还有的话)。
二、事件循环(Event Loop)
Event Loop- queue : 如上文的解释,值得注意的是,除了IO设备的事件(如load)会被添加到queue中,用户操作产生 的事件(如click,touchmove)同样也会被添加到queue中。队列中的这些事件会在主线程的执行栈被清空时被依次读取(队列先进先出,即先被压入队列中的事件会被先执行)。
- callback: 被主线程挂起来的代码,等主线程执行队列中的事件时,事件对应的callback代码就会被执行
【注:因为主线程从”任务队列”中读取事件的过程是循环不断的,因此这种运行机制又称为
Event Loop(事件循环)
】
console.log(1);
setTimeout(function() {
console.log(2);
},5000);
console.log(3);
//输出结果:
//1
//3
//2
解释:
- JavaScript执行引擎主线程运行,产生heap和stack
- 从上往下执行同步代码,log(1)被压入执行栈,因为log是webkit内核支持的普通方法而非WebAPIs的方法,因此立即出栈被引擎执行,输出1
- JavaScript执行引擎继续往下,遇到setTimeout()t异步方法(如图,setTimeout属于WebAPIs),将setTimeout(callback,5000)添加到执行栈
- 因为setTimeout()属于WebAPIs中的方法,JavaScript执行引擎在将setTimeout()出栈执行时,注册setTimeout()延时方法交由浏览器内核其他模块(以webkit为例,是webcore模块)处理
- 继续运行setTimeout()下面的log(3)代码,原理同步骤2
- 当延时方法到达触发条件,即到达设置的延时时间时(5秒后),该延时方法就会被添加至任务队列里。这一过程由浏览器内核其他模块处理,与执行引擎主线程独立
- JavaScript执行引擎在主线程方法执行完毕,到达空闲状态时,会从任务队列中顺序获取任务来执行。
- 将队列的第一个回调函数重新压入执行栈,执行回调函数中的代码log(2),原理同步骤2,回调函数的代码执行完毕,清空执行栈
- JavaScript执行引擎继续轮循队列,直到队列为空
- 执行完毕
三、微任务(Macrotask) 和 宏任务(Microtask)
Event Loop 2不同的任务源会被分配到不同的 Task 队列中,任务源可以分为
微任务(microtask)
和 宏任务(macrotask)
。在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task。
Event Loop 执行顺序如下所示:
- 首先执行同步代码,这属于宏任务
- 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行
- 执行所有微任务
- 当执行完所有微任务后,如有必要会渲染页面
- 然后开始下一轮 Event Loop,执行宏任务中的异步代码,也就是 setTimeout 中的回调函数
实例代码
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
// script start
// async2 end
// Promise
// script end
// promise1
// promise2
// async1 end
// setTimeout
微任务包括: promise
,MutationObserver
。
宏任务包括: script
, setTimeout
,setInterval
,setImmediate
,I/O
,UI rendering
。
这里很多人会有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话才会先执行微任务。
四、永不阻塞
事件循环模型的一个非常有趣的特性是,与许多其他语言不同,JavaScript 永不阻塞。 处理 I/O 通常通过事件和回调来执行,所以当一个应用正等待一个 IndexedDB
查询返回或者一个 XHR
请求返回时,它仍然可以处理其它事情,比如用户输入。
遗留的例外是存在的,如 alert
或者同步 XHR,但应该尽量避免使用它们。
参考: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Using_promises
前端面试之道
https://blog.kaolafed.com/2017/04/21/JavaScript%E5%B9%B6%E5%8F%91%E6%A8%A1%E5%9E%8B%E4%B8%8EEvent%20Loop/
网友评论