文章原文: https://yq.aliyun.com/article...
本文相对于原文有部分修改node
Node.js以高效,轻量著称,具备非阻塞I/O,事件驱动的特性.
非阻塞I/O很浅显的解释就是: 代码以单线程的方式执行,在遇到I/O操做时Node会开辟新的线程去执行I/O操做,主线程代码继续执行.
事件驱动很浅显的解释就是: 事件产生者发布一个事件,事件订阅者在收到事件后执行某段代码.
但非阻塞I/O,事件驱动究竟是如何实现的呢,它们跟Node.js的单线程有什么关系呢?git
C/C++底层:github
Libuv(docs,GitHub)是Node.js关键的一个组成部分,它为上层js提供了统一的API调用,兼容了平台差别,隐藏了底层实现(它来源于libev,然而libev只能运行于Unix-like系统上。为了可以使Node.js运行在Windows/Unix-like系统上,libuv所以产生了)网络
Network I/O: 网络I/O异步
举一个文件操做的例子来阐述Node.js整个的执行流程async
const fs = require("fs") fs.open("./test.txt", "w", (err, data) => { // TODO });
整个代码的调用过程大体可描述为: lib/fs.js -> src/node_file.cc -> uv_fs
tcp
具体来讲,当咱们调用 fs.open
时,Node.js 经过 process.binding
调用 C/C++ 层面的 Open 函数,而后经过它调用 Libuv 中的具体方法 uv_fs_open
,最后执行的结果经过回调的方式传回,完成流程。在图中,能够看到平台判断的流程,须要说明的是,这一步是在编译的时候已经决定好的,并非在运行时中。函数
整体来讲,咱们在 Javascript 中调用的方法,最终都会经过 process.binding
传递到 C/C++ 层面,最终由他们来执行真正的操做。Node.js 即这样与操做系统进行互动。工具
经过这个过程,咱们能够发现,实际上,Node.js 虽说是用的 Javascript,但只是在开发时使用 Javascript 的语法来编写程序。真正的执行过程仍是由 V8 将 Javascript 解释,而后由 C/C++ 来执行真正的系统调用,因此并不须要过度担忧 Javascript 执行效率的问题。能够看出,Node.js 并非一门语言,而是一个平台.oop
根据上文的铺垫,咱们能够知道真正执行系统操做的是Libuv层,Libuv自己就是异步和事件驱动的,因此,当咱们调用I/O操做时,Libuv开启线程来执行此次I/O操做,执行完成后传回给JavaScript进行后续操做.
这里的I/O包括了文件I/O和网络I/O,这二者的实现并不相同,文件I/O和DNS等操做都是依托线程池(Thread Pool)来实现,而网络I/O(包括TCP,UDP,TTY等)是由epoll,IOCP,kqueue来实现的.
一个异步I/O的流程大致以下:
发起I/O调用
执行回调
从这里,咱们能够看到,咱们其实对 Node.js 的单线程一直有个误会。事实上,它的单线程指的是自身 Javascript 运行环境的单线程,Node.js 并无给 Javascript 执行时建立新线程的能力,最终的实际操做,仍是经过 Libuv 以及它的事件循环来执行的。这也就是为何 Javascript 一个单线程的语言,能在 Node.js 里面实现异步操做的缘由,二者并不冲突。
当咱们写了一大堆事件处理函数后,Libuv 如何来执行这些回调呢?这就提到了咱们以前说到的 uv_run,先看一张它的执行流程图:
在 uv_run
函数中,会维护一系列的监视器(观察者队列):
typedef struct uv_loop_s uv_loop_t; typedef struct uv_err_s uv_err_t; typedef struct uv_handle_s uv_handle_t; typedef struct uv_stream_s uv_stream_t; typedef struct uv_tcp_s uv_tcp_t; typedef struct uv_udp_s uv_udp_t; typedef struct uv_pipe_s uv_pipe_t; typedef struct uv_tty_s uv_tty_t; typedef struct uv_poll_s uv_poll_t; typedef struct uv_timer_s uv_timer_t; typedef struct uv_prepare_s uv_prepare_t; typedef struct uv_check_s uv_check_t; typedef struct uv_idle_s uv_idle_t; typedef struct uv_async_s uv_async_t; typedef struct uv_process_s uv_process_t; typedef struct uv_fs_event_s uv_fs_event_t; typedef struct uv_fs_poll_s uv_fs_poll_t; typedef struct uv_signal_s uv_signal_t;
这些监视器都有对应着一种异步操做,它们经过 uv_TYPE_start
,来注册事件监听以及相应的回调。
在 uv_run
执行过程当中,它会不断的检查这些队列中是或有 pending
状态的事件,有则触发,并且它在这里只会执行一个回调,避免在多个回调调用时发生竞争关系,由于 Javascript 是单线程的,没法处理这种状况。
上面的图中,对 I/O 操做的事件驱动,表达的比较清楚。除了咱们常提到的 I/O 操做,图中还表述了一种状况,timer(定时器)。它与其余二者不一样之处在于,它没有单独开立新的线程,而是在事件循环中直接完成的。
事件循环除了维护那些观察者队列,还维护了一个 time
字段,在初始化时会被赋值为0,每次循环都会更新这个值。全部与时间相关的操做,都会和这个值进行比较,来决定是否执行。
在图中,与 timer
相关的过程以下: