添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
  • 执行顺序:同步->微任务->宏任务
  • 先说结论,说完结论不难发现,我们主要区分什么是宏任务、微任务即可!不必现在就明白这个结论的意思,先有个印象,然后再读下文,反复读三遍全文,应该就了解大概意思了。

        可以把主进程看成一个宏任务
        依次执行:
        宏任务1 -> 宏任务1内的[ 微任务1、微任务2、微任务3... ] -> 页面渲染
        宏任务2 -> 宏任务2内的[ 微任务1、微任务2、微任务3... ] -> 页面渲染
        宏任务3 -> 宏任务3内的[ 微任务1、微任务2、微任务3... ] -> 页面渲染
        一直到所以宏任务、微任务都执行完,主进程运行结束
    

    下面是将两篇文章整合到一起的,看完几乎就了解执行顺序了。有点长,慢慢看。

    关于javascript

    javascript是一门单线程语言,在最新的HTML5中提出了Web-Worker,但javascript是单线程这一核心仍未改变。所以一切javascript版的"多线程"都是用单线程模拟出来的,一切javascript多线程都是纸老虎!

    JavaScript事件循环

    既然js是单线程,那就像只有一个窗口的银行,客户需要排队一个一个办理业务,同理js任务也要一个一个顺序执行。如果一个任务耗时过长,那么后一个任务也必须等着。那么问题来了,假如我们想浏览新闻,但是新闻包含的超清图片加载很慢,难道我们的网页要一直卡着直到图片完全显示出来?因此聪明的程序员将任务分为两类:

    当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。关于这部分有严格的文字定义,但本文的目的是用最小的学习成本彻底弄懂执行机制,所以我们用导图来说明:

    导图要表达的内容用文字来表述的话:

  • 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
  • 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
  • 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,也就是常说的Event Loop(事件循环)。
  • 我们不禁要问了,那怎么知道主线程执行栈为空啊?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。

    let data = [];
    $.ajax({
        url:www.javascript.com,
        data:data,
        success:() => {
            console.log('发送成功!');
    console.log('代码执行结束');
    

    上面是一段简易的ajax请求代码:

  • ajax进入Event Table,注册回调函数success
  • 执行console.log('代码执行结束')
  • ajax事件完成,回调函数success进入Event Queue。
  • 主线程从Event Queue读取回调函数success并执行。
  • 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(fn,0)这样的代码,0秒后执行又是什么意思呢?是不是可以立即执行呢?
    答案是不会的,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。举例说明:

    //代码1
    console.log('先执行这里');
    setTimeout(() => {
        console.log('执行啦')
    },0);
    
    //代码2
    console.log('先执行这里');
    setTimeout(() => {
        console.log('执行啦')
    },3000);
    

    代码1的输出结果是:

    //先执行这里
    //执行啦
    

    代码2的输出结果是:

    //先执行这里
    // ... 3s later
    // 执行啦
    

    关于setTimeout要补充的是,即便主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒。有兴趣的同学可以自行了解。

    setInterval

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

    Promise与process.nextTick(callback)

    传统的定时器我们已经研究过了,接着我们探究Promiseprocess.nextTick(callback)的表现。
    Promise的定义和功能本文不再赘述,不了解的读者可以学习一下阮一峰老师的Promise。而process.nextTick(callback)类似node.js版的"setTimeout",在事件循环的下一次循环中调用 callback 回调函数。

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

  • 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');
    
  • 这段代码作为宏任务,进入主线程。
  • 先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。(注册过程与上同,下文不再描述)
  • 接下来遇到了Promisenew Promise立即执行,then函数分发到微任务Event Queue。
  • 遇到console.log(),立即执行。
  • 好啦,整体代码script作为第一个宏任务执行结束,看看有哪些微任务?我们发现了then在微任务Event Queue里面,执行。
  • ok,第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。
  • 事件循环,宏任务,微任务的关系如图所示:

    我们来分析一段较复杂的代码,看看你是否真的掌握了js的执行机制:

    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
  • 第三轮事件循环开始,此时只剩setTimeout2了,执行。
  • 直接输出9。
  • process.nextTick()分发到微任务Event Queue中。记为process3
  • 直接执行new Promise,输出11。
  • then分发到微任务Event Queue中,记为then3
  • 第三轮事件循环结束,第三轮输出9,11,10,12。
  • 整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)

    await
    Promise中的异步体现在then和catch中,其实就可以吧await后面的代码当作是在then中,await后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码。

        async function async1(){
            console.log("async1 start")
            await async2()
            console.log("async1 end")
            await async2()
            console.log("async1 aaa end")
            await async2()
            console.log("async1 bbb end")
        async function async2(){
            console.log("async2")
        console.log("script start")
        setTimeout(function(){
            console.log("setTimeout")
        async1()
        new Promise(function(reslove){
            console.log("promise1")
            reslove()
        }).then(function(){
            console.log("promise2")
        }).then(function(){
            console.log("promise3")
        }).then(function(){
            console.log("promise4")
        console.log("script end")
    

    执行结果:

    script start
    async1 start
    async2
    promise1
    script end
    async1 end
    async2
    promise2
    async1 aaa end
    async2
    promise3
    async1 bbb end
    promise4
    setTimeout
    

    (1)js的异步

    我们从最开头就说javascript是一门单线程语言,不管是什么新框架新语法糖实现的所谓异步,其实都是用同步的方法去模拟的,牢牢把握住单线程这点非常重要。

    (2)事件循环Event Loop

    事件循环是js实现异步的一种方法,也是js的执行机制。

    (3)javascript的执行和运行

    执行和运行有很大的区别,javascript在不同的环境下,比如node,浏览器,Ringo等等,执行方式是不同的。而运行大多指javascript解析引擎,是统一的。

    (4)setImmediate

    微任务和宏任务还有很多种类,比如setImmediate等等,执行都是有共同点的,有兴趣的同学可以自行了解。

    (5)最后的最后

  • javascript是一门单线程语言
  • Event Loop是javascript的执行机制
  • js中的宏任务与微任务

    JavaScript 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,防止主线程的不阻塞,Event Loop 的方案应用而生。Event Loop 包含两类:一类是基于 Browsing Context,一种是基于 Worker。二者的运行是独立的,也就是说,每一个 JavaScript 运行的"线程环境"都有一个独立的 Event Loop,每一个 Web Worker 也有一个独立的 Event Loop。

    本文所涉及到的事件循环是基于 Browsing Context。

    根据规范,事件循环是通过任务队列的机制来进行协调的。一个 Event Loop 中,可以有一个或者多个任务队列(task queue),一个任务队列便是一系列有序任务(task)的集合;每个任务都有一个任务源(task source),源自同一个任务源的 task 必须放到同一个任务队列,从不同源来的则被添加到不同队列。setTimeout/Promise 等API便是任务源,而进入任务队列的是他们指定的具体执行任务。

    在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:

  • 在此次 tick 中选择最先进入队列的任务(oldest task),如果有则执行(一次)
  • 检查是否存在 Microtasks,如果存在则不停地执行,直至清空 Microtasks Queue
  • 更新 render
  • 主线程重复执行上述步骤
  • 在上诉tick的基础上需要了解几点:

  • JS分为同步任务和异步任务
  • 同步任务都在主线程上执行,形成一个执行栈
  • 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  • 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
  • (macro)task,可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。

    浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程如下:

    (macro)task->渲染->(macro)task->...
    

    宏任务包含:

  • script(整体代码)
  • setTimeout
  • setInterval
  • UI交互事件
  • postMessage
  • MessageChannel
  • setImmediate(Node.js 环境)
  • microtask,可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。

    所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染。也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)。

    微任务包含:

  • Promise.then
  • Object.observe
  • MutationObserver
  • process.nextTick(Node.js 环境)
  • 在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
  • 这一次,彻底弄懂 JavaScript 执行机制
    js中的宏任务与微任务
    一次弄懂Event Loop(彻底解决此类面试问题)