调用堆栈(4)--单线程js怎么做到异步

通过前几篇大致了解了js执行的一些机制,包括解析,执行,内存的分配等.
众所周知js是单线程,所以只有一个执行栈,那么一些异步操作,dom操作是怎么实现的?
V8引擎简单看包括内存分配和执行栈,可以看出,异步或dom操作等不归v8管.实际上浏览器有各个单独的线程处理异步(ajax,setTimout),dom事件等.
js执行和页面渲染是互斥的,所以事情都由v8处理会造成卡顿,浏览器多线程和js执行栈完美避免了这种情况.
异步经过其他线程处理结束后,都会返回cb回调函数,js拿到cb,入栈执行即可,称为runtime.

如上图所示,js执行遇到异步,会交给webapi处理,其回调会放在callback queue(回调队列),通过event loop(事件循环),将队列中的callback再次入栈(call stack)执行.


Event loop机制

Event loop实际上就是一个job,用来检测call stack和callback queue,一旦call stack空闲,就将queue中cb入栈执行.
callback queue中任务遵循先进先出

1
2
3
4
5
console.log('start')
setTimeout(function(){console.log('time')},0)
console.log('end')

// start end time

首先,在执行栈中依次执行,start,setTimeout,到异步时交给settimeout线程处理,不会阻塞执行,继续end,此时执行栈空,event loop检测到,将callback queue中cb入栈.

宏任务(macrotask)&微任务(microtasks)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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');

/*
script start
script end
promise1
promise2
setTimeout
*/

同是异步,为什么promise先于settimeout?

  • 宏任务: setInterval setTimeout script setImmediate I/O UI rendering
  • 微任务: promise process.netTick Object.observe MutationObserver

微任务优先级高于宏任务
一个 Event Loop 中,可以有一个或者多个任务队列(task queue),一个任务队列便是一系列有序任务(task)的集合;每个任务都有一个任务源(task source),源自同一个任务源的 task 必须放到同一个任务队列,从不同源来的则被添加到不同队列。 setTimeout/Promise 等 API 便是任务源,而进入任务队列的是他们指定的具体执行任务。

Callback Queue(Task Queue)里的回调事件称为宏任务(macrotask)
微任务(microtasks)是指异步事件结束后,回调函数不会放到 Callback Queue,而是放到一个微任务队列里(Microtasks Queue),在 Call Stack 为空时,Event Loop 会先查看微任务队列里是否有任务,如果有就会先执行微任务队列里的回调事件;如果没有微任务,才会到 Callback Queue 执行回到事件。


整个 Event Loop 的执行顺序如下:

  • 执行一个宏任务(script开始执行是第一个宏任务,栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前微任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取,也就是 callbacke queue)
  • 一个宏任务及其产生的微任务执行完,GUI渲染,视为一个循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
console.log('script start');

setTimeout(function () {
console.log('setTimeout');
}, 0);

new Promise(resolve => {
console.log('Promise');
resolve();
}).then(function () {
setTimeout(function () {
console.log('setTimeout in promise1');
}, 0);
console.log('promise1');
}).then(function () {
console.log('promise2');
});

console.log('script end');

/**
script start
Promise
script end
promise1
promise2
setTimeout
setTimeout in promise1
**/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
async function async1() {
console.log('async1 start');
await async2();
//更改如下:
setTimeout(function() {
console.log('setTimeout1')
},0)
}
async function async2() {
//更改如下:
setTimeout(function() {
console.log('setTimeout2')
},0)
}
console.log('script start');

setTimeout(function() {
console.log('setTimeout3');
}, 0)
async1();

new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');

/**
script start
async1 start
promise1
script end
promise2
setTimeout3
setTimeout2
setTimeout1
**/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
async function a1 () {
console.log('a1 start')
await a2()
console.log('a1 end')
}
async function a2 () {
console.log('a2')
}

console.log('script start')

setTimeout(() => {
console.log('setTimeout')
}, 0)

Promise.resolve().then(() => {
console.log('promise1')
})

a1()

let promise2 = new Promise((resolve) => {
resolve('promise2.then')
console.log('promise2')
})

promise2.then((res) => {
console.log(res)
Promise.resolve().then(() => {
console.log('promise3')
})
})
console.log('script end')


/**
script start
a1 start
a2
promise2
script end
promise1
a1 end
promise2.then
promise3
setTimeout
**/