精华内容
下载资源
问答
  • 其中涉及到事件循环(event loop),宏任务(macrotask),微任务(microtask) 一、事件循环 Event Loop 程序中设置两个线程:一个负责程序本身的运行,称为”主线程”;另一个负责主线程与其他进程(主要是各种I/O...
  • 本文根据 JavaScript 规范入手,阐述了JS执行过程在考虑时效性和效率权衡中的演变,并通过从JS代码运行的基础机制事件队列入手,分析了JS不同任务类型(宏任务、微任务)的差别,通过这些差别给出了详细分析不同任务...

    本文首发于 vivo互联网技术 微信公众号
    链接:https://mp.weixin.qq.com/s/Dm3PrsBy4wPAWnwFgMyZcQ
    作者:Ivan

    本文根据 JavaScript 规范入手,阐述了JS执行过程在考虑时效性和效率权衡中的演变,并通过从JS代码运行的基础机制事件队列入手,分析了JS不同任务类型(宏任务、微任务)的差别,通过这些差别给出了详细分析不同任务嵌套的复杂 JS 代码执行的分析流程。

    一、事件队列与回调

    在使用JavaScript编程时,需要用到大量的回调编程。回调,单纯的理解,其实就是一种设置的状态通知,当某个语句模块执行完后,进行一个通知到对应方法执行的动作。

    最常见的setTimeout等定时器,AJAX请求等。这是由于JavaScript单线程设计导致的,作为脚本语言,在运行的时候,语言设计人员需要考虑的两件重要的事情,就是执行的实时性和效率。

    实时性,就是指在代码执行过程中,代码执行的实效性,当前执行语句任务是否在当前的实效下发挥作用。效率,在这里指的是代码执行过程中,每个语句执行的造成后续执行的延迟率。

    由于JavaScript单线程特性,想要在完成复杂的逻辑执行情况下而不阻塞后续执行,也就是保证效率,回调看似是不可避免的选择。

    早期浏览器的实现和现在可能有许多不同,但是并不会影响我们用其来理解回调过程。

    早期浏览器设计时,比如IE6,一般都让页面内相关内容,比如渲染、事件监听、网络请求、文件处理等,都运行于一个单独的线程。此时要引入JavaScript控制文件,那JavaScript也会运行在于页面相同的线程上。

    当触发某个事件时,有单线程线性执行,这时不仅仅可能是线程中正在执行其他任务,使得当前事件不能立即执行,更可能是考虑到直接执行当前事件导致的线程阻塞影响执行效率的原因。这时事件触发的执行流程,比如函数等,将会进入回调的处理过程,而为了实现不同回调的实现,浏览器提供了一个消息队列。

    当主线上下文内容都程执行完成后,会将消息队列中的回调逻辑一一取出,将其执行。这就是一个最简单的事件机制模型。

    376b632a7c8e594ef620a95235624f83.png

    浏览器的事件回调,其实就是一种异步的回调机制。常见的异步过程有两种典型代表。一种是setTimeout定时器作为代表的,触发后直接进入事件队列等待执行;一种是XMLHTTPRequest代表的,触发后需要调用去另一个线程执行,执行完成后封装返回值进入事件队列等待。在这里并不进行深入讨论。

    由此,我们得到了JavaScript设计的基础线程框架。而宏任务和微任务的差异实现正是为了解决特定问题而在此基础上衍生出来的。而在没有微任务的时代,JavaScript的执行中并没有所谓异步执行的概念,异步执行是在宿主环境中实现的,也就是浏览器提供了。直至实现了微任务,才可以认为JavaScript的代码执行存在了异步过程。

    (由于目前广泛使用的JavaScript引擎是V8,在此我们已V8作为解释对象)

    二、(宏)任务和微任务

    我们常在文章中看到,macroTask(宏任务)和microTask(微任务)的说法。但其实在MDN[链接]中查看的时候,macroTask(宏任务)这一说法对应于microTask(微任务)而言的,而统一区分于microTask其实就是普通的Task任务。在此我们可以粗略的认为普通的Task任务其实都是macroTask。

    任务的定义:

    A task is any JavaScript code which is scheduled to be run by the standard mechanisms such as initially starting to run a program, an event callback being run, or an interval or timeout being fired. These all get scheduled on the task queue.

    (任何按标准机制调度进行执行的JavaScript代码,都是任务,比如执行一段程序、执行一个事件回调或interval/timeout触发,这些都在任务队列上被调度。)

    微任务存在的区别定义:

    First, each time a task exits, the event loop checks to see if the task is returning control to other JavaScript code. If not, it runs all of the microtasks in the microtask queue. The microtask queue is, then, processed multiple times per iteration of the event loop, including after handling events and other callbacks.

    Second, if a microtask adds more microtasks to the queue by calling queueMicrotask(), those newly-added microtasks execute before the next task is run. 

    (当一个任务存在,事件循环都会检查该任务是否正把控制权交给其他 JavaScript 代码。如果不交予执行,事件循环就会运行微任务队列中的所有微任务。接下来微任务循环会在事件循环的每次迭代中被处理多次,包括处理完事件和其他回调之后。其次,如果一个微任务通过调用 queueMicrotask(), 向队列中加入了更多的微任务,则那些新加入的微任务会早于下一个任务运行 。)

    根据定义,可以简单地作出以下理解。

    (宏)任务,其实就是标准JavaScript机制下的常规任务,或者简单的说,就是指消息队列中的等待被主线程执行的事件。在宏任务执行过程中,v8引擎都会建立新栈存储任务,宏任务中执行不同的函数调用,栈随执行变化,当该宏任务执行结束时,会清空当前的栈,接着主线程继续执行下一个宏任务。

    微任务,看定义中与(宏)任务的区别其实比较复杂,但是根据定义就可以知道,其中很重要的一点是,微任务必须是一个异步的执行的任务,这个执行的时间需要在主函数执行之后,也就是微任务建立的函数执行后,而又需要在当前宏任务结束之前。

    由此可以看出,微任务的出现其实就是语言设计中的一种实时性和效率的权衡体现。当宏任务执行时间太久,就会影响到后续任务的执行,而此时因为某些需求,编程人员需要让某些任务在宿主环境(比如浏览器)提供的事件循环下一轮执行前执行完毕,提高实时性,这就是微任务存在的意义。

    常见的创建宏任务的方法有setTimeout定时器,而常见的属于微任务延伸出的技术有Promise、Generator、async/await等。而无论是宏任务还是微任务依赖的都是基础的执行栈和消息队列的机制而运行。根据定义,宏任务和微任务存在于不同的任务队列,而微任务的任务队列应该在宏任务执行栈完成前清空。

    这正是分析和编写类似以下复杂逻辑代码所根据的基本原理,并且做到对事件循环的充分利用。

    三、根据定义得出的分析实例

    function taskOne() {
        console.log('task one ...')
        setTimeout(() => {
            Promise.resolve().then(() => {
                console.log('task one micro in macro ...')
            })
            setTimeout(() => {
                console.log('task one macro ...')
            }, 0)
        }, 0)
        taskTwo()
    }
     
     
    function taskTwo() {
        console.log('task two ...')
        Promise.resolve().then(() => {
            setTimeout(() => {
                console.log('task two macro in micro...')
            }, 0)
        })
     
        setTimeout(() => {
            console.log('task two macro ...')
        }, 0)
    }
     
    setTimeout(() => {
        console.log('running macro ...')
    }, 0)
     
    taskOne()
     
    Promise.resolve().then(() => {
        console.log('running micro ...')
    })

    根据宏任务、微任务定义和调用栈执行以及消息队列就可以分析出console.log的输出顺序,即所代表的执行顺序。

    首先,在执行的第一步,全局上下文进入调用栈,也属于常规任务,可以简单认为此执行也是执行中的一个宏任务。

    在全局上下文中,setTimeout触发设置宏任务,直接进入消息队列,而Promise.resolve().then()中的内容进入当前宏任务执行状态下的微任务队列。taskOne被压入调用栈。当然,因为微任务队列的存放位置,也是申请于环境对象中,可以认为微任务拥有一个单独的队列。

    a53d73b859ad9dcf5c715e088d80f290.png

    此时当前宏任务并没有结束,taskOne函数上下文需要被执行。函数内部的console.log()立即执行,其中的setTimeout触发宏任务,进入消息队列,taskTwo被压入调用栈。

    5e7cf2aa790e19dda7d0cc0154d805b8.png

    此时当前宏任务还没有结束,调用栈中taskTwo需要被执行。函数内部的console.log()立即执行,其中的promise进入微任务的队列,setTimeout进入消息队列。taskTwo出栈执行完毕。

    9a0bf88890f646a3767bb7b308f2433d.png

    此时当前已没有主逻辑执行的代码,而当前宏任务将执行结束,微任务会在当前宏任务完成前执行,所以微任务队列会依次执行,直到微任务队列清空。首先执行running micro,输出打印,然后执行taskTwo中的promise,setTimeout触发宏任务进入消息队列。

     

    此时已经清空微任务队列,当前宏任务结束,主线程会到消息队列进行消费。先执行running macro 宏任务,直接进行打印,没有对应微任务,当前结束,继续执行taskOne setTimeout宏任务,内部执行同理。

    fe9631a11f5b40444ff6be893ea10413.png

    由于微任务队列存在任务,在上一个宏任务taskOne setTimeout执行结束前,需要执行微任务队列中任务。

    aadf5510a59b453e4c3f6a9ebbec50df.png

    接下来所有的宏任务依次执行。得到最终的输出结果。

    4f54c7ec77c2d8e082b2eb6ec2d5e4bc.png

    我们可以在Chrome里面进行验证。看起来并没有问题。

    07edbf367e04aaf15b030e8e0b2a0d6b.png

    四、Nodejs环境中的区别

    这是在浏览器搭载v8引擎的情况下,我们验证了宏任务和微任务的执行机理,那在Nodejs中运行JavaScript代码会有什么不同吗?

    使用命令行直接执行JavaScript脚本文件,得到了以下结果。

    d7f4a1445f05c94f2a29b413701236f9.png

    与浏览器的执行输出结果有所不同。这里的one micro in macro 并没有在一开始执行。这是为什么呢?

    虽然Nodejs的事件循环有不同于浏览器的六个阶段,但是按照定义规范,这里的宏任务和微任务执行,明显没有遵循微任务区分差别的第二点,也就是微任务必须在宏任务执行结束前执行。

    其实这个问题在之前的业务开发中遇到过。由于微任务执行的时序与定义不符,导致数据出现了微小的差异。这里与Nodejs版本迭代中的实现有关。

    通过命令可以看到当前执行的Nodejs版本为10.16.0。

    71fe902dc8a6e71cc32fdb2641cab86b.png

    我们使用nvm切换到更新一些的版本看看执行结果如何。

    d84e5380e53ddc92af6b373bf01310e4.png

    然后再次使用Nodejs执行上述脚本代码。在11版本之上我们得到了和浏览器一致的结果。

    12d0951280e778a90c1ed475fae458a1.png

    从一开始浏览器端就是严格遵循了微任务和宏任务定义进行执行,也就是说,一个宏任务执行完成过程中,就会去检测微任务队列是否有需要执行的任务,即使是微任务嵌套微任务,也会将微任务执行完成,再去执行下一个宏任务。

    而通过查看Nodejs版本日志发现,在Nodejs环境中,在11版本之前,同源的任务放在一起进行执行,也就是宏任务队列和微任务队列只有清空一个后才会执行另一个。

    就算涉及到同源宏任务的嵌套代码,任然会将宏任务一起执行,但是内部的任务则会放到下一个循环中去执行。而在11版本后,Nodejs修改成了与浏览器一样的遵循定义的执行方式。

    对于早于11版本的Nodejs的实现,可能是由于嵌套任务存在的可能性。微任务嵌套微任务可能造成线程中一直处于当前微任务队列执行状态而走不下去,而宏任务的嵌套循环执行,并不会造成内存溢出的问题,因为每个宏任务的执行都是新建的栈。这就是为什么下方的代码会导致栈溢出,而加入setTimeout后就不会报错的原因。

    既然如此,可能开发人员考虑这样情景的时候,不如先把同源任务执行完毕,以免在微任务饿死线程的时候,还有未执行完成的宏任务。然而这不符合规范,也显然不是很合理,这样的操作甚至是失误应该交给JavaScript的开发者。

    function run() {
        run()
    }
    run()
     
     
    function run() {
        setTimeout(run, 0)
    }
    run()

    这也许是早于11版本,Nodejs实现的一个考虑。但是这样并不符合规范,所以我更愿意倾向于相信Nodejs团队在11版本之前的实现存在错误,而在11版本后修复了这个错误。毕竟如果使用同源执行策略,嵌套中的微任务就已经失去了时效性,在宏任务都执行完成后的微任务,与宏任务并没有区别。

    当然了,目前大部分浏览器都倾向于去符合规范的实现方式,但是任然有一些区别。在使用的过程中,如果需要兼容不容的浏览器还是要更了解这些执行过程,以免出现难以察觉和查找的问题。在IE高版本、FireFox和Safari不同版本中,执行会有些不同,有兴趣的可以动手试试,并找出为何不同。

    更多内容敬请关注vivo 互联网技术微信公众号

    注:转载文章请先与微信号:Labs2020联系

    展开全文
  • 微任务可以在实时性和效率之间做一个有效的权衡。 从目前的情况来看,微任务已经被广泛地应用,基于微任务的技术有 MutationObserver、Promise 以及以 Promise 为基础开发出来的很多其他的技术。所以微任务的重要性...

    微任务可以在实时性和效率之间做一个有效的权衡。

    从目前的情况来看,微任务已经被广泛地应用,基于微任务的技术有 MutationObserver、Promise 以及以 Promise 为基础开发出来的很多其他的技术。所以微任务的重要性也与日俱增,了解其底层的工作原理对于你读懂别人的代码,以及写出更高效、更具现代的代码有着决定性的作用。

    有微任务,也就有宏任务,那这二者到底有什么区别?它们又是如何相互取长补短的?


    宏任务

    前面我们已经介绍过了,页面中的大部分任务都是在主线程上执行的,这些任务包括了:

    • 渲染事件(如解析 DOM、计算布局、绘制);
    • 用户交互事件(如鼠标点击、滚动页面、放大缩小等);
    • JavaScript 脚本执行事件;
    • 网络请求完成、文件读写完成事件。

    为了协调这些任务有条不紊地在主线程上执行,页面进程引入了消息队列和事件循环机制,渲染进程内部会维护多个消息队列,比如延迟执行队列和普通的消息队列。然后主线程采用一个 for 循环,不断地从这些任务队列中取出任务并执行任务。我们把这些消息队列中的任务称为宏任务

    消息队列中的任务是通过事件循环系统来执行的,这里我们可以看看在WHATWG 规范中是怎么定义事件循环机制的。

    由于规范需要支持语义上的完备性,所以通常写得都会比较啰嗦,这里我就大致总结了下 WHATWG 规范定义的大致流程:

    • 先从多个消息队列中选出一个最老的任务,这个任务称为 oldestTask;
    • 然后循环系统记录任务开始执行的时间,并把这个 oldestTask 设置为当前正在执行的任务;
    • 当任务执行完成之后,删除当前正在执行的任务,并从对应的消息队列中删除掉这个 oldestTask;
    • 最后统计执行完成的时长等信息。

    以上就是消息队列中宏任务的执行过程,通过前面的学习,相信你也很熟悉这套执行流程了。

    宏任务可以满足我们大部分的日常需求,不过如果有对时间精度要求较高的需求,宏任务就难以胜任了,下面我们就来分析下为什么宏任务难以满足对时间精度要求较高的任务。

    前面我们说过,页面的渲染事件、各种 IO 的完成事件、执行 JavaScript 脚本的事件、用户交互的事件等都随时有可能被添加到消息队列中,而且添加事件是由系统操作的,JavaScript 代码不能准确掌控任务要添加到队列中的位置,控制不了任务在消息队列中的位置,所以很难控制开始执行任务的时间。为了直观理解,你可以看下面这段代码:

    <!DOCTYPE html>
    <html>
        <body>
            <div id='demo'>
                <ol>
                    <li>test</li>
                </ol>
            </div>
        </body>
        <script type="text/javascript">
            function timerCallback2(){
              console.log(2)
            }
            function timerCallback(){
                console.log(1)
                setTimeout(timerCallback2,0)
            }
            setTimeout(timerCallback,0)
        </script>
    </html>
    

    在这段代码中,我的目的是想通过 setTimeout 来设置两个回调任务,并让它们按照前后顺序来执行,中间也不要再插入其他的任务,因为如果这两个任务的中间插入了其他的任务,就很有可能会影响到第二个定时器的执行时间了。

    但实际情况是我们不能控制的,比如在你调用 setTimeout 来设置回调任务的间隙,消息队列中就有可能被插入很多系统级的任务。你可以打开 Performance 工具,来记录下这段任务的执行过程,也可参考文中我记录的图片:
    在这里插入图片描述
    setTimeout 函数触发的回调函数都是宏任务,如图中,左右两个黄色块就是 setTimeout 触发的两个定时器任务。

    现在你可以重点观察上图中间浅红色区域,这里有很多一段一段的任务,这些是被渲染引擎插在两个定时器任务中间的任务。试想一下,如果中间被插入的任务执行时间过久的话,那么就会影响到后面任务的执行了。

    所以说宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合了,比如后面要介绍的监听 DOM 变化的需求。


    微任务

    在理解了宏任务之后,下面我们就可以来看看什么是微任务了。我们介绍过异步回调的概念,其主要有两种方式。

    **第一种是把异步回调函数封装成一个宏任务,添加到消息队列尾部,当循环系统执行到该任务的时候执行回调函数。**这种比较好理解,我们前面介绍的 setTimeout 和 XMLHttpRequest 的回调函数都是通过这种方式来实现的。

    第二种方式的执行时机是在主函数执行结束之后、当前宏任务结束之前执行回调函数,这通常都是以微任务形式体现的。

    那这里说的微任务到底是什么呢?

    微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。

    不过要搞清楚微任务系统是怎么运转起来的,就得站在 V8 引擎的层面来分析下。

    我们知道当 JavaScript 执行一段脚本的时候,V8 会为其创建一个全局执行上下文,在创建全局执行上下文的同时,V8 引擎也会在内部创建一个微任务队列。顾名思义,这个微任务队列就是用来存放微任务的,因为在当前宏任务执行的过程中,有时候会产生多个微任务,这时候就需要使用这个微任务队列来保存这些微任务了。不过这个微任务队列是给 V8 引擎内部使用的,所以你是无法通过 JavaScript 直接访问的。

    也就是说每个宏任务都关联了一个微任务队列。那么接下来,我们就需要分析两个重要的时间点——微任务产生的时机和执行微任务队列的时机。

    我们先来看看微任务是怎么产生的?在现代浏览器里面,产生微任务有两种方式。

    第一种方式是使用 MutationObserver 监控某个 DOM 节点,然后再通过 JavaScript 来修改这个节点,或者为这个节点添加、删除部分子节点,当 DOM 节点发生变化时,就会产生 DOM 变化记录的微任务。

    第二种方式是使用 Promise,当调用 Promise.resolve() 或者 Promise.reject() 的时候,也会产生微任务。

    通过 DOM 节点变化产生的微任务或者使用 Promise 产生的微任务都会被 JavaScript 引擎按照顺序保存到微任务队列中。

    好了,现在微任务队列中有了微任务了,那接下来就要看看微任务队列是何时被执行的。

    通常情况下,在当前宏任务中的 JavaScript 快执行完成时,也就在 JavaScript 引擎准备退出全局执行上下文并清空调用栈的时候,JavaScript 引擎会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务。**WHATWG 把执行微任务的时间点称为检查点。**当然除了在退出全局执行上下文式这个检查点之外,还有其他的检查点,不过不是太重要,这里就不做介绍了。

    如果在执行微任务的过程中,产生了新的微任务,同样会将该微任务添加到微任务队列中,V8 引擎一直循环执行微任务队列中的任务,直到队列为空才算执行结束。也就是说在执行微任务过程中产生的新的微任务并不会推迟到下个宏任务中执行,而是在当前的宏任务中继续执行。

    为了直观地理解什么是微任务,你可以参考下面我画的示意图(由于内容比较多,我将其分为了两张):
    在这里插入图片描述
    在这里插入图片描述
    该示意图是在执行一个 ParseHTML 的宏任务,在执行过程中,遇到了 JavaScript 脚本,那么就暂停解析流程,进入到 JavaScript 的执行环境。从图中可以看到,全局上下文中包含了微任务列表。

    在 JavaScript 脚本的后续执行过程中,分别通过 Promise 和 removeChild 创建了两个微任务,并被添加到微任务列表中。接着 JavaScript 执行结束,准备退出全局执行上下文,这时候就到了检查点了,JavaScript 引擎会检查微任务列表,发现微任务列表中有微任务,那么接下来,依次执行这两个微任务。等微任务队列清空之后,就退出全局执行上下文。

    以上就是微任务的工作流程,从上面分析我们可以得出如下几个结论

    • 微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。
    • 微任务的执行时长会影响到当前宏任务的时长。比如一个宏任务在执行过程中,产生了 100 个微任务,执行每个微任务的时间是 10 毫秒,那么执行这 100 个微任务的时间就是 1000 毫秒,也可以说这 100 个微任务让宏任务的执行时间延长了 1000 毫秒。所以你在写代码的时候一定要注意控制微任务的执行时长。
    • 在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行。

    监听 DOM 变化方法演变

    现在知道了微任务是怎么工作的,那接下来我们再来看看微任务是如何应用在 MutationObserver 中的。MutationObserver 是用来监听 DOM 变化的一套方法,而监听 DOM 变化一直是前端工程师一项非常核心的需求。

    比如许多 Web 应用都利用 HTML 与 JavaScript 构建其自定义控件,与一些内置控件不同,这些控件不是固有的。为了与内置控件一起良好地工作,这些控件必须能够适应内容更改、响应事件和用户交互。因此,Web 应用需要监视 DOM 变化并及时地做出响应。

    虽然监听 DOM 的需求是如此重要,不过早期页面并没有提供对监听的支持,所以那时要观察 DOM 是否变化,唯一能做的就是轮询检测,比如使用 setTimeout 或者 setInterval 来定时检测 DOM 是否有改变。这种方式简单粗暴,但是会遇到两个问题:如果时间间隔设置过长,DOM 变化响应不够及时;反过来如果时间间隔设置过短,又会浪费很多无用的工作量去检查 DOM,会让页面变得低效。

    直到 2000 年的时候引入了 Mutation Event,Mutation Event 采用了观察者的设计模式,当 DOM 有变动时就会立刻触发相应的事件,这种方式属于同步回调。

    采用 Mutation Event 解决了实时性的问题,因为 DOM 一旦发生变化,就会立即调用 JavaScript 接口。但也正是这种实时性造成了严重的性能问题,因为每次 DOM 变动,渲染引擎都会去调用 JavaScript,这样会产生较大的性能开销。比如利用 JavaScript 动态创建或动态修改 50 个节点内容,就会触发 50 次回调,而且每个回调函数都需要一定的执行时间,这里我们假设每次回调的执行时间是 4 毫秒,那么 50 次回调的执行时间就是 200 毫秒,若此时浏览器正在执行一个动画效果,由于 Mutation Event 触发回调事件,就会导致动画的卡顿。

    也正是因为使用 Mutation Event 会导致页面性能问题,所以 Mutation Event 被反对使用,并逐步从 Web 标准事件中删除了。

    为了解决了 Mutation Event 由于同步调用 JavaScript 而造成的性能问题,从 DOM4 开始,推荐使用 MutationObserver 来代替 Mutation Event。MutationObserver API 可以用来监视 DOM 的变化,包括属性的变化、节点的增减、内容的变化等。

    那么相比较 Mutation Event,MutationObserver 到底做了哪些改进呢?

    首先,MutationObserver 将响应函数改成异步调用,可以不用在每次 DOM 变化都触发异步调用,而是等多次 DOM 变化后,一次触发异步调用,并且还会使用一个数据结构来记录这期间所有的 DOM 变化。这样即使频繁地操纵 DOM,也不会对性能造成太大的影响。

    我们通过异步调用和减少触发次数来缓解了性能问题,那么如何保持消息通知的及时性呢?如果采用 setTimeout 创建宏任务来触发回调的话,那么实时性就会大打折扣,因为上面我们分析过,在两个任务之间,可能会被渲染进程插入其他的事件,从而影响到响应的实时性。

    这时候,微任务就可以上场了,在每次 DOM 节点发生变化的时候,渲染引擎将变化记录封装成微任务,并将微任务添加进当前的微任务队列中。这样当执行到检查点的时候,V8 引擎就会按照顺序执行微任务了。

    综上所述, MutationObserver 采用了“异步 + 微任务”的策略。

    • 通过异步操作解决了同步操作的性能问题;
    • 通过微任务解决了实时性的问题。

    总结

    好了,今天就介绍到这里,下面我来总结下今天的内容。

    首先我们回顾了宏任务,然后在宏任务的基础之上,我们分析了异步回调函数的两种形式,其中最后一种回调的方式就是通过微任务来实现的。

    接下来我们详细分析了浏览器是如何实现微任务的,包括微任务队列、检查点等概念。

    最后我们介绍了监听 DOM 变化技术方案的演化史,从轮询到 Mutation Event 再到最新使用的 MutationObserver。MutationObserver 方案的核心就是采用了微任务机制,有效地权衡了实时性和执行效率的问题。

    注释:本文章借鉴于李兵老师的《浏览器工作原理和实践》课程,希望自己通过总结,更有助于理解,也方便日后再学习;

    展开全文
  • 这时,需要区分两种任务:宏任务与微任务。再说宏任务与微任务之前,我们先说一个概念。javaScript是一门单线程语言,所以一切javaScript版的“多线程”都是的单线程模拟出来的 一、JavaScript中的同步和异步 同步...

    在正常情况先下,JavaScript任务是同步执行的,即执行完一个前一个任务,然后执行后一个任务。也就是跟着主线任务按序执行。
    只有遇到异步任务的情况下,执行顺序才会改变。这时,需要区分两种任务:宏任务与微任务。再说宏任务与微任务之前,我们先说一个概念。javaScript是一门单线程语言,所以一切javaScript版的“多线程”都是的单线程模拟出来的

    一、JavaScript中的同步和异步

    同步和异步其实在我们的理解中上很容易明白,同步就是我按照代码的顺序来执行程序。异步就是遇到异步程序,我直接开一个新线程来执行,在理解上我们可以这样想,但是事实上我们上面也说了,在javaScript中是单线程的,所以是不存在开一个新线程这样说法的,那么异步的到底是怎样实现我们感觉上的新线程的呢?
    我们先看下面这一张导图:
    在这里插入图片描述
    导图要表达的内容用文字来表述的话:

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

    我们不禁要问了,那怎么知道主线程执行栈为空啊?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。

    $.ajax({
    	url:www.baidu.com,
    	data:data,
    	success:()=>{
    		console.log("发送成功!");
    	}
    })
    console.log("代码执行结束")

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

    • ajax进入Event Table,注册回调函数success。
    • 执行console.log(‘代码执行结束’)。
    • ajax事件完成,回调函数success进入Event Queue。
    • 主线程从Event Queue读取回调函数success并执行。console.log(“发送成功!”)

    二、setTimeout / setInterval

    关于setTimeout

    我们都知道setTimeout是异步的,在经过指定时间后才会执行 里面的回调函数。以下面为例

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

    我们常规的对setTime的认识就是,里面的回调函数task只有经过了3秒之后才会执行。所以最后的结果就是先输出"执行console”再执行task()函数。
    好,我们就按现在的这个想法,来看看下面这个例子:

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

    这时候你还会认为task回调函数是在3秒之后执行吗,显然是不是的。这时候我们就需要重新理解一下setTimeout的定义。
    我们用上面的导图来解释这个情况:

    1. task()进入Event Table并注册,计时开始。
    2. 执行sleep函数,很慢,非常慢,计时仍在继续。
    3. 3秒到了,计时事件timeout完成,task()进入Event Queue,但是sleep也太慢了吧,还没执行完,只好等着。
    4. sleep终于执行完了,task()终于从Event Queue进入了主线程执行。

    上述的流程走完,我们知道setTimeout这个函数,是经过指定时间后,把要执行的任务(本例中为task())加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于3秒。

    我们还经常遇到setTimeout(fn,0)这样的代码,0秒后执行又是什么意思呢?是不是可以立即执行呢?
    答案是不会的,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。举例说明:

    setTimeout(() => {
        console.log('执行啦')
    },0);
    console.log('先执行这里');
    

    这里最后的结果是“先执行这里”,在输出“执行啦”。同样是上面的道理,先执行主线程的任务,再执行Event Queue中的任务。

    关于setTimeout要补充的是,即便主线程为空,0毫秒实际上也是达不到的,根据HTML的标准,最低是4毫秒。

    关于setTimeout

    说完setTimeout我们再聊聊跟它相似的setInterval,setInterval会每隔指定的事件将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。

    唯一需要注意的一点是,对于setInterval(fn,ms)来说,我们已经知道不是每过ms秒会执行一次fn,而是每过ms秒,会有fn进入Event Queue。一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。

    三、宏任务 、微任务

    我们说了同步任务和异步任务只是我们对程序的宏观认识,但是当有多个异步任务的时候它的执行顺序又是怎样的呢。这里我们就必须得提到比同步任务和异步任务更为深刻的概念:宏任务/微任务。

    宏任务:包括整体代码script,setTimeout,setInterval
    微任务:包括Promise,process.nextTick,async await

    代码再执行的时候,会从主线程开始,当遇到宏任务,会将该任务放入宏任务Event Queue,如遇到微任务,就会把任务放入微任务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里面,执行。
    
     - ok,第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。
    
     - 结束。
    
    **例子:**我们来分析一下下面这段的代码
    
    ```javascript
    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。
    • 遇到Promise,new Promise直接执行,输出7。then被分发到微任务Event Queue中。我们记为then1。
    • 又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2。
    宏任务Event Queue微任务Event Queue
    setTimeout1Process1
    setTimeout2then1
    • 上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。
    • 我们发现了process1和then1两个微任务。
    • 执行process1,输出6。
    • 执行then1,输出8。

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

    • 首先输出2。接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process2。new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2。
    宏任务Event Queue微任务Event Queue
    setTimeout2process2
    then2
    • 第二轮事件循环宏任务结束,我们发现有process2和then2两个微任务可以执行。
    • 输出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
    • 第三轮事件循环宏任务执行结束,执行两个微任务process3和then3。
    • 输出10。
    • 输出12。
    • 第三轮事件循环结束,第三轮输出9,11,10,12。

    整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。
    (请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)

    展开全文
  • 在线发布任务源码多用户版 任务悬赏,点赞,微任务引流系统威客赚钱.rar
  • 然后还有 事件循环、消息队列,还有微任务、宏任务这些这几天在掘金、知乎等论坛翻阅了不少大佬的文章,似乎了解到了一二,然后在这里把自己的体会总结出来,帮助大家快速理解,也能增加自己的记忆。单线程与多线程 ...

    前言

    我们都知道javascript是一门单线程、异步、非阻塞、解析类型脚本语言。

    • 单线程 ??
    • 异步 ??
    • 非阻塞 ??
    • 然后还有 事件循环、消息队列,还有微任务、宏任务这些

    这几天在掘金、知乎等论坛翻阅了不少大佬的文章,似乎了解到了一二,然后在这里把自己的体会总结出来,帮助大家快速理解,也能增加自己的记忆。

    单线程与多线程

    JavaScript 的设计就是为了处理浏览器网页的交互(DOM操作的处理、UI动画等),决定了它是一门单线程语言。如果有多个线程,它们同时在操作 DOM,那网页将会一团糟。

    由此,我们就可以知道 js 处理任务是一件接着一件处理,从上往下顺序执行的

    console.log('开始')
    console.log('中间')
    console.log('结束')

    // 开始
    // 中间
    // 结束

    这个时候,思维开拓的同学可能就会说 那如果一个任务的处理耗时(或者是等待)很久的话,如:网络请求、定时器、等待鼠标点击等,后面的任务也就会被阻塞,也就是说会阻塞所有的用户交互(按钮、滚动条等),会带来极不友好的体验。

    但是:

    console.log('开始')
    console.log('中间')
    setTimeout(() => {
      console.log('timer over')
    }, 1000)
    console.log('结束')

    // 开始
    // 中间
    // 结束
    // timer over

    会发现 timer over 会在 打印结束之后打印,也就是说计时器并没有阻塞后面的代码

    其实,JavaScript 单线程指的是浏览器中负责解释和执行 JavaScript 代码的只有一个线程,即为JS引擎线程,但是浏览器的渲染进程是提供多个线程的,如下:

    • JS引擎线程
    • 事件触发线程
    • 定时触发器线程
    • 异步http请求线程
    • GUI渲染线程

    当遇到计时器、DOM事件监听或者是网络请求的任务时,JS引擎会将它们直接交给 webapi,也就是浏览器提供的相应线程(如定时器线程为setTimeout计时、异步http请求线程处理网络请求)去处理,而JS引擎线程继续后面的其他任务,这样便实现了 异步非阻塞

    定时器触发线程也只是为 setTimeout(..., 1000) 定时而已,时间一到,还会把它对应的回调函数(callback)交给 任务队列 去维护,JS引擎线程会在适当的时候去任务队列取出任务并执行。

    JS引擎线程什么时候去处理呢?消息队列又是什么?

    事件循环与消息队列

    JavaScript 通过 事件循环 event loop 的机制来解决这个问题。

    其实 事件循环 机制和 任务队列 的维护是由事件触发线程控制的。

    事件触发线程 同样是浏览器渲染引擎提供的,它会维护一个 任务队列

    JS引擎线程遇到异步(DOM事件监听、网络请求、setTimeout计时器等...),会交给相应的线程单独去维护异步任务,等待某个时机(计时器结束、网络请求成功、用户点击DOM),然后由 事件触发线程 将异步对应的 回调函数 加入到消息队列中,消息队列中的回调函数等待被执行。

    同时,JS引擎线程会维护一个 执行栈,同步代码会依次加入执行栈然后执行,结束会退出执行栈。

    如果执行栈里的任务执行完成,即执行栈为空的时候(即JS引擎线程空闲),事件触发线程才会从消息队列取出一个任务(即异步的回调函数)放入执行栈中执行。

    消息队列是类似队列的数据结构,遵循先入先出(FIFO)的规则。

    1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
    2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
    3. 一但"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
    4. 主线程不断重复上面的第三步。

    只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复,这种机制就被称为事件循环(event loop)机制。
    c29bedf42502f00bcc5865d43e3abb64.png

    上面说到了异步,JavaScript 中有同步代码与异步代码。,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

    具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

    同步:

    console.log('hello 0')

    console.log('hello 1')

    console.log('hello 2')

    // hello 0
    // hello 1
    // hello 2

    它们会依次执行,执行完了后便会返回结果(打印结果)。

    异步:

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

    console.log('hello 1')

    // hello 1
    // hello 0

    上面的 setTimeout 函数便不会立刻返回结果,而是发起了一个异步,setTimeout 便是异步的发起函数或者是注册函数,() => {...} 便是异步的回调函数。

    异步一般是以下:

    • 网络请求
    • 计时器
    • DOM时间监听

    宏任务与微任务

    Promise同样是用来处理异步的:

    console.log('script start')

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

    Promise.resolve().then(function() {
        console.log('promise1')
    }).then(function() {
        console.log('promise2')
    })

    console.log('script end')

    // script start
    // script end
    // promise1
    // promise2
    // timer over

    "promise 1" "promise 2" 在 "timer over" 之前打印了?

    这里有一个新概念:macro-task(宏任务) 和 micro-task(微任务)。

    所有任务分为 macro-taskmicro-task:

    • macro-task:主代码块、setTimeout、setInterval等(可以看到,事件队列中的每一个事件都是一个 macro-task,现在称之为宏任务队列)
    • micro-task:Promise、process.nextTick等

    JS引擎线程首先执行主代码块。

    每次执行栈执行的代码就是一个宏任务,包括任务队列(宏任务队列)中的,因为执行栈中的宏任务执行完会去取任务队列(宏任务队列)中的任务加入执行栈中,即同样是事件循环的机制。

    在执行宏任务时遇到Promise等,会创建微任务(.then()里面的回调),并加入到微任务队列队尾。

    micro-task必然是在某个宏任务执行的时候创建的,而在下一个宏任务开始之前,浏览器会对页面重新渲染(task >> 渲染 >> 下一个task(从任务队列中取一个))。同时,在上一个宏任务执行完成后,渲染页面之前,会执行当前微任务队列中的所有微任务。

    也就是说,在某一个macro-task执行完后,在重新渲染与开始下一个宏任务之前,就会将在它执行期间产生的所有micro-task都执行完毕(在渲染前)。

    这样就可以解释 "promise 1" "promise 2" 在 "timer over" 之前打印了。"promise 1" "promise 2" 做为微任务加入到微任务队列中,而 "timer over" 做为宏任务加入到宏任务队列中,它们同时在等待被执行,但是微任务队列中的所有微任务都会在开始下一个宏任务之前都被执行完。

    在node环境下,process.nextTick的优先级高于Promise,也就是说:在宏任务结束后会先执行微任务队列中的nextTickQueue,然后才会执行微任务中的Promise。

    执行机制:

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

    宏任务 macro-task(Task)

    一个event loop有一个或者多个task队列。task任务源非常宽泛,比如ajax的onload,click事件,基本上我们经常绑定的各种事件都是task任务源,还有数据库操作(IndexedDB ),需要注意的是setTimeout、setInterval、setImmediate也是task任务源。总结来说task任务源:

    • script
    • setTimeout
    • setInterval
    • setImmediate
    • I/O
    • requestAnimationFrame
    • UI rendering

    微任务 micro-task(Job)

    microtask 队列和task 队列有些相似,都是先进先出的队列,由指定的任务源去提供任务,不同的是一个 event loop里只有一个microtask 队列。另外microtask执行时机和Macrotasks也有所差异

    • process.nextTick
    • promises
    • Object.observe
    • MutationObserver

    宏任务和微任务的区别

    • 宏队列可以有多个,微任务队列只有一个,所以每创建一个新的settimeout都是一个新的宏任务队列,执行完一个宏任务队列后,都会去checkpoint 微任务。
    • 一个事件循环后,微任务队列执行完了,再执行宏任务队列
    • 一个事件循环中,在执行完一个宏队列之后,就会去check 微任务队列

    宏任务与微任务示例

    1、主线程上添加宏任务和微任务

    console.log('-------开始--------');

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

    new Promise((resolve, reject) => {
      for (let i = 0; i 5; i++) {
        console.log(i);
      }
      resolve()
    }).then(()=>{
      console.log('Promise'); 
    })

    console.log('-------结束--------');

    //-------开始--------
    //0
    //1
    //2
    //3
    //4
    //-------结束--------
    //Promise
    //setTimeout

    解析

    第一轮事件循环:

    • 整体script作为第一个宏任务进入主线程,遇到console.log,输出-------开始--------
    • 遇到setTimeout,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1
    • 遇到Promise,new Promise直接执行,循环依次输出0、1、2、3、4、。then被分发到微任务Event Queue中。我们记为then1
    • 继续往下,遇到clg,直接输出-------结束--------,到此第一轮事件循环即将结束,会先看当前循环有没有产生出微任务,有依次按产生顺序执行。
    • 发现有then1,输出 Promise,当前微任务执行完毕,到此,第一轮事件循环结束。

    发现 setTimeout1宏任务,开始第二轮事件循环:

    • 遇到clg,直接输出setTimeout,没有微任务,第二轮事件循环结束。

    所有宏任务执行完毕,整个程序执行完毕

    2 、在微任务中创建微任务

    setTimeout(()=> console.log('setTimeout4'))

    new Promise(resolve => {
      resolve()
      console.log('Promise1')
    }).then(()=> {
      console.log('Promise3')
      Promise.resolve().then(() => {
        console.log('before timeout')
      }).then(() => {
        Promise.resolve().then(() => {
          console.log('also before timeout')
        })
      })
    })

    console.log('结束')

    //Promise1
    //结束
    //Promise3
    //before timeout
    //also before timeout
    //setTimeout4

    解析

    第一轮事件循环:

    • 遇到setTimeout,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1
    • 遇到Promise,new Promise直接执行,输出Promise1。then被分发到微任务Event Queue中。我们记为then1(这里注意不要看then里面的内容)。
    • 遇到console.log,输出结束
    • 执行微任务then1,遇到clg,输出Promise3,遇到 Promise.resolve().then,then被分发到微任务Event Queue中。我们记为then2
    • 执行 then2,clg输出before timeout,生成微任务then3
    • 执行 then3,clg输出also before timeout,至此微任务,执行完毕,第一轮事件循环结束

    发现 setTimeout1宏任务,开始第二轮事件循环:

    • console.log,输出setTimeout4,无微任务,第二轮事件循环结束

    3、微任务队列中创建宏任务

    new Promise((resolve) => {
      console.log('new Promise(macro task 1)');
      resolve();
    }).then(() => {
      console.log('micro task 1');
      setTimeout(() => {
        console.log('setTimeout1');
      }, 0)
    })

    setTimeout(() => {
      console.log('setTimeout2');
    }, 500)

    console.log('========== 结束==========');

    //new Promise(macro task 1)
    //========== 结束==========
    //micro task 1
    //setTimeout1
    //setTimeout2

    解析

    第一轮事件循环:

    • 遇到Promise,new Promise直接执行,输出new Promise(macro task 1)。then被分发到微任务Event Queue中。我们记为then1
    • 遇到setTimeout,其回调函数被分发到宏任务Event Queue中。记为宏set1
    • clg输出========== 结束==========
    • 执行微任务then1,clg输出micro task 1,遇到setTimeout,生成宏任务,记为宏set2,至此微任务,执行完毕,第一轮事件循环结束

    发现有两个定时器宏任务,这里优先执行宏set2(为什么优先执行,下面详细解释),开始第二轮事件循环:

    • clg输出setTimeout1,无微任务,第二轮事件循环结束

    执行宏set1,开始第三轮事件循环:

    • clg输出setTimeout2,无微任务,第三轮事件循环结束

    为什么会优先宏set2

    尽管 宏set1先被定时器触发线程处理,但是宏set2 的callback会先加入消息队列。

    上面,宏set2的延时为 0ms,HTML5标准规定 setTimeout 第二个参数不得小于4(不同浏览器最小值会不一样),不足会自动增加,所以 "setTimeout2" 还是会在 "setTimeout1" 之后。

    就算延时为 0ms,只是宏set2的回调函数会立即加入消息队列而已,回调的执行还是得等执行栈为空(JS引擎线程空闲)时执行。

    其实 setTimeout 的第二个参数并不能代表回调执行的准确的延时事件,它只能表示回调执行的最小延时时间,因为回调函数进入消息队列后需要等待执行栈中的同步任务执行完成,执行栈为空时才会被执行。

    4、宏任务中创建微任务

    setTimeout(() => {
      console.log('timer_1');
      setTimeout(() => {
        console.log('timer_3')
      }, 0) 
      new Promise(resolve => {
        resolve()
        console.log('new promise')
      }).then(() => {
        console.log('promise then')
      })
    }, 0)

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

    console.log('结束')

    //结束
    // timer_1
    //new promise
    //promise then
    // timer_2
    //timer_3

    解析

    第一轮事件循环:

    • 创建宏set1
    • 创建宏set2
    • console.log输出 “结束”
    • 无微任务,当前事件循环结束

    执行宏set1,开始第二事件循环:

    • console.log输出 “timer_1”
    • 创建宏set3
    • new Promise,直接输出 'new promise',创建微任务then1
    • 执行微任务then1,输出 'promise then'
    • 无微任务,当前事件循环结束

    执行宏set2,开始第三事件循环:

    • clg 输出 'timer_2'
    • 无微任务,当前事件循环结束

    执行宏set3,开始第四事件循环:

    • clg 输出 'timer_3'
    • 无微任务,当前事件循环结束

    事件冒泡+事件循环

     <div class="outer">
        <div class="inner">div>
      div>
    var outer = document.querySelector('.outer');
    var inner = document.querySelector('.inner');

      function onClick() {
        console.log('inner');

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

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

      }
      function onClick2() {
        console.log('outer');

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

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

      }

      inner.addEventListener('click', onClick);
      outer.addEventListener('click', onClick2);

    // inner
    // inner-promise
    // outer
    // outer-promise
    // inner-timeout
    // outer-timeout

    事件冒泡是从内往外触发的,所以:

     (1)点击 inner,onClick 函数入执行栈执行,打印 "click"。执行完后执行栈为空,因为事件冒泡的缘故,事件触发线程会将向上派发事件的任务放入宏任务队列。
    (2)遇到 setTimeout,在最小延迟时间后,将回调放入宏任务队列。遇到 promise,将 then 的任务放进微任务队列
    (3)此时,执行栈再次为空。开始清空微任务,打印 "promise"
    (4)此时,执行栈再次为空。从宏任务队列拿出一个任务执行,即前面提到的派发事件的任务,也就是冒泡。
    (5)事件冒泡到 outer,执行回调,重复上述 "click"、"promise" 的打印过程。
    (6)从宏任务队列取任务执行,这时我们的宏任务队列已经累计了两个 setTimeout 的回调了,所以他们会在两个 Event Loop 周期里先后得到执行。

    总结

    • 从上往下,同步直接执行,异步分发MacroTask或者microtask
    • 碰到MacroTask直接执行,并且把回调函数放入MacroTask执行队列中(下次事件循环执行);碰到microtask直接执行。把回调函数放入microtask执行队列中(本次事件循环执行)
    • 当同步任务执行完毕后,去执行微任务microtask。(microtask队列清空)
    • 由此进入下一轮事件循环:执行宏任务 MacroTask (setTimeout,setInterval,callback)

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

    console.log('1');

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

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

    请在评论方留下正确的打印循序!!

    结尾

    希望大家看了本篇文章都有收获 ...

    关注【前端知识小册】,第一时间获取前端优质文章!

    aa8c8401ab974abe9c1c1da8214492c1.png

    展开全文
  • 二、宏任务和微任务 2.1 初识宏任务和微任务 2.2 宏任务和微任务 2.3 常见的宏任务和微任务 2.4 注意点: 一、事件环 1.1 初识事件环 和浏览器中一样NodeJS中也有事件环(EventLoop),但是由于执行代码的...
  • 新浪微博的微任务是什么.docx
  • 所以为了解决这个问题,js委托宿主环境(浏览器)帮忙执行耗时的任务,执行完成后,在通知js去执行回调函数,而宿主环境帮我们执行的这些耗时任务也就是异步任务 js本身是无法发起异步的,但是es5之后提出了Promise...
  • 关于异步任务呢,又区分为两种【宏任务、微任务】,接下来让我们详细的了解两种的执行顺序与区别。 代码如下(示例): <script> console.log('1') setTimeout(function () { console.log('2') }); n
  • 宏任务与微任务面试题

    千次阅读 2020-09-03 20:50:07
    宏任务与微任务面试题 对于宏任务与微任务的执行顺序问题一直都是前端面试一到过不去的坎,这东西多看是没用的,多做就是了,刷了10来到面试题,立马顿悟。 1 async function async1 () { console.log('async1...
  • JavaScript中的宏任务和微任务

    千次阅读 2019-03-12 22:30:33
    主线程执行完后,假如产生了宏任务和微任务,那么他们的回调会被分别推到宏任务队列和微任务队列中去,等到下一次事件循环来到时,若存在微任务,则会先依次执行完所有的微任务,然后再依次执行所有的宏任务。...
  • 微任务/宏任务和同步/异步之间的关系

    千次阅读 多人点赞 2021-05-14 10:13:09
    彻底理清微任务/宏任务和同步/异步之间的关系 前言 网上已有很多相关的js执行机制的文章了,那为啥还要写这个? 原因是其中一个机制大家有两套说法,不多BB直接上争议点 ↓ 1.认为宏任务包含所有script代码的 2.宏...
  • EXP2: 在微任务中创建微任务 EXP3: 宏任务中创建微任务 EXP4:微任务队列中创建的宏任务 总结 这篇博文仅为个人理解,文章内提供一些更加权威的参考,如有片面及错误,欢迎指正 1. 事件轮询...
  • 宏任务和微任务

    千次阅读 2020-09-15 17:00:11
    也就是说,在 JS 中,立即执行的代码叫做同步任务;过了一段时间之后,满足了一定条件时才执行的代码叫做异步任务。 按照这种分类,JS 的执行机制是这样的: 首先判断 JS 是同步还是异步,同步就进入主进程,异步就...
  • => { console.log('then2') }) console.log('end') 应该不少同学都能答出来,结果为: start promise end then1 then2 setTimeout 这个就涉及到JavaScript事件轮询中的宏任务和微任务。那么,你能说清楚到底宏任务和...
  • 主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所...
  • 微任务队列:Promise的then回调、Mutation Observe API、queueMicrotask 两个队列的优先级 所有同步任务都在主线程上执行,形成一个执行栈 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行...
  • 而异步任务又可以分为微任务和宏任务。 代码示例: console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve().then(function() { console.l
  • JS宏任务微任务

    2020-11-06 20:55:32
    微任务和宏任务: 任务的划分: js代码的任务,分为哪个东西调度? 1:js引擎 → 异步 → Promise(调用动作) →(微任务); 2:浏览器宿主 → 异步 → XMLHttpRequest(Ajax)/ setInterval / setTimeout / …/ ...
  • 宏任务和微任务、事件循环 JavaScript是单线程的,也就是说,同一个时刻,JavaScript只能执行一个任务,其他任务只能等待。 为什么JavaScript是单线程的 js是运行于浏览器的脚本语言,因其经常涉及操作dom,如果...
  • 首先一定要明确一点:JS 是...单线程导致的问题就是后面的任务等待前面任务完成,如果前面任务很耗时(比如读取网络数据),后面任务不得不一直等待!! 为了解决这个问题,利用多核 CPU 的计算能力,HTML5 提出 We...
  • 1.宏任务和微任务 宏任务和微任务是等待任务队列中的异步任务的处理机制;(JS执行有同步任务队列和等待任务队列) 浏览器的任务队列: 主任务队列存储的都是同步任务; 等待任务队列存储的都是异步任务; 首先...
  • 然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。听起来有点绕,我们用文章最开始的一段代码说明:   setTimeout(function() { console.log('setTimeout'); ...
  • node中我对Event Loop和宏任务,微任务的理解

    万次阅读 多人点赞 2019-10-24 13:49:37
    异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。 只要主线程空了,就会去读取"任务队...
  • 宏任务微任务同步任务执行顺序以及使用async函数的值 有道云笔记地址:http://note.youdao.com/noteshare?id=7d1ac13517dee68ff3b16e2079338bd9&sub=AB8C799DBAD64A2EBBD6FD088A00FB95 async await 使用: ...
  • 答:宏任务先执行然后再执行微任务。因为script是一个大的宏任务! 首先你需要了解任务分类,在js中任务分为同步任务和异步任务,为什么这么搞?因为js是单线程的,为什么是单线程的?因为js设计之初是为了解决页面...
  • JS的同步和异步、微任务和宏任务

    千次阅读 2020-11-26 20:41:40
    js是运行于浏览器的脚本语言,因其经常涉及操作dom,如果是多线程的,如果一个线程修改dom,另一个线程删除dom,那么浏览器就不知道该先执行哪个操作,所以js执行的时候会按照一个任务一个任务来执行,那么任务是...
  • 宏任务和微任务理解

    2021-02-09 23:48:23
    前言:理解宏任务和微任务前,先得理解两个过程,如下: 一、JavaScript执行过程 const name = "miracle"; console.log(name); function sum(num1, num2) { return num1 + num2; } function bar() { return sum(20, ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 132,135
精华内容 52,854
关键字:

微任务

友情链接: newton.zip