js single thread and event-loop

Pros and Cons for single threaded javasript

由浏览器决定,JavaScript主要用途是操作DOM,决定了它只能是单线程,否则会带来复杂的同步问题。

Pros:

  1. 适合高并发
  2. 适合I/O密集型应用

Cons:

  1. 不适合CPU密集型应用。如果有长时间运行的计算(大循环),将会导致CPU时间片不能释放,使得后续I/O无法发起。

大纲:

  1. 基本知识点,宏任务、微任务…
  2. 事件循环机制过程

浏览器中的事件循环

JavaScript代码执行过程中,除了依靠函数调用栈来搞定函数的执行顺序,还依靠任务队列(task queue)来搞定另外一些代码的执行。整个执行过程,成为事件循环过程。一个线程中,事件循环是唯一的,但任务队列可以拥有多个。任务队列又分为macro-task和micro-task,在最新标准中称为task和jobs。

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

event-loop

macro-task大概包括:

  • script(整体代码)
  • setTimeOut
  • setInterval
  • setImmediate
  • I/O
  • UI render
  • DOM

micro-task大概包括:

  • process.nextTick
  • Promise
  • Async/Await(Promise)
  • MutationObserver(h5新特性)

几个例子

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
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 -> async1 end -> promise1 -> promise2 -> setTimeout

  1. 首先全局匿名函数最先执行,输出”script start”
  2. 然后遇到async1()函数,调用,将该函数压入执行栈,接着执行 “await async2()”。这里await关键字的作用就是await下面的代码只有当await后面的promise返回结果后才可以执行。而await async2() 语句就像执行普通函数一样执行async2(),进入async2输出”async2 end”。
  3. await关键字下面的语句相当于.then(),加入micro-task队列,那么async1函数执行结束,弹出执行栈。
  4. 遇到setTimeout(),加入macro-task队列。
  5. 接着执行Promise中的executor函数,然后.then()被加入micro-task队列。
  6. 然后执行最后的输出,当前宏任务结束。
  7. 此时事件循环机制开始工作:然后从micro-task队列中依次执行微任务。

而当await函数后面跟的是一个异步函数的调用(这里我们强调返回值为异步才为异步函数):

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
console.log('script start')

async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
return Promise.resolve().then(()=>{
console.log('async2 end1')
})
}
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')

在最新的chrome v8引擎中执行为:

script start
async2 end
Promise
script end
async2 end1
promise1
promise2
async1 end
setTimeout

在这里我们理解为,进入await标记的代码后,将Promise后的首个链式调用注册为micro-task,然后继续执行,直到当前宏任务完成后,微任务也完成后,最后执行await后面的代码,最后再调用其它宏任务。

一个典型的async await的使用为:

1
2
3
4
async function f() {
await p
console.log('ok')
}

我们可以将其简化理解为promise:

1
2
3
4
5
function f() {
return RESOLVE(p).then(() => {
console.log('ok')
})
}

『RESOLVE(p)』接近于『Promise.resolve(p)』,不过有微妙而重要的区别:p 如果本身已经是 Promise 实例,Promise.resolve 会直接返回 p 而不是产生一个新 promise。

这里我们复习一下Ajax原生请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function search(term, onload, onerror) {
var xhr, results, ur
l;
url = `http://example.com/search?q=${term}`;

xhr = new XMLHttpRequest();
xhr.open('GET', url, true);

xhr.onload = function(e) {
if(this.status===200) {
results = JSON.parse(this.responseText);
onload(results);
}
};
xhr.onerror = function(e) {
onerror(e);
};

xhr.send();
}

search('Hello World', console.log, console.error);

如果使用Promise对象,可以写成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function search(term) {
var url =`http://example.com/search?q=${term}`;
var xhr = new XMLHttpRequest();
var result;
xhr.open('GET', url, true);
var p = new Promise(function(resolve, reject){
xhr.open('GET', url, true); //async or not
xhr.onload = function(e) {
if(this.status===200) {
result = JSON.parse(this.responseText);
onload(results);
}
}
xhr.onerror = function(e) {
reject(e);
}
xhr.send();
});
return p;
}

search("Hello World").then(console.log, console.error)

Comments