最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 事件循环(Event Loop)

    正文概述 掘金(Korey)   2021-02-24   839

    javascript 从诞生之日起就是一门单线程的非阻塞的脚本语言。而非阻塞则是当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如 I/O 事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。到底是如何实现非阻塞这一点呢?答案就是 事件循环(Event Loop)

    当函数被调用时,会被添加到 调用栈 栈中的顶部,执行完成之后就从栈顶部移出该函数,直到栈内被清空。每次栈内被清空都会去读取 任务队列 有没有任务,有就按照顺序读取执行。如果这个时候栈中又出现了事件,该事件又去调用了 WebAPIs 里的异步方法,那这些异步方法会在再被调用的时候放在 任务队列 里,一直循环读取-执行的操作,就形成了 事件循环

    调用栈\执行栈(call stack) 是一种 后进先出(LIFO) 的数据结构,任务队列先进先出(FIFO) 的数据结构。

    任务队列

    事件循环是通过 任务队列(task queue) 的机制来进行协调的。一个事件循环中,可以有一个或者多个任务队列,一个任务队列便是一系列有序任务 task 的集合,每个任务都有一个任务源 task source,源自同一个任务源的 task 必须放到同一个任务队列,从不同源来的则被添加到不同队列。

    JavaScript 单线程中的任务分为 同步任务异步任务。同步任务会在 调用栈 中按照顺序排队等待主线程执行,异步任务则会在异步有了结果后将注册的回调函数添加到 任务队列(消息队列) 中等待主线程空闲(即栈内被清空)的时候读取到栈中等待主线程执行。

    异步任务队列可分为 task(macrotask) 宏任务队列和 microtask(job) 微任务队列两类,不同的 API 注册的异步任务会依次进入自身对应的队列中,然后等待 Event Loop 将它们依次压入执行栈中执行,宏任务队列可以有多个,微任务队列只有一个。

    • 宏任务主要包含:script(整体代码)setTimeoutsetIntervalI/OUI交互事件postMessageMessageChannelsetImmediate(Node.js 环境)requestAnimationFrame
    • 微任务主要包含:Promise的方法及其派生MutationObserver(浏览器)Object.observe(已废弃)查阅

    浏览器中的事件循环

    流程

    在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:

    • 在此次 tick 中选择最先进入队列的任务(oldest task),如果有则执行(一个),如果执行中有异步任务就放至各自的队列中
    • 检查是否存在 Microtasks,如果存在则不停地执行,直至清空 Microtasks Queue
    • 更新 render(update rendering)(GUI 线程)
    • 取出下一个宏任务 task,主线程重复执行上述步骤(回到 JS 线程)

    事件循环(Event Loop)

    相关点

    await

    await 将直接使用 Promise.resolve() 相同语义查阅,即:

    async function async1() {
      await async2();
      console.log('async1 end');
    }
    //等价于
    function async1() {
      Promise.resolve(async2()).then(() => {
        console.log('async1 end');
      });
    }
    //等价于
    function async1() {
      new Promise((resolve) => {
        resolve(async2());
      }).then(() => {
        console.log('async1 end');
      });
    }
    

    update rendering(视图渲染)

    update rendering(视图渲染)发生在本轮事件循环的 microtask 队列被执行完之后,也就是说执行任务的耗时会影响视图渲染的时机。通常浏览器以每秒 60 帧(60fps)的速率刷新页面,这个帧率最适合人眼交互,大概 16.7ms 渲染一帧,所以如果要让用户觉得顺畅,单个 macrotask 及它相关的所有 microtask 最好能在 16.7ms 内完成。

    也不是每轮事件循环都会执行 update rendering,浏览器有自己的优化策略,可能把几次的视图更新累积到一起重绘。重绘之前会通知 requestAnimationFrame 执行回调函数,即requestAnimationFrame 的执行时机是在一次或多次事件循环的 UI render 阶段。查阅 1,查阅 2

    life of a frame

    浏览器页面是一帧一帧绘制出来的,每一帧(Frame)都需要完成哪些工作?

    1. Input event:处理用户的交互,如点击、触碰、滚动等事件
    2. JSJS 解析执行(可能有多个事件循环)
    3. Begin frame:帧开始。窗口尺寸变更,页面滚动等的处理
    4. rAfrequestAnimationFrame
    5. Layout:布局
    6. Paint: 绘制

    事件循环(Event Loop)

    上面六个步骤完成后没超过 16 ms,说明时间有富余,此时就会执行 requestIdleCallback 里注册的任务。

    requestAnimationFrame & requestIdleCallback

    • requestAnimationFrame: 告诉浏览器在下次重绘之前执行传入的回调函数(通常是用于操纵 dom,更新动画的函数);由于是每帧执行一次,那结果就是每秒的执行次数与浏览器屏幕刷新次数一样,通常是每秒 60 次。

    • requestIdleCallback: 会在浏览器空闲时间执行回调,也就是允许开发人员在主事件循环中执行低优先级任务,而不影响一些延迟关键事件。如果有多个回调,会按照先进先出原则执行;但是当传入了 timeout,为了避免超时,有可能会打乱这个顺序;由于它发生在一帧的最后,此时页面布局已经完成,所以不建议在 requestIdleCallback 里再操作 DOM,这样会导致页面再次重绘。

    例子:

    // 一个sleep函数,模拟阻塞
    function sleep(d) {
        for (var t = Date.now(); Date.now() - t <= d;);
    }
    let count = 0;
    function callself(){
        console.log(++count, 'frame')
        sleep(16)
        if(count<20){
            window.requestAnimationFrame(callself);
        }
    }
    // 当count<20时候,就一直使用raf占满16ms,这样模拟一帧中无空闲时间
    window.requestAnimationFrame(callself);
    
    function cb1({didTimeout}){
        console.log('idle cb1', didTimeout)
    }
    function cb2({didTimeout}){
        console.log('idle cb2', didTimeout)
    }
    function cb3({didTimeout}){
        console.log('idle cb3', didTimeout)
    }
    
    // 注册三个rIC回调,正常是按照先进先出原则执行这三个回调,当设置的有timeout,该回调会被提前
    window.requestIdleCallback(cb1)
    window.requestIdleCallback(cb2)
    window.requestIdleCallback(cb3, {
        timeout: 30
    })
    

    浏览器举例

    通过例子加深对浏览器事件循环执行顺序的理解:

    eg1:

    Promise.resolve().then(function promise1() {
      console.log('promise1');
    });
    setTimeout(function setTimeout1() {
      console.log('setTimeout1');
      Promise.resolve().then(function promise2() {
        console.log('promise2');
      });
    }, 0);
    
    setTimeout(function setTimeout2() {
      console.log('setTimeout2');
    }, 0);
    

    eg2:

    解析查阅

    new Promise((resolve) => {
      resolve(1);
      Promise.resolve().then(() => console.log(2));
      console.log(4);
    }).then((t) => console.log(t));
    console.log(3);
    

    eg3:

    解析查阅 参照

    new Promise((resolve) => {
      resolve(1);
      Promise.resolve({
        then: function (resolve, reject) {
          console.log(2);
          resolve(3);
        },
      }).then((t) => console.log(t));
      console.log(4);
    }).then((t) => console.log(t));
    console.log(5);
    

    eg4:

    async function async1() {
      console.log('async1 start');
      await async2();
      console.log('async1 end');
    }
    async function async2() {
      console.log('async2');
    }
    console.log('script start');
    setTimeout(function () {
      console.log('setTimeout');
    }, 0);
    async1();
    new Promise(function (resolve) {
      console.log('promise1');
      resolve();
    })
      .then(function () {
        console.log('promise2');
      })
      .then(function () {
        console.log('promise3');
      });
    console.log('script end');
    

    eg5:

    console.log('start');
    
    var intervalA = setInterval(() => {
      console.log('intervalA');
    }, 0);
    
    setTimeout(() => {
      console.log('timeout');
    
      clearInterval(intervalA);
    }, 0);
    
    var intervalB = setInterval(() => {
      console.log('intervalB');
    }, 0);
    
    var intervalC = setInterval(() => {
      console.log('intervalC');
    }, 0);
    
    new Promise((resolve, reject) => {
      console.log('promise');
    
      for (var i = 0; i < 10000; ++i) {
        i === 9999 && resolve();
      }
    
      console.log('promise after for-loop');
    })
      .then(() => {
        console.log('promise1');
      })
      .then(() => {
        console.log('promise2');
    
        clearInterval(intervalB);
      });
    
    new Promise((resolve, reject) => {
      setTimeout(() => {
        console.log('promise in timeout');
        resolve();
      });
    
      console.log('promise after timeout');
    })
      .then(() => {
        console.log('promise4');
      })
      .then(() => {
        console.log('promise5');
    
        clearInterval(intervalC);
      });
    
    Promise.resolve().then(() => {
      console.log('promise3');
    });
    
    console.log('end');
    

    eg6:

    解析查阅

    <script>
      console.log('start');
    
      setTimeout(() => {
        console.log('timeout1');
      }, 0);
    
      Promise.resolve().then(() => {
        console.log('promise1');
      });
    </script>
    <script>
      setTimeout(() => {
        console.log('timeout2');
      }, 0);
    
      requestAnimationFrame(() => {
        console.log('requestAnimationFrame');
      });
    
      Promise.resolve().then(() => {
        console.log('promise2');
      });
    
      console.log('end');
    </script>
    <!-- 输出:start promise1 end promise2 requestAnimationFrame timeout1 timeout2  -->
    

    NODE 中的事件循环(适用于 NODE 11 以下)

    Node.js 采用 V8 作为 js 的解析引擎,而事件循环方面使用了自己设计的 libuvNode.js 的事件循环核心对应 libuv 中的 uv_run 函数,整个事件循环迭代就是一个 while 无限循环。

    事件循环模型

       ┌───────────────────────┐
    ┌─>│        timers         │ 执行到期的 `setTimeout` 和 `setInterval` 回调
    │  └──────────┬────────────┘
    │  ┌──────────┴────────────┐
    │  │     I/O callbacks     │ 执行到期的一些被延迟调用的 `I/O` 回调
    │  └──────────┬────────────┘
    │  ┌──────────┴────────────┐
    │  │     idle, prepare     │ 仅 `node` 内部使用
    │  └──────────┬────────────┘      ┌───────────────┐
    │  ┌──────────┴────────────┐      │   incoming:   │
    │  │         poll          │<──connections───     │ 立即执行大部分 `I/O` 回调
    │  └──────────┬────────────┘      │   data, etc.  │
    │  ┌──────────┴────────────┐      └───────────────┘
    │  │        check          │ 执行到期的 `setImmediate` 的回调
    │  └──────────┬────────────┘
    │  ┌──────────┴────────────┐
    └──┤    close callbacks    │ 执行注册 `close` 事件的回调,如 `socket` 等
       └───────────────────────┘
    

    其中外部输入数据则从 poll 阶段开始。

    • event loop 总是要经历以上阶段,由 timer 阶段开始,由 close 回调函数阶段结束。
    • event loop 的每个阶段都有一个任务队列。
    • event loop 到达某个阶段时,将执行该阶段的任务队列,该任务队列完成后执行 nextTick队列,然后执行 微任务队列。直到队列清空或执行的回调达到系统上限后,才会转入下一个阶段。
    • 当所有阶段被顺序执行一次后,称 event loop 完成了一个 tick

    事件循环(Event Loop)

    poll

    poll 是一个至关重要的阶段,会做两件事情:

    • 计算当前轮询需要阻塞后续阶段(即维持)的时间:由后续 tick 各个阶段是否存在不为空的回调函数队列最近的计时器时间节点 决定。若所有队列为空且不存在任何计时器,那么事件循环将 无限制地维持在 poll 阶段。其中
      • 对于事件循环部分属性而言:
        • uv_stop() 函数标记为停止时,不阻塞;
        • 不处于活动状态时且不存在活动的 request 时,不阻塞;
        • idle 句柄队列不为空时,不阻塞;
        • I/O callbacks 回调队列不为空时,不阻塞;
        • closing 句柄不为空时,不阻塞;
      • 对于计时器而言:
        • 若不存在任何计时器(setTimeout/setInterval),那么当前事件循环中的 poll 阶段将一直阻塞
        • 若最近计时器时间节点<=开始时间,则表明在计时器二叉最小堆中至少存在一个过期的计时器,那么当前 poll 阶段的超时时间将被设置为 0 即不阻塞。这是为了尽可能快的进入下一阶段,即尽可能快地结束当前事件循环。
        • 若最近计时器时间节点>开始时间,poll 将根据此差值来阻塞当前阶段,阻塞是为了保持在该阶段从而尽可能快的处理异步 I/O 事件。
    • 处理 poll 队列的事件回调(事件循环 tick 总有一种维持 poll 状态的倾向,为了尽可能快的处理随时可能到来异步 I/O 事件)

    如果 poll 阶段进入 idle 状态并且存在 setImmediate,那么 poll 阶段将打破无限制的等待状态,并进入 check 阶段执行 setImmediate

    node 内置定时器

    setTimeout(() => {
      //一些代码;
    }, timeout);
    

    nodejs 中所有计时器是通过一个双向链表实现关联。有且仅有两种计时器:setTimeout/setIntervalsetImmediate。同浏览器一致,所有的计时器实现都不能保证在到达时间阈值后回调函数一定会被立即执行,它们只能保证在到达时间阈值后,尽快执行由计时器注册的回调函数。

    所有计时器在 libuv 中是以计时器回调函数的执行时间节点(即 time + timeout,而不是计时器时间阈值(上述代码里的timeout))构成的二叉最小堆结构来存储。通过二叉最小堆的根节点来获取时间线上最近的 timer 对应的回调函数的句柄,再通过该句柄对应的 timeout 值获取最近的计时器的执行时间节点。

    时间阈值 timeout 的取值范围是 1 ~ 231-1 ms,且为整数。所有超出时间阈值范围的时间阈值都会被重置为 1ms,且所有非整数值会被转换为 整数值。即 setTimeout(callback, 0) 会自动转为 setTimeout(callback, 1)

    node 注意点

    • process.nextTick(): 这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当 每个阶段 完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且 优先于其他 microtask 执行
    • setTimeout(callback, 0)setImmediate(callback) 的执行顺序是随机的,跟代码执行时间与 1ms 大小比较有关。而上述代码在 I/0 callbacks 阶段调用则执行顺序是 setImmediate 在前,setTimeout 在后(第二轮)。

    node 举例

    根据以上知识点,以下这些例子就很容易理解了:

    eg1:

    const fs = require('fs');
    
    fs.readFile('test.txt', () => {
      console.log('readFile');
      setTimeout(() => {
        console.log('timeout');
      }, 0);
      setImmediate(() => {
        console.log('immediate');
      });
    });
    

    eg2:

    console.log('start');
    setTimeout(() => {
      console.log('timer1');
      Promise.resolve().then(function () {
        console.log('promise1');
      });
    }, 0);
    setTimeout(() => {
      console.log('timer2');
      Promise.resolve().then(function () {
        console.log('promise2');
      });
    }, 0);
    Promise.resolve().then(function () {
      console.log('promise3');
    });
    console.log('end');
    

    eg3:

    setTimeout(() => {
      console.log('timer1');
      Promise.resolve().then(function () {
        console.log('promise1');
      });
    }, 0);
    process.nextTick(() => {
      console.log('nextTick');
      process.nextTick(() => {
        console.log('nextTick');
        process.nextTick(() => {
          console.log('nextTick');
          process.nextTick(() => {
            console.log('nextTick');
          });
        });
      });
    });
    

    eg4:

    function sleep(time) {
      let startTime = new Date();
      while (new Date() - startTime < time) {}
      console.log('1s over');
    }
    setTimeout(() => {
      console.log('setTimeout - 1');
      setTimeout(() => {
        console.log('setTimeout - 1 - 1');
        sleep(1000);
      });
      new Promise((resolve) => resolve()).then(() => {
        console.log('setTimeout - 1 - then');
        new Promise((resolve) => resolve()).then(() => {
          console.log('setTimeout - 1 - then - then');
        });
      });
      sleep(1000);
    });
    
    setTimeout(() => {
      console.log('setTimeout - 2');
      setTimeout(() => {
        console.log('setTimeout - 2 - 1');
        sleep(1000);
      });
      new Promise((resolve) => resolve()).then(() => {
        console.log('setTimeout - 2 - then');
        new Promise((resolve) => resolve()).then(() => {
          console.log('setTimeout - 2 - then - then');
        });
      });
      sleep(1000);
    });
    

    node 11 版本后

    和浏览器趋同,都是每执行一个宏任务就执行完微任务队列,故上面例子在不同版本表现不一致。查阅 1,查阅 2

    两者循环区别(NODE 11 之前)

    • 在浏览器中,事件循环是由 macrotask、microtask 组成,执行顺序是 macrotask->microtask
    • Node.js 中,事件循环由多个 阶段 phase多个回调函数队列 callbacks queues 组成。在每一个阶段执行顺序是 macrotask->nextTick->microtask

    其他

    • 从 Chrome 源码看事件循环
    • 从 libuv 看 nodejs 事件循环

    起源地下载网 » 事件循环(Event Loop)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元