zoukankan      html  css  js  c++  java
  • Node.js 事件循环

    本文地址 http://www.cnblogs.com/jasonxuli/p/6074231.html
     
     
     
    >>> 文末有简单总结
     
     
     
    什么是事件循环(Event Loop)
     
    事件循环能让 Node.js 执行非阻塞 I/O 操作 -- 尽管JavaScript事实上是单线程的 -- 通过在可能的情况下把操作交给操作系统内核来实现。
     
    由于大多数现代系统内核是多线程的,内核可以处理后台执行的多个操作。当其中一个操作完成的时候,内核告诉 Node.js,相应的回调就被添加到轮询队列(poll queue)并最终得到执行。本主题随后会解释更多相关细节。
     
     
    事件循环
     
    Node.js 开始的时候会初始化事件循环,处理目标脚本,脚本可能会进行异步API调用、定时任务或者process.nextTick(),然后开始进行事件循环。
     
    下面的表格简要描述了事件循环的操作顺序。
     
         ┌───────────────────────┐
    ┌─> │        timers                        │
    │   └──────────┬────────────┘
    │   ┌──────────┴────────────┐
    │   │     I/O callbacks                    │
    │   └──────────┬────────────┘
    │   ┌──────────┴────────────┐
    │   │     idle, prepare                    │
    │   └──────────┬────────────┘      ┌───────────────┐
    │   ┌──────────┴────────────┐      │   incoming:             │
    │   │         poll                         │<───┤  connections,           │
    │   └──────────┬────────────┘      │   data, etc.            │
    │   ┌──────────┴────────────┐      └───────────────┘
    │   │        check                         │
    │   └──────────┬────────────┘
    │   ┌──────────┴────────────┐
    └──┤    close callbacks                   │
             └──────────────────────———————─┘
    注:每个方框代表事件循环中的一个阶段。
     
    每个阶段都有一个需要执行的回调函数的先入先出(FIFO)队列。同时,每个阶段都是特殊的,基本上,当事件循环进行到某个阶段时,会执行该阶段特有的操作,然后执行该阶段队列中的回调,直到队列空了或者达到了执行次数限制。这时候,事件循环会进入下一个阶段,循环往复。
     
    由于这些操作可能产生更多的计划任务操作,并且轮询阶段处理的新事件会被加入到内核的队列,轮询事件被处理的时候会有新的轮询事件加入。于是,长时回调任务会导致轮询阶段的时间超过了定时器的阈值。 详情见 定时器(timers)和轮询(poll)部分。
     
    注:Windows 和 Unix/Linux 的实现有轻微的矛盾之处,但并不影响刚才的描述。 最重要的部分都有了。实际上有七八个阶段,但我们关注的 -- Node.js 实际使用的 -- 就是上面这些。
     
     
    阶段总览 (Phases Overview)
     
    • 计时器(timers):本阶段执行setTimeout() 和 setInterval() 计划的回调;
    • I/O 回调: 执行几乎全部发生异常的 close 回调, 由定时器和setImmediate()计划的回调;
    • 空闲,预备(idle,prepare):只内部使用;
    • 轮询(poll): 获取新的 I/O 事件;nodejs这时会适当进行阻塞;
    • 检查(check): 调用 setImmediate() 的回调;
    • close callbacks: 例如 socket.on('close', ... );
     
    在事件循环运行之间,Node.js 检查是否有正在等待的异步I/O 或者定时器,如果没有就清除并结束。
     
     
    阶段细节
     
    定时器(timers)
     
    定时器的用途是让指定的回调函数在某个阈值后会被执行,具体的执行时间并不一定是那个精确的阈值。定时器的回调会在制定的时间过后尽快得到执行,然而,操作系统的计划或者其他回调的执行可能会延迟该回调的执行。
     
    注:从技术上来看,轮询阶段控制了定时器的执行时机。
     
    例如,你设定了在100ms后执行某个操作,然后脚本开始执行一个需要95ms的文件读取操作:
     
    var fs = require('fs');
    
    function someAsyncOperation (callback) {
      // Assume this takes 95ms to complete
      fs.readFile('/path/to/file', callback);
    }
    
    var timeoutScheduled = Date.now();
    
    setTimeout(function () {
    
      var delay = Date.now() - timeoutScheduled;
    
      console.log(delay + "ms have passed since I was scheduled");
    }, 100);
    
    
    // do someAsyncOperation which takes 95 ms to completesomeAsyncOperation(function () {
    
      var startCallback = Date.now();
    
      // do something that will take 10ms...
      while (Date.now() - startCallback < 10) {
        ; // do nothing
      }
    
    });
     
    当事件循环进入轮询阶段时,队列是空的(fs.readFile()还没完成),因此时间会继续流逝知道最快的定时器需要执行。过了95ms后,fs.readFile() 读完文件了,它的回调被添加到轮询队列,这个回调需要执行10ms。等到这个回调执行完,队列中没有回调了,这时事件循环看到了最近到时的定时器,然后回到定时器阶段(timers phase)来执行之前的定时器回调。
    在这个例子中,从定义定时器到回调执行中间过了105ms。
     
    注:为了防止轮询阶段持续时间太长,libuv 会根据操作系统的不同设置一个轮询的上限。
     
     
    I/O callbacks
     
    这个阶段执行一些诸如TCP错误之类的系统操作的回调。例如,如果一个TCP socket 在尝试连接时收到了 ECONNREFUSED错误,某些 *nix 系统会等着报告这个错误。这个就会被排到本阶段的队列中。
     
     
    轮询(poll)
     
    轮询阶段有两个主要功能:
    1,执行已经到时的定时器脚本,然后
    2,处理轮询队列中的事件。
     
    当事件循环进入到轮询阶段却没有发现定时器时:
    • 如果轮询队列非空,事件循环会迭代回调队列并同步执行回调,直到队列空了或者达到了上限(前文说过的根据操作系统的不同而设定的上限)。
    • 如果轮询队列是空的:
      • 如果有setImmediate()定义了回调,那么事件循环会终止轮询阶段并进入检查阶段去执行定时器回调;
      • 如果没有setImmediate(),事件回调会等待回调被加入队列并立即执行。
     
    一旦轮询队列空了,事件循环会查找已经到时的定时器。如果找到了,事件循环就回到定时器阶段去执行回调。
     
     
    检查(check)
     
    这个阶段允许回调函数在轮询阶段完成后立即执行。如果轮询阶段空闲了,并且有回调已经被 setImmediate() 加入队列,事件循环会进入检查阶段而不是在轮询阶段等待。
     
    setImmediate() 是个特殊的定时器,在事件循环中一个单独的阶段运行。它使用libuv的API 来使得回调函数在轮询阶段完成后执行。
     
    基本上,随着代码的执行,事件循环会最终进入到等待状态的轮询阶段,可能是等待一个连接、请求等。然而,如果有一个setImmediate() 设置了一个回调并且轮询阶段空闲了,那么事件循环会进入到检查阶段而不是等待轮询事件。   ---- 这车轱辘话说来说去的
     
     
    关闭事件的回调(close callbacks)
     
    如果一个 socket 或句柄(handle)被突然关闭(is closed abruptly),例如 socket.destroy(), 'close' 事件会被发出到这个阶段。否则这种事件会通过 process.nextTick() 被发出。
     
     
    setImmediate() vs setTimeout()
     
    这两个很相似,但调用时机会的不同会导致它们不同的表现。
     
    • setImmediate() 被设计成一旦轮询阶段完成就执行回调函数;
    • setTimeout() 规划了在某个时间值过后执行回调函数;
     
    这两个执行的顺序会因为它们被调用时的上下文而有所不同。如果都是在主模块调用,那么它们会受到进程性能的影响(运行在本机的其他程序会影响它们)。
     
    例如,如果我们在非 I/O 循环中运行下面的脚本(即在主模块中),他俩的顺序是不固定的,因为会受到进程性能的影响:
     
    // timeout_vs_immediate.jssetTimeout(function timeout () {
      console.log('timeout');
    },0);
    
    setImmediate(function immediate () {
      console.log('immediate');
    });
    $ node timeout_vs_immediate.js
    timeout
    immediate

    $ node timeout_vs_immediate.js
    immediate
    timeout
    但是如果把它们放进 I/O 循环中,setImmediate() 的回调总是先执行:
     
    // timeout_vs_immediate.jsvar fs = require('fs')
    
    fs.readFile(__filename, () => {
      setTimeout(() => {
        console.log('timeout')
      }, 0)
      setImmediate(() => {
        console.log('immediate')
      })
    })
    $ node timeout_vs_immediate.js
    immediate
    timeout

    $ node timeout_vs_immediate.js
    immediate
    timeout
     
    setImmediate() 比 setTimeout() 优势的地方是 setImmediate() 在 I/O 循环中总是先于任何定时器,不管已经定义了多少定时器。
     
     
    process.nextTick()
     
    理解 process.nextTick()
     
    你可能已经注意到了 process.nextTick() 没有在上面那个表格里出现,虽然它确实是一个异步API。这是因为它技术上不属于事件循环。然而,nextTickQueue 会在当前操作结束后被处理,不管是在事件循环的哪个阶段。
     
    回头看看之前那个表格,你在某个阶段的任何时候调用它,它的所有回调函数都会在事件循环继续进行之前得到处理。有时候这会导致比较糟糕的情况,因为它允许你用递归调用的方式去“阻塞” I/O,这会让事件循环无法进入到轮询阶段。
     
    为什么要允许这样
     
    部分是因为 Node.js 的设计哲学:API 应该总是异步的,即使本不需要是异步。
     
    blablabla,后面几段看的我有点尴尬+晕。既尴尬又晕是觉得这几段说的有点啰嗦,而且举的例子不合适。例子要么是同步的,不是异步的。要么是例子里的写法完全可以避免,比如应该先添加 'connect' 事件监听再进行 .connect() 操作;又或者变量声明最好放在变量使用之前,可以避免变量的提前声明和当时赋值的麻烦。
     
    难道是我没理解里面的秘辛?
     
     
    process.nextTick() vs setTimeout()
     
    这两个函数有些相似但是名字让人困惑:
    • process.netxtTick() 在事件循环的当前阶段立即生效;
    • setImmediate() 生效是在接下来的迭代或者事件循环的下一次tick;
     
    本质上,它们的名字应该互换一下。process.nextTick() 比 setImmediate() 更“立刻”执行,但这是个历史问题没法改变。如果改了,npm上大堆的包就要挂了。
     
    我们推荐开发者在所有情况下都使用 setImmediate() 因为它更显而易见(reason about),另外兼容性也更广,例如浏览器端。
     
    为什么使用 process.nextTick() 
     
    有两大原因:
     
    1. 允许用户处理错误,清理不需要的资源,或许在事件循环结束前再次尝试发送请求;
    2. 必须让回调函数在调用栈已经清除(unwound)后并且事件循环继续下去之前执行;
     
    下面的两个例子都是类似的,即在 line1 派发事件,却在 line2 才添加监听,因此监听的回调是不可能被执行到的。
    于是可以用 process.nextTick() 使得当前调用栈先执行完毕,也即先执行 line2 注册事件监听,然后在 nextTick 派发事件。
     
    const EventEmitter = require('events');
    const util = require('util');
    
    function MyEmitter() {
      EventEmitter.call(this);
    
      // use nextTick to emit the event once a handler is assigned
      process.nextTick(function () {
        this.emit('event');
      }.bind(this));
    }
    util.inherits(MyEmitter, EventEmitter);
    
    const myEmitter = new MyEmitter();
    myEmitter.on('event', function() {
      console.log('an event occurred!');
    });
     
     
     
    翻译总结:
     
    这篇文章写的不太简练,也可能为了有更多的受众吧,我感觉车轱辘话比较多,一个意思要说好几遍。
     
    从编程应用的角度简单来说:
     
    Node.js 中的事件循环大概有七八个阶段,每个阶段都有自己的队列(queue),需要等本阶段的队列处理完成后才进入其他阶段。阶段之间会互相转换,循环顺序并不是完全固定的 ,因为很多阶段是由外部的事件触发的。
     
    其中比较重要的是三个:
     
    1. 定时器阶段 timers:
      定时器阶段执行定时器任务(setTimeOut(), setInterval())。
    2. 轮询阶段 poll:
              轮询阶段由 I/O 事件触发,例如 'connect','data' 等。这是比较重/重要的阶段,因为大部分程序功能就是为了 I/O 数据。
              本阶段会处理定时器任务和 poll 队列中的任务,具体逻辑:
      • 处理到期的定时器任务,然后
      • 处理队列任务,直到队列空了或者达到上限
      • 如果队列任务没了:
        • 如果有 setImmediate(),终止轮询阶段并进入检查阶段去执行;
        • 如果没有 setImmediate(),那么就查看有没有到期的定时器,有的话就回到定时器阶段执行回调函数;
    1. 检查阶段 check:
              当轮询阶段空闲并且已经有 setImmediate() 的时候,会进入检查阶段并执行。
     
    比较次要但也列在表格中的两个:
     
    1. I/O 阶段:
              本阶段处理 I/O 异常错误;
    1. 'close'事件回调:
              本阶段处理各种 'close' 事件回调;
     
    关于 setTimeout(), setImmediate(), process.nextTick():
     
    • setTimeout()           在某个时间值过后尽快执行回调函数;
    • setImmediate()       一旦轮询阶段完成就执行回调函数;
    • process.nextTick()   在当前调用栈结束后就立即处理,这时也必然是“事件循环继续进行之前” ;
     
    优先级顺序从高到低: process.nextTick() > setImmediate() > setTimeout()
    注:这里只是多数情况下,即轮询阶段(I/O 回调中)。比如之前比较 setImmediate() 和 setTimeout() 的时候就区分了所处阶段/上下文。
     
     
    另:
     
    关于调用栈,事件循环还可以参考这篇文章:
     
    这篇文章里对事件任务区分了大任务(macro task) 、小任务(micro task),每个事件循环只处理一个大任务 ,但会处理完所有小任务。
    这一点和前面的文章说的不同。

    examples of microtasks:

    • process.nextTick
    • promises
    • Object.observe

    examples of macrotasks:

    • setTimeout
    • setInterval
    • setImmediate
    • I/O
     
  • 相关阅读:
    Vue基础简介
    Vue基础简介
    django生命周期请求l流程图
    CSRF与auth模块
    cookie与session django中间件
    Django forms组件与钩子函数
    ajax结合sweetalert实现删除按钮动态效果
    ajax数据交互
    如何绕过CDN找源站ip
    IP地址的另一种形式---一种隐藏IP的方法
  • 原文地址:https://www.cnblogs.com/jasonxuli/p/6074231.html
Copyright © 2011-2022 走看看