zoukankan      html  css  js  c++  java
  • JS运行机制之 Event Loop 的思考

    先举个栗子,如下:

    for (var i = 0; i < 5; i++) {
        setTimeout(function() {
            console.log('i: ',i);        //一秒之后输出几乎没有时间间隔依次输出5个5
        }, 1000);
    }
    console.log(i);                      //立即输出5
    

    想必很多人看到立马能看出答案吧,但是为什么定时器不能依次打印出0, 1,2,3,4呢?答案稍后分晓。

    那到底怎么才能依次输出我们想要的结果呢?大家可能都想到是利用闭包,或者是利ES6中的let声明,再或者可以用Promise, 如果还不过瘾就用ES7 的async或者await; 如下,但是今天我们主要不讲这个。

    //利用闭包
    for (var i = 0; i < 5; i++) {
        (function(i) {
            setTimeout(function() {  
                console.log(i);   //0,1,2,3,4
            }, 1000);
        })(i);
    }
     
    console.log( i);             //5
    
    //let
    for (let i = 0; i < 5; i++) {
        setTimeout(function() {
            console.log (i);
        }, 1000);        //0 ,1,2,3,4
    }
     
    console.log( i);     //这里会报错,因为let声明的块级作用域,外面是拿不到这个i的,使用没有声明的变量当然会报错啦
    

      

    一、为什么js是单线程?

    大家都知道js不同于其他语言,它是单线程的。那么问题来了,为什么不是多线程呢?按道理来说多线程不是能够同时解决问题提高效率么?除了多线程产生冲突、抢占资源等答案,还可以是什么呢?

    其实,这跟它作为浏览器脚本语言的用途有关,浏览器的脚本语言主要的用途是用来与用户互动,会产生DOM的操作,这就是问题的关键,假设js是多线程,有一个线程是删除DOM操作,有个在当前DOM添加内容,这时候浏览器应该怎么办呢?这就决定了js应该被设计成单线程。那js有没有多线程的可能呢?答案是肯定的,HTML5提出的web Worker标准,但是,

    “为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质”  ----阮一峰的一篇博客

    二 、 Event Loop 事件循环

    因为js是单线程,所以执行时候需要排队,前一个任务结束之后,后一个任务才会执行,那么如果前一个任务执行很久,后面一个任务就要等很久。如果一些等待不是因为CPU计算慢产生的,比如IO设备的使用,那么js会把等待的任务挂起,执行后面的任务,等IO返回结果,再回头执行直线挂起的任务。

    这样任务就有了同步任务和异步任务,同步任务是主线程上执行的任务,形成一个执行栈(execution context stack),是排队进行的。异步任务不是在主线程执行的,是在“任务队列”(Queue)里面,只有当主线程所有同步的任务执行完成,任务队列中的异步任务才会进入主线程,然后被执行。

    异步执行机制如下:---阮一峰

    可视化描述---MDN

    主线程上:

    一个函数1被调用了,创建一个堆栈帧,包含了函数1的参数和局部变量,当函数1中又调用了函数2,又创建了一个堆栈帧,此时这个堆栈帧在第一个堆栈帧之前,包含了函数2的参数和局部变量。主线程先执行了至于顶层的堆栈帧(函数2产生的),当函数2返回时,对应的堆栈帧就出栈了,接着继续执行函数1的堆栈帧,直到栈空了。

    消息队列:
    一个 js运行时包含了一个待处理的消息队列。每一个消息都与一个函数相关联。当栈为空时,从队列中取出一个消息进行处理。这个处理过程包含了调用与这个消息相关联的函数(以及因而创建了一个初始堆栈帧)。当栈再次为空的时候,也就意味着消息处理结束。 

    添加消息:

    在浏览器里,添加事件可以是当一个事件出现且有一个事件监听器被绑定时,消息被随时添加。也可以是在调用setTimeout等函数时候,

    在将来的某个时间后在消息对列中添加。

    setTimeout:

    调用该函数时候会在将来的某个时间后在消息对列中添加一个消息,如果执行栈中有其他任务没有完成(假设有一个很耗时的计算),setTimeout消息必须必须等到执行栈的任务完成才会处理,所以说该函数的第二个参数仅仅表示最少的时间 而非确切的时间。

    即使你设置零延迟:

     setTimeout(function cb1() {
        console.log(‘我是第二个被执行的’);
      }, 0);                           
     console.log(‘我是第一个被执行的’);       //先打印这句    
              
    

    事实上,js中规定,定时器的第二个参数设置最少不能小于4ms, 小于的话就按最小的4ms执行。

    上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任

    务队列"中加入各种事件(click,load)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。

    三、回头看看开头的栗子

    for循环和外面的console.log()是主线程上的同步任务,他们按循序执行,先执行for循环结束(此时i变为5),再执行console.log();

    for循环中的setTimeout是在任务队列中,它只有等在前主线程中的栈空了之后,到一个时间才会被被执行,此时作用域中的i已经变为5,此时setTimeout中的回调函数所读取的i就是5了。

    还有一个问题?

    输出5的时间间隔是多少?答案显而易见是,立即打印一个5,1000ms之后几乎同时输出5个5; why???

    正如上面解释的,for循环一次,会在任务队列中加上一个setTimeou任务(该任务是在1000ms后执行回调函数),这样循环结束,任务列表里面就有了5个setTimeou任务,且当主线程中栈空了之后,任务列表就开始进栈,等待1000ms之后执行回调,(注意此时的i变量已经变成5了)所以后面的5个5几乎在同时依次打印出来。

    四、任务队列

    JS分为同步任务和异步任务;

    同步任务都是在主线程上执行,形成一个执行栈;

    主线程之后,事件触发线程管理着一个任务队列【宏任务(MacroTask)和微任务(MicroTask)】,只要异步任务有了运行结果,就会往任务队列里面放置一个事件。

    一旦执行栈中的所有同步任务执行完毕,此时JS引擎空闲,系统就会读取任务队列,将之前异步任务的结果(事件)添加到可执行栈中,开始执行。

    4.1 宏任务

    task(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。

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

    task主要包含:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)

    4.2微任务

    microtask(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。

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

    microtask主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 环境)

    4.3运行机制

    在事件循环中,每进行一次循环操作称为 tick,但关键步骤如下:

    • 执行所有执行栈中的任务(宏任务)
    • 执行栈中执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
    • 当执行栈空了之后,立即执行当前微任务队列中的所有微任务(依次执行)
    • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
    • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
  • 相关阅读:
    Tarjan专题
    Catalan数
    状压DP
    威尔逊定理证明:
    【fzoj 2376】「POJ2503」Babelfish
    Android 源码
    Android实现推送方式解决方案
    Android apk 签名
    圆角的实现
    Android 资源
  • 原文地址:https://www.cnblogs.com/leaf930814/p/6718595.html
Copyright © 2011-2022 走看看