JS单线程
在进程和线程(浏览器多进程和JS单线程)一文中已经提到,JavaScript是一门单线程的语言。因此,JavaScript在同一时间内只能做一件事,单线程意味着,如果在同一时间有多个任务(每一段JS程序都可以看做是一个任务),这些任务就需要进行排队,前一个任务执行完,才会执行下一个任务。比如说下面这段代码
function fun1() {
console.log(1);
}
function fun2() {
console.log(2);
}
fun1();
fun2();
// 输出 1 2
因为代码是从上到下依次执行的,执行完fun1(),才继续执行fun2(),因此会依次输出1,2。但是如果fun1()中的代码执行的是读取文件或者ajax操作,文件的读取和数据的获取都需要一定时间,如果我们完全等fun1()执行完才继续执行fun2(),对于用户而言,就意味着页面"卡死",严重影响用户体验。
JavaScript在设计的时候,就已经考虑到这个问题,主线程可以完全不用等待文件的读取完毕或ajax的加载成功,可以先挂起处于等待中的任务,先运行排在后面的任务,等到文件的读取或ajax有了结果后,再回过头执行挂起的任务。因此,就有了同步任务和异步任务的概念。
同步任务和异步任务
同步任务(synchronous):在主线程上排队执行的任务。只有前一个任务执行完毕,才能继续执行下一个任务。
异步任务(asynchronous):不进入主线程,而进入 任务队列 的任务。只有某个异步任务可以执行了,该任务才会进入主线程。
在任务队列中,其实还分为 宏任务队列(Task Queue)和微任务队列(Microtask Queue),对应的里面存放的就是 宏任务 和 微任务 。
常见宏任务:setTimeout、setInterval、 setImmediate(Node.js 环境)
常见微任务:Promise.then、process.nextTick(Node.js 环境)
个人的理解是,宏任务和微任务都是异步任务。 也有些文章将script整体代码归类到宏任务,而我理解它是第一执行的主线程,只能说每个人的理解方式不同,没有找到统一标准。
事件循环(Event Loop)
Event Loop主要包含三大块 :一个函数执行栈、一个宏任务队列和一个微任务队列。
同步任务都在主线程上执行,形成一个执行栈,在JS引擎上的任务(也就是主线程上的任务),只有前一个任务执行完毕,才能执行后一个任务。当JS引擎中的任务执行完成了,就会去查询异步的任务队列中是否有可以执行的任务。一旦这些异步任务可以执行了,就会将它添加到JS引擎中,以此循环。
具体运行机制如下:
- 在执行栈上的任务,只有前一个任务执行完毕,才能执行后一个任务;
- 当执行栈上的任务执行完毕之后,检查微任务队列,取出队列中所有事件压入执行栈执行;
- 完毕之后,检查宏任务队列,取出一个事件压入执行栈执行;
- 完毕之后,再次检查微任务队列,取出所有的事件压入执行栈执行;
- 完毕之后,检查宏任务队列,取出一个事件压入执行栈执行,重复该过程。
由于JS引擎从“任务队列”中读取事件的这整个过程是不断循环的,所以这种运行机制又称为 Event Loop(事件循环)。
注意:对于宏任务每次只从宏任务队列种取一个事件压入执行栈执行,微任务每次从微任务队列种取出所有的事件压入执行栈执行。可以简单理解为 微任务的优先级高于宏任务。
经典问题:
console.log('start');
setTimeout(() => {
console.log('setTimeout');
}, 0)
new Promise((resolve) => {
console.log('Promise');
resolve()
}).then(() => {
console.log('then1');
}).then(() => {
console.log('then2');
});
console.log('end');
- 主线程:start promise end
- 宏任务:setTimeout
- 微任务:then1 then2
任务队列既然是队列,就遵循先进先出原则,根据上述js运行机制,最终依次输出:
start promise end then1 then2 setTimeout
总结
-
计算机领域中的同步(Synchronous)和异步(Asynchronous)和我们生活中的同步和异步的概念是恰好相反的,感觉是翻译要背这个锅。生活中的同步,突出的是“同”,相同的步伐,一起行动,而计算机中的同步则是排队等待执行。在开发中无论是前端还是后端,同步和异步都是同种概念。
-
同步需要等待所有步骤执行完了才能继续往下执行,异步只需要发起调用后就可以继续其他逻辑。
-
同步会造成线程阻塞,但是异步执行不会造成自己的线程阻塞。
-
同步可以保证顺序一致,但是容易导致阻塞;异步可以解决阻塞问题,但是会改变顺序性,我们要根据不同的需求去写代码。