“JavaScript定位:单线程、异步、非阻塞、解释型脚本语言。”
(曾经被问过一个问题:js 是单线程,为什么会有异步代码执行?)
为什么是单线程?JavaScript 的设计就是为了处理浏览器网页间的交互(DOM 操作的处理、UI 动画等),如果有多个线程同时操作 DOM,那么网页会是一团糟。
js 是单线程的,处理任务是一件接着一件处理。按照代码顺序,从上往下执行:
console.log('script start')
console.log('do something...')
console.log('script end')
// script start
// do something...
// script end
如果一个任务特别耗时,或者需要很长的时间才执行,按照 js 单线程的逻辑来,那么页面就会变得卡顿,会阻塞用户与网页间的交互。
但是实际上不是:
console.log('script start')
console.log('do something...')
setTimeout(() => {
console.log('timer over')
}, 1000)
console.log('click page')
console.log('script end')
// script start
// do something...
// click page
// script end
// timer over
即使有个定时器延时,也没有阻塞下面代码执行。那为什么说 js 是单线程的呢?
那是因为 js 单线程指的是浏览器负责解释和执行 JavaScript 代码的只有一个线程,即 js 引擎线程。但是浏览并不仅仅只有这一个线程(即 js 单线程浏览器多线程)。浏览器提供了多个线程:
- JS 引擎线程
- 事件触发线程
- 定时器触发线程
- 异步 HTTP 请求线程
- GUI 渲染线程
当遇到耗时任务时,比如定时器、DOM 事件监听、网络请求等,JS 引擎线程就会把他们交给相应的 webapi ,也就是浏览器相应的线程去处理,而 js 引擎线程会继续向下执行代码,所以即使有耗时请求,也不会出现阻塞。
异步操作
setTimeout(() => {
console.log('hello 0')
}, 1000)
dom.onclick = function () {
// do something...
}
上面的 setTimeout
函数便不会立刻返回结果,而是发起了一个异步,setTimeout 便是异步的发起函数或者是注册函数,() => {...} 便是异步的回调函数。
js 引擎只关心异步操作的发起和回调,其间的异步操作会交给浏览器的其他线程去执行。
异步操作一般包括:
- 网络请求
- 定时器
- DOM 事件监听
- ...
事件循环和消息队列
当 js 引擎遇到异步操作时,会将异步操作交给浏览器其他线程去维护,等待某个时机(比如定时器结束、DOM 被点击、http 请求成功),然后事件触发线程会将异步操作对应的回调函数放到消息队列中,等待 js 引擎线程去执行。
js 引擎线程会维护一个执行栈,同步任务会被依次加入到执行栈中被执行,执行后出执行栈。等执行栈为空时,事件触发线程会从消息队列中取一个任务加入到执行栈中执行,执行完后出执行栈,等执行栈为空时,时间触发线程会重复上一步操作。这种机制就被称为事件循环(event loop)机制。
消息队列是类似队列的数据结构,遵循先入先出(FIFO)的规则。
只有调用 promise 实例的 then 方法时,then 方法里面的回调才会被推入微任务中。
new Promise(resolve => {
resolve(1);
new Promise(resolve => {
resolve(3)
}).then((t) => console.log(t));
}).then(t => console.log(t));
// 3 1
事件触发线程将异步操作的回调加入到执行栈时,一定是在执行栈为空时才能加入。所以如果 js 引擎线程在执行同步代码期间,有异步操作到达了某个时机(比如定时时间很短),被事件触发线程加入到了消息队列中,此时事件触发线程不会马上将任务插入到执行栈中,而是会等待执行栈将同步任务执行完之后再将任务加入到执行栈中去执行。代码解释:
setTimeout(function () {
console.log('6666')
}, 0);
for (let i = 0; i < 99999; i++) {
console.log(i);
}
就算延时为 0ms,只是定时器的回调函数会立即加入消息队列而已,回调的执行还是得等执行栈为空(JS引擎线程空闲)时执行。
对于同是异步任务来说,在执行栈为空时,要看哪个任务先达到某个时机。
console.log('script start')
setTimeout(() => {
console.log('timer 1 over')
}, 1000)
setTimeout(() => {
console.log('timer 2 over')
}, 0)
console.log('script end')
// script start
// script end
// timer 2 over
// timer 1 over
这里会先打印 "timer 2 over",然后打印 "timer 1 over",尽管 timer 1 先被定时器触发线程处理,但是 timer 2 的callback会先加入消息队列。因为同是“同等级的”异步任务,但是第二个异步任务先达到时机(定时器时间结束)。
宏任务与微任务
console.log('script start')
setTimeout(function() {
console.log('timer over')
}, 0)
Promise.resolve().then(function() {
console.log('promise1')
}).then(function() {
console.log('promise2')
})
console.log('script end')
// script start
// script end
// promise1
// promise2
// timer over
// "promise 1" "promise 2" 做为微任务加入到微任务队列中,而 "timer over" 做为宏任务加入到宏任务队列中,它们同时在等待被执行,但是微任务队列中的所有微任务都会在开始下一个宏任务之前都被执行完。
所有任务分为 macrotask
和 microtask
:
-
macrotask:主代码块、setTimeout、setInterval等(可以看到,事件队列中的每一个事件都是一个 macrotask,现在称之为宏任务队列)
-
microtask:Promise.then (catch、 finally)、process.nextTick等。
JS 引擎线程首先执行主代码块。每次执行栈执行的代码就是一个宏任务。
在执行宏任务遇到 promise 等,会创建微任务(.then() 里面的回调),并加入到微任务队列队尾。
microtask 必然是在某个宏任务执行的时候创建的,而在下一个宏任务开始之前,浏览器会对页面重新渲染(task
>> 渲染
>> 下一个task
(从任务队列中取一个))。同时,在宏任务执行完成后,页面渲染之前,会执行当前微任务队列中的所有微任务。
也就是说,在某一个macrotask执行完后,在重新渲染与开始下一个宏任务之前,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)。
执行机制:
-
执行一个宏任务(栈中没有就从事件队列中获取)
-
执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
-
宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
-
当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
-
渲染完毕后,JS引擎线程继续,开始下一个宏任务(从宏任务队列中获取)
setTimeout(() => {
console.log('setTimeout');
}, 0);
const promise = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve();
}, 0);
});
console.log(77777);
const promise1 = new Promise(function (resolve, reject) {
resolve();
});
console.log(88888);
setTimeout(() => {
console.log('setTimeout1');
}, 0);
promise.then(function () {
console.log('promise');
});
promise1.then(function () {
console.log(666);
setTimeout(() => {
console.log('promise1');
}, 0);
});
setTimeout(() => {
console.log('setTimeout2');
}, 0);
// 77777
// 88888
// 666
// setTimeout
// promise
// setTimeout1
// setTimeout2
// promise1
总结
-
JavaScript 是单线程语言,决定于它的设计最初是用来处理浏览器网页的交互。浏览器负责解释和执行 JavaScript 的线程只有一个(所有说是单线程),即JS引擎线程,但是浏览器同样提供其他线程,如:事件触发线程、定时器触发线程等。
-
异步一般是指:
- 网络请求
- 定时器
- DOM事件监听
-
事件循环机制:
- JS 引擎线程会维护一个执行栈,同步代码会依次加入到执行栈中依次执行并出栈。
- JS 引擎线程遇到异步函数,会将异步函数交给相应的Webapi,而继续执行后面的任务。
- Webapi会在条件满足的时候,将异步对应的回调加入到消息队列中,等待执行。
- 执行栈为空时,JS引擎线程会去取消息队列中的回调函数(如果有的话),并加入到执行栈中执行。
- 完成后出栈,执行栈再次为空,重复上面的操作,这就是事件循环(event loop)机制。
自己梳理,增加印象,增加理解!
加上自己无聊看到一段代码
setTimeout(() => {
console.log('aaa');
new Promise(function (resolve, reject) {
console.log('bbb');
setTimeout(() => {
console.log('ccc');
}, 0);
resolve();
}).then(function () {
console.log('ddd')
})
console.log('kkk');
}, 0);
console.log('eee');
setTimeout(() => {
console.log('fff');
}, 0);
new Promise(function (resolve, reject) {
console.log('ggg');
resolve();
}).then(function () {
console.log('hhh')
}).catch(function () {
console.log('iii')
})
console.log('jjj');
/*
eee
ggg
jjj
hhh
aaa
bbb
kkk
ddd
fff
ccc
*/