一. JavaScript是一门单线程非阻塞的脚本语言(这是由最初的用途所决定:与浏览器交互)
-
单线程
JavaScript语言的一大特点是单线程,为何是单线程呢?原因之一在于当初也是最主要的执行环境 —浏览器中,在浏览器中,我们需要执行各种各样的dom操作,试想一下,如果是多线程的,那么两个或者多个线程对同一dom操作时,比如一个向其添加事件,另一个删除了这个dom,此时就会出现问题了。为了保证不会发生类似的情况,JavaScript选择只用一个主线程来执行代码,也就是说,每次只能执行一项任务,其他任务都得按照顺序排队等待被执行,只有当前的任务执行完成之后,才会执行下一个任务。
当然,现如今人们也意识到,单线程在保证执行顺序的同时,也限制了JavaScript的效率,因此开发出了web worker技术(这项技术号称让JavaScript成为一门多线程语言),但是使用web worker这项技术开的多线程也存在很多限制,例如:所有新线程都受主线程的完全控制,不能独立执行。这意味着这些“线程” 实际上应属于主线程的子线程。另外,这些子线程并没有执行I/O操作的权限,只能为主线程分担一些诸如计算等任务。所以严格来讲这些线程并没有完整的功能,也因此这项技术并非改变了javascript语言的单线程本质。
2.非阻塞
非阻塞则是当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如I/O事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。非阻塞是通过事件循环机制实现的。 JS通常是非阻塞的,除了某些特殊情况,JS会停止代码执行,比如:
1. alert, confirm, prompt(除了Opera)
2. “页面上的程序正忙”的系统对话框弹出
二. 浏览器中的js事件循环机制
1. 任务队列
所有的任务可以分为同步任务和异步任务,同步任务,顾名思义,就是立即执行的任务,同步任务一般会直接进入到主线程中执行;而异步任务,就是异步执行的任务,比如ajax网络请求,setTimeout 定时函数等都属于异步任务,异步任务会通过任务队列( Event Queue )的机制来进行协调。 同步和异步任务分别进入不同的执行环境,同步的进入主线程,即主执行栈,异步的进入 Event Queue
。主线程内的任务执行完毕为空,会去 Event Queue 读取对应的任务,推入主线程执行。 上述过程的不断重复就是我们说的 Event Loop
(事件循环)。
如下图所示:
同步和异步任务分别进入不同的执行环境,同步的进入主线程,即主执行栈,异步的进入任务队列。主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。 上述过程的不断重复就是我们说的 Event Loop (事件循环)。
2.事件循环
在事件循环中,每进行一次循环操作称为tick
,每一次 tick 的任务处理模型是比较复杂的,其关键的步骤可以总结如下:
- 执行栈选择最先进入队列的宏任务(通常是
script
整体代码),开始执行宏任务 ---------------------------------------------宏任务微任务在下面👇 -
检查是否存在微任务( Microtasks ),如果存在则不停地执行,直至清空微任务队列(Microtask Queue)
-
更新 render(每一次事件循环,浏览器都可能会去更新渲染)
-
主线程重复执行上述步骤
我们可以认为任务队列执行优先级: 同步 > 微任务 > 宏任务
如下图所示:
3. 宏任务与微任务
异步任务分为 宏任务(macrotask) 与 微任务 (microtask)
1.宏任务
script(整体代码)、setTimeout、setInterval、UI 渲染、 I/O、postMessage、 MessageChannel、setImmediate(Node.js 环境)
2.微任务
Promise、 MutaionObserver、process.nextTick(Node.js环境)
三. 举例(面试常谈)
1. 先来个简单的
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
输出结果
2.稍微增加点难度
setTimeout(()=>{
console.log(1)
},0)
let a=new Promise((resolve)=>{
console.log(2)
resolve()
}).then(()=>{
console.log(3)
}).then(()=>{
console.log(4)
})
console.log(5)
// 输出
//2 5 3 4 1
3.再复杂一些
new Promise((resolve,reject)=>{
console.log("promise1")
resolve()
}).then(()=>{
console.log("then11")
new Promise((resolve,reject)=>{
console.log("promise2")
resolve()
}).then(()=>{
console.log("then21")
}).then(()=>{
console.log("then23")
})
}).then(()=>{
console.log("then12")
})
输出结果
4.难度++
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log( 'async2');
}
console.log("script start");
setTimeout(function () {
console.log("settimeout");
},0);
async1();
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log('script end');
输出结果