zoukankan      html  css  js  c++  java
  • Evevt Loop 事件循环

    JavaScript 是一门单线程的语言

    也就是说,JS一次只能做一件事情。

    cpu处理指令速度非常快,远比磁盘I/O和网络I/O速度快,所以一些cpu直接执行的任务就成了优先执行主线任务(即同步任务synchronous),然后需要io返回数据的任务就成了等待被执行的任务(即异步任务asynchronous)

    • 同步任务:在主线程上排队执行的任务,前一个任务执行完毕,才能执行后一个任务;
    • 异步任务:不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

    总之:

    只要主线程空了,就会去读取”任务队列”,这就是JavaScript的运行机制
    

    一.什么是event Loop的执行机制

    (1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

    (2)主线程之外,还存在一个"任务队列"(task queue)。异步的任务在event table 里面注册函数,当满足触发条件的时候, 进入event queque(就是事件队列)。

    (3)一旦"执行栈"中的所有同步任务执行完毕,就去task queue里面查看是否有可执行的异步任务。如果,有就推入到主线程的执行栈(stack)。

    (4)主线程不断重复上面的第三步。

    参考上图,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。

    练习 异步任务-setTimeout

    练习1:

    console.log('script start');
    setTimeout(() => {
      console.log('setTimeout');
    }, 1000);
    console.log('script end');
    

    输出顺序:

    script start
    script end
    setTimeout
    

    练习2:

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

    输出顺序:

    script start
    script end
    setTimeout
    

    练习3:

    console.log('script start');
    setTimeout(() => {
      console.log('setTimeout');
    }, 0);
    
    // 具体数字不定,这取决于你的硬件配置和浏览器
    for (let i = 0; i < 999999999; i++) {
      // do something
    }
    
    console.log('script end');
    

    输出顺序:

    script start
    script end
    setTimeout
    

    练习4:

    console.log('1');
    setTimeout(() => {
      console.log('2');
    }, 0);
    setTimeout(() => {
      console.log('3');
    }, 1000);
    console.log('4');
    

    输出顺序:

    1
    4
    2
    3
    

    二 事件队列作用

    由于 JS 是单线程的,同步执行任务会造成浏览器的阻塞,所以我们将 JS 分成一个又一个的任务,通过不停的循环来执行事件队列中的任务。这就使得当我们挂起某一个任务的时候可以去做一些其他的事情,而不需要等待这个任务执行完毕。

    同步任务

    例1:

    window.a = 1;
    function foo() {
    window.a = window.a + 1;
    return foo();
    }

    foo();

    例2:栈溢出(出现死循环):

    function foo() {
      return foo();
    }
    
    foo();
    

    结果:

    例3: 同步阻塞

    例4: 同步阻塞

    应用

    1. ajax请求

    通过事件循环机制,我们则不需要等待 ajax 响应之后再进行工作。我们则是设置一个回调函数,将 ajax 请求挂起,然后继续执行后面的代码,至于请求何时响应,对我们的程序不会有影响,甚至它可能永远也不响应,也不会使浏览器阻塞。而当响应成功了以后,浏览器的事件表则会将回调函数添加至事件队列中等待执行。

    例:


    输出顺序:

    Hi
    JSConfEU
    ajax data
    

    2. 事件监听器

    事件监听器的回调函数也是一个任务,当我们注册了一个事件监听器时,浏览器事件表会进行登记,当我们触发事件时,事件表便将回调函数添加至事件队列当中。

    例:

    3. 同步遍历和异步遍历

    // Synchronous 同步遍历
    [1, 2, 3, 4].forEach((i) => {
      console.log(i);
    });
    
    // Asynchronous 异步遍历
    function asyncForEach(array, cb) {
      array.forEach(() => {
        setTimeout(cb, 0);
      });
    }
    
    asyncForEach([1, 2, 3, 4], (i) => {
      console.log(i);
    });
    

    采用同步遍历的方法,当数组长度上升到3位数的时候,便会出现阻塞,但是异步遍历却不会出现阻塞现象(除非数组长度非常大,那是因为计算机的内存空间不足)。
    这是因为同步遍历方法是一个单独的任务,这个任务会将所有的数组元素遍历一遍,然后才会开始下一个任务。而异步遍历的方法将每一次遍历拆分成一个单独的任务,一个任务只遍历一个数组元素,所以在每个任务之间,我们的浏览器可以进行渲染,所以我们不会看见阻塞的情况。

    4. UI渲染

    DOM 操作会触发浏览器对文档进行渲染,如修改排版规则,修改背景颜色等等,那么这类操作是如何在浏览器当中奏效的?

    至此我们已经知道了事件循环是如何执行的,事件循环器会不停的检查事件队列,如果不为空,则取出队首压入执行栈执行。当一个任务执行完毕之后,事件循环器又会继续不停的检查事件队列,不过在这间,浏览器会对页面进行渲染。这就保证了用户在浏览页面的时候不会出现页面阻塞的情况,这也使 JS 动画成为可能, jQuery 动画在底层均是使用 setTimeout 和 setInterval 来进行实现。

    想象一下如果我们同步的执行动画,那么我们不会看见任何渐变的效果,浏览器会在任务执行结束之后渲染窗口。反之我们使用异步的方法,浏览器会在每一个任务执行结束之后渲染窗口,这样我们就能看见动画的渐变效果了。

    三. Microtasks 和 Macrotasks

    具体到任务队列,又分位 microtasks 和 macrotasks

    属于microtasks的任务有:

    process.nextTick
    promise.then
    Object.observe
    MutationObserver
    

    属于macrotasks的任务有:

    setTimeout
    setInterval
    setImmediate
    I/O
    UI渲染
    

    在执行事件时:

    1. 从script(整体代码)开始第一次循环。执行所有主线程的同步函数,遇到异步函数,分别添加到microtasks和macrotasks任务队列
    2. 同步函数执行后,开始执行异步函数中的任务队列,首先执行所有的micro-task所有的micro-task执行完成后,循环执行macro-task任务, 再次执行micro-task,这样一直循环下去.
    总之:
    整体的js代码在执行完主线程的同步任务,然后有microtask执行microtask,没有microtask执行下一个macrotask,如此往复循环至结束
    

    练习

    练习1:

    setTimeout(() => {
      console.log(4);
    }, 0);
    new Promise(((resolve) => {
      console.log(1);
      for (let i = 0; i < 10000; i++) {
        i == 9999 && resolve();
      }
      console.log(2);
    })).then(() => {
      console.log(5);
    });
    console.log(3);
    

    输出顺序:

    1
    2
    3
    5
    4
    

    练习2

    console.log('start');
    const interval = setInterval(() => {
      console.log('setInterval');
    }, 0);
    setTimeout(() => {
      console.log('setTimeout 1');
      Promise.resolve().then(() => {
        console.log('promise 3');
      }).then(() => {
        console.log('promise 4');
      }).then(() => {
        setTimeout(() => {
          console.log('setTimeout 2');
          Promise.resolve().then(() => {
            console.log('promise 5');
          }).then(() => {
            console.log('promise 6');
          }).then(() => {
            clearInterval(interval);
          });
        }, 0);
      });
    }, 0);
    Promise.resolve().then(() => {
      console.log('promise 1');
    }).then(() => {
      console.log('promise 2');
    });
    

    输出顺序:

    start
    promise 1
    promise 2
    setInterval
    setTimeout 1
    promise 3
    promise 4
    setInterval
    setTimeout 2
    promise 5
    promise 6
    

    总结

    JavaScript 是一门单线程的语言,但是其事件循环的特性使得我们可以异步的执行程序。这些异步的程序也就是一个又一个独立的任务,这些任务包括了 setTimeout、setInterval、ajax、eventListener 等等。关于事件循环,我们需要记住以下几点:

    1. 事件队列严格按照时间先后顺序将任务压入执行栈执行;
    2. 当执行栈为空时,浏览器会一直不停的检查事件队列,如果不为空,则取出第一个任务;
    3. 在每一个任务结束之后,浏览器会对页面进行渲染;

    参考:

    1. 菲利普·罗伯茨:到底什么是Event Loop呢?
    2. whatwg规范
    3. Promise的队列与setTimeout的队列有何关联?
    4. 深入理解 JavaScript 事件循环
  • 相关阅读:
    react className 有多个值时的处理 / react 样式使用 百分比(%) 报错
    更改 vux Tabbar TabbarItem标题下方的文字激活时的颜色
    angular 图片加载失败 情况处理? 如何在ionic中加载本地图片 ?
    angular 资源路径问题
    webpack 项目实战
    百度地图 创建 自定义控件(vue)
    function 之 arguments 、call 、apply
    手写 redux 和 react-redux
    ARC以及MRC中setter方法的差异
    运行时中给一个对象绑定另外一个对象
  • 原文地址:https://www.cnblogs.com/qiqi715/p/10016429.html
Copyright © 2011-2022 走看看