JavaScript 执行机制(★)

Posted by violetks on April 5, 2023

一、JavaScript 是一门单线程语言,是按照语句出现的顺序执行。

二、JavaScript 事件循环分为:同步任务异步任务

  • 任务进入执行栈之后,同步任务和异步任务分别进入不同的执行场所,同步的进入主线程,异步的进入Event Table并注册函数。
  • 当指定的事情完成时,Event Table会将这个函数移入Event Queue
  • 主线程内的任务执行完毕为空之后,会去Event Queue中读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,也就是常说的Event Loop 事件循环
let data = [];
$.ajax({
    url: www.javascript.com,
    data: data,
    success: () => {
      console.log('发送成功!');
    }
})
console.log('代码执行结束');

上面是一段简易的 Ajax 请求代码:
1、Ajax 进入 Event Table,注册回调函数 success。
2、执行 console.log(‘代码执行结束’)。
3、Ajax 事件完成,回调函数 success 进入 Event Queue。
4、主线程从 Event Queue 读取回调函数 success 并执行。

三、setTimeout 异步延时执行

setTimeout(() => {
  task();
}, 3000)
console.log('执行console');
// 先执行 console.log 这个同步任务,再执行 task()
setTimeout(() => {
  task()
}, 3000)
sleep(10000000)
// 控制台执行 task() 需要的时间远远超过 3 秒

上面代码执行过程:
1、task()进入 Event Table 并注册,计时开始。
2、执行sleep函数,很慢,非常慢,计时仍在继续。
3、3 秒到了,计时事件setTimeout完成,task()进入 Event Queue,但是sleep还没执行完,只好等着。
4、sleep终于执行完了,task()终于从 Event Queue 进入了主线程执行。
总结:setTimeout这个函数,是经过指定时间后,把要执行的任务task()加入到 Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于 3 秒。
setTimeout(fn,0)指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。

// 代码 1
console.log('先执行这里');
setTimeout(() => {
  console.log('执行啦')
}, 0);
// 输出结果:
// 先执行这里
// 执行啦

// 代码 2
console.log('先执行这里');
setTimeout(() => {
  console.log('执行啦')
}, 3000);
// 输出结果:
// 先执行这里
// ... 3s later
// 执行啦

四、setInterval

setInterval会每隔指定的时间将注册的函数置入 Event Queue,如果前面的任务耗时太久,那么同样需要等待。
唯一需要注意的一点是,对于setInterval(fn,ms)来说,我们已经知道不是每过ms秒会执行一次fn,而是每过ms秒,会有fn进入 Event Queue。一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。

五、除了广义的同步任务和异步任务,对任务还有更精细的定义。

  • macro-task(宏任务):包括整体代码 script,setTimeout,setInterval
  • micro-task(微任务):Promise,process.nextTick

不同类型的任务会进入对应的 Event Queue,比如setTimeoutsetInterval会进入相同的 Event Queue。
事件循环的顺序,决定 JS 代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。

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

new Promise(function(resolve) {
    console.log('promise');
}).then(function() {
    console.log('then');
})

console.log('console');

// 输出:promise console setTimeout

这段代码作为宏任务,进入主线程。
1、先遇到setTimeout,那么将其回调函数注册后分发到宏任务 Event Queue。(注册过程与上同,下文不再描述)
2、接下来遇到了Promisenew Promise立即执行,then函数分发到微任务 Event Queue。
3、遇到console.log(),立即执行。
4、整体代码 script 作为第一个宏任务执行结束,看看有哪些微任务?我们发现了then在微任务 Event Queue 里面,执行。
5、第一轮事件循环结束,开始第二轮循环,当然要从宏任务 Event Queue 开始。我们发现了宏任务 Event Queue 中setTimeout对应的回调函数,立即执行。
6、结束。

JS执行机制.png

console.log('1');

setTimeout(function() {
  console.log('2');
  process.nextTick(function() {
    console.log('3');
  })
  new Promise(function(resolve) {
    console.log('4');
    resolve();
  }).then(function() {
    console.log('5')
  })
})
process.nextTick(function() {
  console.log('6');
})
new Promise(function(resolve) {
  console.log('7');
  resolve();
}).then(function() {
  console.log('8')
})

setTimeout(function() {
  console.log('9');
  process.nextTick(function() {
    console.log('10');
  })
  new Promise(function(resolve) {
    console.log('11');
    resolve();
  }).then(function() {
    console.log('12')
  })
})

第一轮事件循环流程分析如下:

  • 整体 script 作为第一个宏任务进入主线程,遇到console.log,输出 1。
  • 遇到setTimeout,其回调函数被分发到宏任务 Event Queue 中。我们暂且记为setTimeout1
  • 遇到process.nextTick(),其回调函数被分发到微任务 Event Queue 中。我们记为process1
  • 遇到Promisenew Promise直接执行,输出 7。then被分发到微任务 Event Queue 中。我们记为then1
  • 又遇到了setTimeout,其回调函数被分发到宏任务 Event Queue 中,我们记为setTimeout2
宏任务 Event Queue 微任务 Event Queue
setTimeout1 process1
setTimeout2 then1
  • 上表是第一轮事件循环宏任务结束时各 Event Queue 的情况,此时已经输出了 1 和 7。
  • 我们发现了process1then1两个微任务。
  • 执行process1,输出 6。
  • 执行then1,输出 8。

第一轮事件循环正式结束,这一轮的结果是输出 1,7,6,8。那么第二轮时间循环从setTimeout1宏任务开始:

  • 首先输出 2。接下来遇到了process.nextTick(),同样将其分发到微任务 Event Queue 中,记为process2new Promise立即执行输出 4,then也分发到微任务 Event Queue 中,记为then2
宏任务 Event Queue 微任务 Event Queue
setTimeout2 process2
  then2
  • 第二轮事件循环宏任务结束,我们发现有process2then2两个微任务可以执行。
  • 输出 3。
  • 输出 5。
  • 第二轮事件循环结束,第二轮输出 2,4,3,5。
  • 第三轮事件循环开始,此时只剩setTimeout2了,执行。
  • 直接输出 9。
  • process.nextTick()分发到微任务 Event Queue 中。记为process3
  • 直接执行new Promise,输出 11。
  • then分发到微任务 Event Queue 中,记为then3
宏任务 Event Queue 微任务 Event Queue
  process3
  then3
  • 第三轮事件循环宏任务执行结束,执行两个微任务process3then3
  • 输出 10。
  • 输出 12。
  • 第三轮事件循环结束,第三轮输出 9,11,10,12。

整段代码,共进行了三次事件循环,完整的输出为 1,7,6,8,2,4,3,5,9,11,10,12。