精华内容
下载资源
问答
  • 宏任务和微任务

    千次阅读 2020-11-05 15:02:32
    宏任务微任务 1.同步任务和异步任务 JavaScript是单线程执行的语言,在同一个时间只能做一件事情。这就导致后面的任务需要等到前面的任务完成才能执行,如果前面的任务很耗时就会造成后面的任务一直等待。为了解决...

    宏任务与微任务

    1.同步任务和异步任务

    JavaScript是单线程执行的语言,在同一个时间只能做一件事情。这就导致后面的任务需要等到前面的任务完成才能执行,如果前面的任务很耗时就会造成后面的任务一直等待。为了解决这个问题JS中出现了同步任务和异步任务。

    (1)同步任务

    在主线程上排队执行的任务只有前一个任务执行完毕,才能执行后一个任务,形成一个执行栈。

    (2)异步任务

    不进入主线程,而是进入任务队列,当主线程中的任务执行完毕,就从任务队列中取出任务放进主线程中来进行执行。由于主线程不断重复的获得任务、执行任务、再获取再执行,所以者种机制被叫做事件循环(Event Loop)

    2.宏任务与微任务

    我们会发现异步任务种的事件并不是严格按照顺序来执行的,如下:

        <script>  
    setTimeout(() => {
                console.log("定时器");
            }, 0);
            Promise.resolve().then(value=>{
                 console.log('Promise');
            })
            console.log('console');
    </script>
    

    执行结果为:
    在这里插入图片描述

    ​ 这时我们就会发现任务队列中的任务是有优先级的,上文中的代码Promise的优先级高于setTimeout。再往下探索发现,任务队列中的任务也是分为两种,宏任务和微任务。

    常见的微任务有Promiseprocess.nextTickMutationObserver

    常见的宏任务:scriptsetTimeoutsetIntervalsetImmediate

    发出疑问:到底是宏任务优先还是微任务优先?(个人观点:微任务优于宏任务)
    
    经过实践并参考了前辈们的文章后做出总结:
    Event Loop执行顺序为:

    先执行宏任务script,并执行里面的同步任务;执行栈为空后查询是否存在微任务,存在就立即执行,然后开始下一轮的事件循环。

    从上面的执行顺序可知,如果宏任务中有大量的计算并且需要操作DOM的话,为了更快响应,可以把操作DOM放进微任务务中。

    3.定时器

    定时器在开发中经常能用到,一个是超时调用定时器setTimeout(),也叫做爆炸定时器,另一个是间歇调用定时器setInterval()。但是在我们设置延时后会发现延时的时间和触发回调函数的时间不一样,究其原因,仍然是和执行机制有关。

    首先我们知道定时器是宏任务,在定时器计时完毕后会将它的回调函数加入到任务队列中,等待下一次的事件循环。这也就是说下一次执行的定时器仅仅只是回调函数,计时已经在定时器模块中完成了。

    如下代码:

    setInterval(()=>{
                console.log('计时5S');
            },5000)
            for(let i=0;i<10009;i++){
                console.log('');
            }
    

    毫无疑问,首先执行的是for循环,但是当for循环执行结束后会立即打印出“计时5S”两次。

    4.Promise代表的微任务

    代码:

     setTimeout(()=>{
                console.log('定时器');//1
            },0)
            new Promise(resolve=>{
                console.log('Promise');//2
                resolve()
            })
            .then(()=>{console.log('then')});//3
            console.log('console');//4
    //输出结果:2>4>3>1
    

    5.任务共享内存

    let i=0
            setTimeout(()=>{
                console.log(++i);//1
            },0)
            setTimeout(()=>{
                console.log(++i);//2
            },0)
    //把两个定时器加入到任务队列中,执行完第一个时i自增变为1,再执行第二个定时器函数后i为2
    
    展开全文
  • 本文根据 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联系

    展开全文
  • 很多朋友在写面试题:宏任务微任务的执行时机时,容易犯迷糊。之前我也写过两篇文章,这次加上详细的讲解,让大家更容易理解。 什么是宏任务微任务? 1、首先要称得上是宏任务微任务的,必须是一个回调函数,...

    背景

    很多朋友在写面试题:宏任务与微任务的执行时机时,容易犯迷糊。之前我也写过两篇文章,这次加上详细的讲解,让大家更容易理解。

    什么是宏任务、微任务?

    1、首先要称得上是宏任务、微任务的,必须是一个回调函数,例如:

     setTimeout(getList,5000);
    

    这个getList就是一个回调函数

    2、必须是异步执行的回调函数

    3、区分宏任务与微任务

    宏任务
    渲染事件(如解析 DOM、计算布局、绘制);

    用户交互事件(如鼠标点击、滚动页面、放大缩小等);

    JavaScript 脚本执行事件;

    网络请求完成、文件读写完成事件

    微任务

    MutationObserver;

    Promise.resolve();

    就这么些任务,在浏览器环境中,系统已经把他们区分成了宏任务和微任务,并没有其他特殊的含义!!死记下来就行了

    每个宏任务对应了一个微任务队列

    每一个宏任务,都对应了一个微任务队列,没什么特别的,你现在知道了这点,而且知道了哪些是宏任务和微任务

    全局同步代码,是特殊的宏任务

    全局的同步执行代码,也要看成一个宏任务,那这个宏任务,也对应了他的微任务队列,例如:

    for (let i=0;i<1000;i++){
    
    //dosomething
    
    }
    Promise.resolve().then(()=>{
      //dosomething
    })
    
    setTimeout(()=>{
      //dosomething
    })
    

    当for循环(全局的同步代码执行完毕后,就会开始执行微任务队列的任务),这个时候就会去执行Promise.resolve().then里面的代码。

    那么接下来,就会执行setTImeout回调里面的代码

    假设宏任务里面再继续有微任务

    for (let i=0;i<1000;i++){
    
    //dosomething
    
    }
    Promise.resolve().then(()=>{
      console.log(1);
    })
    
    setTimeout(()=>{
      console.log(2);
      setTimeout(()=>{
       console.log(3);
      })
      Promise.resolve().then(()=>{
      console.log(4);
    })
    })
    

    打印顺序是:1 2 4 3

    分析:

    当全局的同步代码(宏任务)执行完毕后,执行全局宏任务对应的微任务队列,输出1.

    清空全局同步代码对应的微任务队列后,开始执行下一个宏任务,下一个宏任务是定时器回调函数,输出2.

    这个定时器宏任务对应了一个微任务队列,此时执行Promise.resolve.then这个微任务,输出4

    最后执行完这个微任务队列后,继续执行下一个宏任务,输出3

    加餐

    大家看完上面,可能只知道了宏任务和微任务的执行时机,但是整个系统是怎么任务调度的?

    其实大家可以把整个页面任务调度系统看成是一个for循环

    整个系统的任务调度

    • 执行全局的同步代码,同步代码执行完毕之后,开启for循环模式

    • 接着读取全局同步代码对应的微任务队列并且执行完毕

    • 再继续执行此时需要执行的宏任务,例如:到时间需要执行的定时器回调函数等

    • 执行完宏任务后,继续执行对应的微任务队列,清空微任务队列

    • 接着继续执行此时需要执行的宏任务…

    周而复始,像一个for循环一样。

    用一张图来讲解就是:在这里插入图片描述

    展开全文
  • 微任务可以在实时性效率之间做一个有效的权衡。 从目前的情况来看,微任务已经被广泛地应用,基于微任务的技术有 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 方案的核心就是采用了微任务机制,有效地权衡了实时性和执行效率的问题。

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

    展开全文
  • 宏任务 渲染进程内部会维护多个消息队列,比如延迟执行队列普通的消息队列。然后主线程采用一个 for 循环,不断地从这些任务队列中取出任务并执行任务。我们把这些消息队列中的任务称为宏任务。 主线程上执行的...
  • 一、理解宏任务和微任务 这个就像去银行办业务一样,先要取号进行排号。 一般上边都会印着类似:“您的号码为XX,前边还有XX人。”之类的字样。 因为柜员同时职能处理一个来办理业务的客户,这时每一个来办理业务...
  • 概念5:宏任务和微任务 ES6 规范中,microtask 称为 jobs,macrotask 称为 task 宏任务是由宿主发起的,而微任务由JavaScript自身发起。 在ES3以及以前的版本中,JavaScript本身没有发起异步请求的能力,也就没有...
  • 微任务永远比宏任务先执行,当微任务中没有任务了才执行宏任务,中途如果微任务有加入任务,那么将停止宏任务,继续执行微任务 常见的宏任务队列 ajax,setTimeout,setInterval,DOM监听,UI Rendering等 常见的微任务...
  • 因为JS单线程的特点,所以提出了任务队列的概念。主线程从"任务队列"中读取事件,这个过程是循环不断的。这个过程会不断重复。所以被称为Event Loop.事件循环的概念很常见,比如在vue源码中nextTick中就有涉及到这个...
  • JavaScript中的宏任务和微任务

    千次阅读 2019-03-12 22:30:33
    主线程执行完后,假如产生了宏任务和微任务,那么他们的回调会被分别推到宏任务队列和微任务队列中去,等到下一次事件循环来到时,若存在微任务,则会先依次执行完所有的微任务,然后再依次执行所有的宏任务。...
  • 宏任务和微任务、事件循环 JavaScript是单线程的,也就是说,同一个时刻,JavaScript只能执行一个任务,其他任务只能等待。 为什么JavaScript是单线程的 js是运行于浏览器的脚本语言,因其经常涉及操作dom,如果...
  • 同步任务 叫做非耗时任务,指的是在主线程上排队执行的那些任务 只有前一个任务执行完毕了,才能执行后一个任务 异步任务 叫做耗时任务,异步任务由 javascript 委托给宿主环境进行执行的 当异步任务完成后,会...
  • javascript的宏任务和微任务

    万次阅读 多人点赞 2018-04-11 20:28:19
    最后他让我回去看一下宏任务和微任务。 首先说一下普通的异步函数的执行过程吧:   同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。当指定的事情完成时,...
  • 堆(heap)、栈(stack)、事件循环(Event Loop) 三、引出宏任务和微任务     进入正题,除了广义的同步任务和异步任务,我们对任务有更精细的定义如下,微任务的优先级比宏任务高,只要微任务队列里面有任务...
  • 宏任务(macrotask )和微任务(microtask ) macrotask microtask 表示异步任务的两种分类。 在挂起任务时,JS 引擎会将所有任务按照类别分到这两个队列中,首先在macrotask 的队列(这个队列也被叫做task ...
  • 宏任务和微任务理解

    2021-02-09 23:48:23
    (3)下面是浏览器的事件循环过程图: 三、宏任务和微任务 但是事件循环中并非只维护着一个队列,事实上是有两个队列: (1)宏任务队列(macrotask queue):ajax、setTimeout、setInterval、DOM监听、UI ...
  • 每个线程都会有它自己的event loop(事件循环),在event loop任务队列中分为宏任务和微任务。 macro-task(宏任务):包括整体代码script,setTimeout,setInterval micro-task(微任务):Promise,process.nextTick...
  • js的宏任务和微任务

    2021-06-01 19:52:20
    先来看一道简单的面试题: console.log('start') setTimeout(() => { console.log('setTimeout') }, 0) new Promise((resolve) => { console.log('promise') resolve() ... console.log('then2')
  • 我们来看看答案:1 5 7 6 2 3 4 这是我的理解:宏任务和微任务, settimeout属于宏任务, promise属于微任务,正常情况下,先执行完同步队列的任务, 再执行异步队列中的微任务,最后才执行异步队列中的宏任务
  • JavaScript 把异步任务又做了进一步划分,异步任务又分为两类: 同步任务:非耗时任务 异步任务又称 耗时任务 ...宏任务和微任务的执行顺序 每一个宏任务执行完以后都会检查是否存在待执行的微...
  • 答:宏任务先执行然后再执行微任务。因为script是一个大的宏任务! 首先你需要了解任务分类,在js中任务分为同步任务和异步任务,为什么这么搞?因为js是单线程的,为什么是单线程的?因为js设计之初是为了解决页面...
  • vue 题解宏任务和微任务宏任务: 当前调用栈中执行的代码成为宏任务 (主代码块 定时器等等) 微任务: 当前(此次事件循环中)宏任务执行完, 在下一个宏任务开始之前需要执行的任务,可以理解成回调事件 (promise ...
  • 宏任务(macro-task) 定义消息队列中的任务称为宏任务。产生 宿主环境提供的方法是宏任务,例如setTimeout, setInterval。这些都是浏览器或者Node环境实现的。 执行 不断从消息队列中取出并被事件循环执行。 类型 ...
  • eventloop 事件循环 – 像是轮询,不间断地看有没有需要执行的回调能不能执行(轮询) js执行机制(有问题,请留言指出,谢谢):从上到下开始执行,同步代码一直往下走,异步代码放到队列当中,然后继续走同步,同步走完之后,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 17,469
精华内容 6,987
关键字:

宏任务和微任务