JS运行机制宏任务,微任务

因为JavaScript是一门单线程语言,所以我们可以得出结论:

  • JavaScript是按照语句出现的顺序执行的
let a = '1';
console.log(a);

let b = '2';
console.log(b);

然而实际上js是这样的:

setTimeout(function () {
    console.log('定时器开始啦!')
})

new Promise(function (resolve) {
    console.log('for循环开始啦!')
    for (var i = 0; i < 10000; i ++) {
        i == 99 && resolve()
    }
}).then(function () {
    console.log('执行then函数')
})

console.log('代码执行结束')

// chrome 验证下顺序
// for循环开始啦!
// 代码执行结束
// 执行then函数
// 定时器开始啦!
1. 关于JavaScript

JavaScript是一门单线程语言,在html5中提出来web-worker,但javascript是单线程这一核心未改变,所以一切JavaScript的多线程都是单线程模拟出来的

2.JavaScript事件循环

分为两类:

  • 同步任务
  • 异步任务


    15fdd88994142347.jpg

图片用文字来表达的话:

  • 同步和异步任务分别进入不同的执行场所,同步任务进入主线程,异步任务进入Event Table并注册函数。
  • 当指定的事情完成后,Event Table 会将这个函数移入Event Queue。
  • 主线程内的任务执行完,会去Event Queue读取对应得函数,进入主线程执行。
  • 上述的过程会不断重复,也就是常说的Event Loop(事件循环)

那么怎么知道主线程执行栈为空?js引擎在monitoring process进程,会持续不断的和检查主线程执行栈是否为空,一旦为空,回去Event Queue检查是否有等待被调用的函数。

上一段代码:

let data = []

$.ajax({
    url: 'www.js.com',
    data: data,
    success: function () {
        console.log('success')
    }
})

console.log('代码执行结束')
  • ajax进入Event Table,注册回调函数success。
  • 执行console.log('代码执行结束')
  • ajax事件完成,回调函数进入Event Queue。
  • 主线程完成所有任务开始读取Event Queue的回调函数success并执行
3.setTimeout

大名鼎鼎的setTimeout无需再多言,大家对他的第一印象就是异步可以延时执行,我们经常这么实现延时3秒执行:

setTimeout(() => {
    console.log('延时3秒');
},3000)

渐渐的setTimeout用的地方多了,问题也出现了,有时候明明写的延时3秒,实际却5,6秒才执行函数,这又咋回事啊?

先看一个例子:

setTimeout(() => {
    task();
},3000)
console.log('执行console');

根据前面我们的结论,setTimeout是异步的,应该先执行console.log这个同步任务,所以我们的结论是:

//执行console
//task()

去验证一下,结果正确!
然后我们修改一下前面的代码:

setTimeout(() => {
    task()
},3000)

sleep(10000000)

乍一看其实差不多嘛,但我们把这段代码在chrome执行一下,却发现控制台执行task()需要的时间远远超过3秒,说好的延时三秒,为啥现在需要这么长时间啊?
这时候我们需要重新理解setTimeout的定义。我们先说上述代码是怎么执行的

  • task()进入Event Table 并注册,计时开始。
  • 执行sleep函数,很慢,计时仍在继续。
  • 3秒到了,计时事件timeout完成,task()进入Event Queue,但是sleep还没有执行完成。
  • sleep执行完成了,task()终于从Event Queue进入主线程开始执行。
    上述的流程走完,我们知道setTimeout这个函数,是经过指定时间后,把要执行的任务(本例中为task())加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于3秒。
    关于setTimeout要补充的是,即便主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒。有兴趣的同学可以自行了解。
4.setInterval

上面说完了setTimeout,当然不能错过它的孪生兄弟setInterval。他俩差不多,只不过后者是循环的执行。对于执行顺序来说,setInterval会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。
唯一需要注意的一点是,对于setInterval(fn,ms)来说,我们已经知道不是每过ms秒会执行一次fn,而是每过ms秒,会有fn进入Event Queue。一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。这句话请读者仔细品味。

5.Promise与process.nextTick(callback)

Promise的定义和功能本文不再赘述,不了解的读者可以学习一下阮一峰老师的Promise。而process.nextTick(callback)类似node.js版的"setTimeout",在事件循环的下一次循环中调用 callback 回调函数。

进入正题:除了广义上的同步任务和异步任务,我们对任务有更精细的定义

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

不同的任务类型会进入对应得Event Queue,比如setTimeout和setInterval会进入相同的Event Queue。

事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。听起来有点绕,我们用文章最开始的一段代码说明:

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

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

console.log('console');

  • 这段代码作为宏任务,进入主线程。
  • 先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。
  • 接下来遇到了Promise,new Promise立即执行,then函数分发到微任务Event Queue。
  • 遇到console.log,立即执行
  • 整体代码的script作为第一个宏任务执行结束,接下来查看微任务,发现then在微任务Event Queue中,执行。
  • 第一轮宏任务和微任务执行结束,开始第二轮任务,在Event Queue中发现了setTimeout对应得回调函数,执行。
  • 结束。

参考作者原文

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

友情链接更多精彩内容