zoukankan      html  css  js  c++  java
  • 深入浅出Javascript事件循环机制

    一、JS单线程、异步、同步概念

    众所周知,JS是单线程(如果一个线程删DOM,一个线程增DOM,浏览器傻逼了~所以只能单着了),虽然有webworker酱紫的多线程出现,但也是在主线程的控制下。webworker仅仅能进行计算任务,不能操作DOM,所以本质上还是单线程。

      单线程即任务是串行的,后一个任务需要等待前一个任务的执行,这就可能出现长时间的等待。但由于类似ajax网络请求、setTimeout时间延迟、DOM事件的用户交互等,这些任务并不消耗 CPU,是一种空等,资源浪费,因此出现了异步。通过将任务交给相应的异步模块去处理,主线程的效率大大提升,可以并行的去处理其他的操作。当异步处理完成,主线程空闲时,主线程读取相应的callback,进行后续的操作,最大程度的利用CPU。此时出现了同步执行和异步执行的概念,同步执行是主线程按照顺序,串行执行任务;异步执行就是cpu跳过等待,先处理后续的任务(CPU与网络模块、timer等并行进行任务)。由此产生了任务队列与事件循环,来协调主线程与异步模块之间的工作。
    注释:HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

    二、事件循环机制

    如上图为事件循环示例图(或JS运行机制图),流程如下:

        step1:主线程读取JS代码,此时为同步环境,形成相应的堆和执行栈;
        step2:  主线程遇到异步任务,指给对应的异步进程进行处理(WEB API);
        step3:  异步进程处理完毕(Ajax返回、DOM事件处罚、Timer到等),将相应的异步任务推入任务队列;
        step4: 主线程执行完毕,查询任务队列,如果存在任务,则取出一个任务推入主线程处理(先进先出);
        step5: 重复执行step2、3、4;称为事件循环。
      执行的大意:
        同步环境执行(step1) -> 事件循环1(step4) -> 事件循环2(step4的重复)…
      其中的异步进程有:
        a、类似onclick等,由浏览器内核的DOM binding模块处理,事件触发时,回调函数添加到任务队列中;
        b、setTimeout等,由浏览器内核的Timer模块处理,时间到达时,回调函数添加到任务队列中;
        c、Ajax,由浏览器内核的Network模块处理,网络请求返回后,添加到任务队列中。

    三、函数调用栈与任务队列

    Javascript有一个main thread 主进程和call-stack(一个调用堆栈),在对一个调用堆栈中的task处理的时候,其他的都要等着。当在执行过程中遇到一些类似于setTimeout等异步操作的时候,会交给浏览器的其他模块(以webkit为例,是webcore模块)进行处理,当到达setTimeout指定的延时执行的时间之后,task(回调函数)会放入到任务队列之中。一般不同的异步任务的回调函数会放入不同的任务队列之中。等到调用栈中所有task执行完毕之后,接着去执行任务队列之中的task(回调函数)。参考如上图

    四、测试

    for (var i = 0; i < 5; i++) {
        setTimeout(function() {
          console.log(new Date, i);
        }, 1000);
    }
    console.log(new Date, i);

    这段代码很短,只有 7 行,我想,能读到这里的同学应该不需要我逐行解释这段代码在做什么吧。候选人面对这段代码时给出的结果也不尽相同,以下是典型的答案:

    • A. 20% 的人会快速扫描代码,然后给出结果:0,1,2,3,4,5
    • B. 30% 的人会拿着代码逐行看,然后给出结果:5,0,1,2,3,4
    • C. 50% 的人会拿着代码仔细琢磨,然后给出结果:5,5,5,5,5,5

    只要你对 JS 中同步和异步代码的区别、变量作用域、闭包等概念有正确的理解,就知道正确答案是 C,代码的实际输出是:

    2017-03-18T00:43:45.873Z 5
    2017-03-18T00:43:46.866Z 5
    2017-03-18T00:43:46.868Z 5
    2017-03-18T00:43:46.868Z 5
    2017-03-18T00:43:46.868Z 5
    2017-03-18T00:43:46.868Z 5
    1. 首先i=0时,满足条件,执行栈执行循环体里面的代码,发现是setTimeout,将其出栈之后把延时执行的函数交给Timer模块进行处理。

    2. 当i=1,2,3,4时,均满足条件,情况和i=0时相同,因此timer模块里面有5个相同的延时执行的函数。

    3. 当i=5的时候,不满足条件,因此for循环结束,console.log(new Date, i)入栈,此时的i已经变成了5。因此输出5。

    4. 此时1s已经过去,timer模块将5个回调函数按照注册的顺序返回给任务队列。

    5. 执行引擎去执行任务队列中的函数,5个function依次入栈执行之后再出栈,此时的i已经变成了5。因此几乎同时输出5个5。

    6. 因此等待的1s的时间其实只有输出第一个5之后需要等待1s,这1s的时间是timer模块需要等到的规定的1s时间之后才将回调函数交给任务队列。等执行栈执行完毕之后再去执行任务对列中的5个回调函数。这期间是不需要等待1s的。因此输出的状态就是:5 -> 5,5,5,5,5,即第1个 5 直接输出,1s之后,输出 5个5;

     

    五、进阶

    如果添加了Promise又如何工作呢? 
    我们知道,Promise的回调函数不是传入的,而是使用then来调用的。因此,Promise中定义的函数应该是马上执行的,then才是其回调函数,放入queue队列中。 
    在参考的文章中还提到了一个重要的概念:

    macro-task包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。 
    micro-task包括:process.nextTick, Promises, Object.observe, MutationObserver 
    执行顺序:函数调用栈清空只剩全局执行上下文,然后开始执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次执行macro-task中的一个任务队列,执行完之后再执行所有的micro-task,就这样一直循环。

    参考文献:https://www.cnblogs.com/hity-tt/p/6733062.html

  • 相关阅读:
    SpringBoot启动过程中,候选类的过滤和加载
    Dubbo发布过程中,扩展点的加载
    Dubbo发布过程中,服务发布的实现
    Dubbo发布过程中,服务端调用过程
    SpringBean加载过程中,循环依赖的问题(一)
    Dubbo发布过程中,消费者的初始化过程
    DiscuzQ构建/发布小程序与H5前端
    Delphi写COM+的心得体会
    DBGridEh导出Excel等格式文件
    数据库直接通过bcp导出xml文件
  • 原文地址:https://www.cnblogs.com/yu-hailong/p/8638883.html
Copyright © 2011-2022 走看看