├── .gitignore ├── libuv-simple ├── libuv-simple │ ├── text.txt │ └── main.c └── libuv-simple.xcodeproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── zf.xcuserdatad │ │ └── UserInterfaceState.xcuserstate │ ├── xcuserdata │ └── zf.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist │ └── project.pbxproj ├── fz.js ├── docs ├── assets │ ├── now.png │ ├── origin.png │ ├── time-wheel.png │ └── diifer-socket.png ├── leftSider.md ├── index.html ├── v8.md ├── modulejs.md ├── modulejs2.md ├── timer.md ├── cluster.md ├── process.md ├── modulec++.md ├── a-nextTick.md ├── stream.md └── eventloop-libuv.md ├── n.js ├── package.json ├── binding.gyp ├── node.test.js ├── README.md ├── .vscode └── launch.json ├── hello.cc ├── writable.js └── readable.js /.gitignore: -------------------------------------------------------------------------------- 1 | Release -------------------------------------------------------------------------------- /libuv-simple/libuv-simple/text.txt: -------------------------------------------------------------------------------- 1 | i m file 2 | -------------------------------------------------------------------------------- /fz.js: -------------------------------------------------------------------------------- 1 | function Add(a, b) { 2 | return a + b 3 | } 4 | 5 | module.exports = Add 6 | -------------------------------------------------------------------------------- /docs/assets/now.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foveluy/Fz-node/HEAD/docs/assets/now.png -------------------------------------------------------------------------------- /n.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | fs.createReadStream('./README.md').pipe(process.stdout) -------------------------------------------------------------------------------- /docs/assets/origin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foveluy/Fz-node/HEAD/docs/assets/origin.png -------------------------------------------------------------------------------- /docs/assets/time-wheel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foveluy/Fz-node/HEAD/docs/assets/time-wheel.png -------------------------------------------------------------------------------- /docs/assets/diifer-socket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foveluy/Fz-node/HEAD/docs/assets/diifer-socket.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fz-node", 3 | "version": "", 4 | "description": "基于8.9.3版本的源码阅读、解析" 5 | } 6 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "addon", 5 | "sources": [ "hello.cc" ] 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /node.test.js: -------------------------------------------------------------------------------- 1 | setImmediate(()=>{ 2 | console.log('setImmediate') 3 | }) 4 | 5 | process.nextTick(()=>{ 6 | console.log('nextTick') 7 | }) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js 源码解析 2 | 3 | 基于8.9.3版本的源码阅读、解析 4 | 5 | 6 | # 本书地址 7 | 8 | - [书地址](https://215566435.github.io/Fz-node/) 9 | - [本书基于TrumpDoc](https://github.com/215566435/TrumpDoc) 10 | 11 | 12 | 13 | # 协议 14 | 15 | MIT 16 | -------------------------------------------------------------------------------- /libuv-simple/libuv-simple.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /libuv-simple/libuv-simple.xcodeproj/project.xcworkspace/xcuserdata/zf.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foveluy/Fz-node/HEAD/libuv-simple/libuv-simple.xcodeproj/project.xcworkspace/xcuserdata/zf.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /docs/leftSider.md: -------------------------------------------------------------------------------- 1 | * [Libuv事件循环](eventloop-libuv.md) 2 | * [timer](timer.md) 3 | * [多进程架构:process](process.md) 4 | * [多进程架构:cluster](cluster.md) 5 | * [模块化c++层](modulec++.md) 6 | * [模块化js层](modulejs.md) 7 | * [模块化js层2](modulejs2.md) 8 | * [Stream模块](stream.md) 9 | * [附录:nextTick实现和优化](a-nextTick.md) 10 | * [本书Github](https://github.com/215566435/Fz-node) -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "启动程序", 11 | "program": "${file}" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /libuv-simple/libuv-simple.xcodeproj/xcuserdata/zf.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | libuv-simple.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /hello.cc: -------------------------------------------------------------------------------- 1 | // hello.cc 2 | #include 3 | 4 | namespace demo { 5 | 6 | using v8::FunctionCallbackInfo; 7 | using v8::Isolate; 8 | using v8::Local; 9 | using v8::Object; 10 | using v8::String; 11 | using v8::Value; 12 | 13 | void Method(const FunctionCallbackInfo& args) { 14 | Isolate* isolate = args.GetIsolate(); 15 | args.GetReturnValue().Set(String::NewFromUtf8(isolate, "world")); 16 | } 17 | 18 | void init(Local exports) { 19 | NODE_SET_METHOD(exports, "hello", Method); 20 | } 21 | 22 | NODE_MODULE(NODE_GYP_MODULE_NAME, init) 23 | 24 | } // namespace demo -------------------------------------------------------------------------------- /writable.js: -------------------------------------------------------------------------------- 1 | const Writable = require('stream').Writable 2 | 3 | const writable = Writable() 4 | // 实现`_write`方法 5 | // 这是将数据写入底层的逻辑 6 | writable._write = function(data, enc, next) { 7 | // 将流中的数据写入底层 8 | process.stdout.write(data.toString().toUpperCase()) 9 | // 写入完成时,调用`next()`方法通知流传入下一个数据 10 | process.nextTick(next) 11 | // setTimeout(() => { 12 | // next() 13 | // }, 1) 14 | } 15 | 16 | //数据源 17 | const data = [1, 2, 3, 4, 5, 6, 7] 18 | while (true) { 19 | // 将一个数据写入流中 20 | writable.write(data.shift() + '\n') 21 | //数据空的时候退出 22 | if (data.length === 0) break 23 | } 24 | // 再无数据写入流时,需要调用`end`方法 25 | writable.end() 26 | 27 | const timer = setInterval(() => { 28 | console.log('哈哈哈') 29 | }, 0) 30 | 31 | // 所有的数据都写完了 32 | writable.on('finish', () => { 33 | process.stdout.write('DONE') 34 | clearInterval(timer) 35 | }) 36 | -------------------------------------------------------------------------------- /libuv-simple/libuv-simple/main.c: -------------------------------------------------------------------------------- 1 | // 2 | // main.cpp 3 | // libuv-simple 4 | // 5 | // Created by Z F on 2018/3/28. 6 | // Copyright © 2018年 Z F. All rights reserved. 7 | // 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | uv_fs_t open_req; 14 | uv_fs_t _read; 15 | 16 | static char buffer[1024]; 17 | static uv_buf_t iov; 18 | 19 | void on_read(uv_fs_t *req) { 20 | 21 | printf("%s\n",iov.base); 22 | } 23 | 24 | 25 | void on_open(uv_fs_t *req) { 26 | printf("%zd\n",req->result); 27 | 28 | iov = uv_buf_init(buffer, sizeof(buffer)); 29 | uv_fs_read(uv_default_loop(), &_read, (int)req->result, 30 | &iov, 1, -1, on_read); 31 | } 32 | 33 | int main() { 34 | const char* path = "/Users/zf/Desktop/Fz-node/libuv-simple/libuv-simple/text.txt"; 35 | uv_fs_open(uv_default_loop(), &open_req,path, O_RDONLY, 0, on_open); 36 | uv_run(uv_default_loop(), UV_RUN_DEFAULT); 37 | uv_fs_req_cleanup(&open_req); 38 | return 0; 39 | } 40 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Fz-node 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /readable.js: -------------------------------------------------------------------------------- 1 | const Readable = require('stream').Readable 2 | 3 | class FzReadable extends Readable { 4 | constructor(iterator) { 5 | super({ 6 | highWaterMark: 0 7 | }) 8 | this.iterator = iterator 9 | } 10 | 11 | //子类必须实现的方法 12 | _read() { 13 | const res = this.iterator.next() 14 | if (res.done) { 15 | //当收到null的时候,流就停止了 16 | return this.push(null) 17 | } 18 | const needMoreData = this.push(`${res.value}`) 19 | // setTimeout(() => { 20 | 21 | // }, 0) 22 | } 23 | } 24 | 25 | const iterator = (function(limit) { 26 | return { 27 | next: function() { 28 | if (limit--) { 29 | return { done: false, value: limit + Math.random() } 30 | } 31 | return { done: true } 32 | } 33 | } 34 | })(10) 35 | 36 | const readable = new FzReadable(iterator) 37 | 38 | //调用on('data')就会进入流动模式,数据会自发地通过data事件输出,不需要消耗方反复调用read(n)。 39 | //调用on('data')会在nexttick中使用read(0)方法去请求数据 40 | 41 | readable.on('data', data =>{ 42 | console.log(data.toString()) 43 | }) 44 | 45 | readable.on('readable', (chunk) => { 46 | // console.log('readable') 47 | // while (null !== readable.read()); 48 | }) 49 | 50 | // const timer = setInterval(() => { 51 | // console.log('哈哈哈') 52 | // }, 0) 53 | 54 | readable.on('end', () => { 55 | process.stdout.write('DONE') 56 | // clearInterval(timer) 57 | }) 58 | 59 | //doRead 60 | //当缓存中的数据足够多时,即不需要向底层请求数据。用doRead来表示read(n)是否需要向底层取数据 61 | 62 | //state.length 缓存数据 63 | -------------------------------------------------------------------------------- /docs/v8.md: -------------------------------------------------------------------------------- 1 | # v8 知识梳理以及编程入门 2 | 3 | 学习 v8 编程的好处就是能够知道 node 是如何通过写JavaScript进行调用系统 API 的,如果你有c/c++又或者是比较底层的语言的基础,研究明白 v8 是如何调用系统底层的,而系统是如何提供函数给v8调用的,那么我们对 node 的理解层度就会更上一层楼。 4 | 5 | # 本节主要折腾的几个事情 6 | 7 | - 如何通过JS调用c/c++层 8 | - 如何通过c/c++层调用JS层 9 | - 如何通过JS调用c++的类 10 | 11 | 当我们实现以上的JS-C/C++层的调用时,我们距离自己造一个node.js已经不远了 12 | 13 | # V8概念梳理 14 | 15 | v8 执行代码的过程主要是: 16 | 17 | - JavaScript源码输入 18 | - 转换成AST(抽象语法树) 19 | - JIT(just in time) 20 | - NativeCode 21 | 22 | 这对于我们编程有了最初的印象,接下来,我们介绍一下各个内部的概念 23 | 24 | 25 | # v8::Isolate 26 | 27 | Isolate 的概念给大家来看一定非常陌生,其英文原意是:隔离。在操作系统中,我们有一个概念和之类似:进程。进程是完全相互隔离的,一个进程里有多个线程,同时各个进程之间并不相互共享资源。Isolate 也是一样,Isolate1和Isolate2两个拥有各自堆栈的虚拟机实例,且相互完全隔离。 28 | 29 | - An isolate is a VM instance with its own heap. It represents an isolated instance of the V8 engine. V8 isolates have completely separate states. Objects from one isolate must not be used in other isolates. 30 | 31 | # v8::handle(v8::Local和v8:Persistent) 32 | 33 | 在新的版本中,v8::handle拆成了更为形象的两个类:v8::Local和v8:Persistent。用一个更形象的比喻,那么v8::Local更像是JavaScript中的let。在 V8 中,内存的分配都交付给了 V8,那么我们就最好不要使用自己的 new 方法来创建对象,而是使用 v8::Local 里的各种方法来创建一个对象。由v8::Local创建的对象,能够被 v8 自动进行管理,也就是传说中的GC(垃圾清理机制)。 34 | 35 | Persistent代表的是持久的意思,更类似全局变量,申请和释放一定要记得使用:Persistent::New, Persistent::Dispose这两个方法,否则会内存侧漏。 36 | 37 | # Scope 38 | 39 | Scope是一个比较小范围的GC单元,分别有```v8::HandleScope```和```v8::Context::Scope```。 40 | 41 | 42 | ```v8::HandleScope```一般情况下,HandleScope会在一个函数的开头进行声明,然后用于管理这整个函数所创建的Handle,而```v8::Context::Scope```也类似,只不过他是直接管理```context```对象的。 43 | ```js 44 | void F(){ 45 | //一开头放一个 46 | v8::HandleScope handle_scope(isolate); 47 | v8::Local source1 =....... 48 | v8::Local source2 =...... 49 | 50 | //开头放一个 51 | v8::Local context = Context::New(isolate); 52 | v8::Context::Scope context_scope(context); 53 | } 54 | 55 | ``` 56 | 这样一来,整个函数的handle和context就会被这两个scope分别管理起来,函数跑完了,那么就会自动释放掉了。 57 | 58 | # Context 59 | 60 | Context的概念我更喜欢把比喻成闭包,虽然不是,但是很像。在V8里面,Context主要是用于创建一个JavaScript的运行上下文环境。更形象生动的说法是Html的iFrame,一个网页中可以有多个iFrame,每个iFrame又有不同的运行环境。 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /docs/modulejs.md: -------------------------------------------------------------------------------- 1 | # Node.js模块化 (一) 2 | 3 | 模块化对于一门语言来说是属于架构层面的。我们将代码拆分成小Function,小类,但实际上我们还需要一种模块化机制去使得我们更大粒度的控制代码。这一种机制可以叫做:命名空间(namespace,包,模块化)。 4 | 5 | 6 | # 没有模块化会怎样? 7 | 8 | 有同学非常的疑惑,老子的代码自己写的我当然知道,我只要保证我的函数名不重复不就可以了?很可惜,这么思考的同学还是```图样图森破```了。在实际工程中我们有以下痛点: 9 | - 部门A开发一个包 10 | - 部门B开发了一个包 11 | - 如果没有模块化机制,部门A的同学必须大量与部门B的同学进行沟通,以防止函数名、函数变量、全局变量的不重复。 12 | 13 | 任何一个大工程都是多人开发的模式进行,没有一个良好的模块化机制,那么这无疑就增添了巨大的沟通成本。试想,1000个部门合作,1000个部门开发各自的包,然后1000个部门每天就在讨论```这个变量名和那个变量名不能用```,那谁都别想完成一个工程了。 14 | 15 | 在这种致命的痛点之下,模块化应运而生。 16 | 17 | 18 | # JavaScript 模块化 19 | 20 | 玩具语言js并没有原生的模块化机制,在```node.js```诞生初期,也并没有模块化机制,包括最新的ES6中的```import xxoo```也只是一种语法糖。庆幸的是,在JavaScript中并不需要语言上的支持,完全用一种Hack的方式就能实现模块化。 21 | 22 | 可能我这么表达,大家还是不能理解什么是模块化的思想,接下来,当我一步一步带你实现模块化并且解析```Node.js```的模块化实现。 23 | 24 | # 简单实现一个模块化 25 | 26 | 想要理解Node.js模块化如何做的,那我们要先来使用 JavaScript 实现模块化。 27 | 28 | ### 一个简单的函数 29 | ```js 30 | function Add(a, b) { 31 | return a + b 32 | } 33 | ``` 34 | 我们有一个简单的Add函数,想要将这个Add函数进行模块化,我们就得使用```闭包```。 35 | 36 | ```js 37 | function FzModule(module) { 38 | function Add(a, b) { 39 | return a + b 40 | } 41 | 42 | } 43 | ``` 44 | 代码修改一下,我们用一个FzModule来包住我们的Add函数,这个FzModule的参数是一个叫做Module的玩意。 45 | 46 | ### 一个叫Modules的类 47 | ```js 48 | function Modules() { 49 | this.exports = {} 50 | } 51 | 52 | const newModule = new Modules() 53 | ``` 54 | 55 | ### 组合Modules和我们的FzModule 56 | ```js 57 | function FzModule(modules) { 58 | function Add(a, b) { 59 | return a + b 60 | } 61 | modules.exports = Add 62 | return modules 63 | } 64 | 65 | function Modules() { 66 | this.exports = {} 67 | } 68 | 69 | const newModule = new Modules() 70 | 71 | const fz = FzModule(newModule) 72 | 73 | console.log('5+6=', fz.exports(5, 6)) 74 | ``` 75 | 76 | 毫无意外,当我们运行代码的时候,我们已经会获得```5+6= 11``` 77 | 78 | ### 把FzModule移出去 79 | 80 | 我们将FzModule移动到fz.js中 81 | ```js 82 | //fz.js 83 | ;(function FzModule(modules) { 84 | function Add(a, b) { 85 | return a + b 86 | } 87 | modules.exports = Add 88 | return modules 89 | }) 90 | ``` 91 | 在这里有必要解释一下,我特意在function外面包着一个(),这个叫做```立即执行```函数,意思就是只要你执行这个文件,这个函数就会自动执行。 92 | 93 | 我们回到刚刚的```main.js``` 94 | ```js 95 | //main.js 96 | const vm = require('vm') 97 | const fs = require('fs') 98 | 99 | const source = fs.readFileSync('./fz.js','utf-8') //我们读取fz.js中的源码字符串 100 | const fn = vm.runInThisContext(source)//注意,这一行代码是编译我们fz.js中的字符串 101 | 102 | function Modules() {//不用多说,我们自己创建的module对象 103 | this.exports = {} 104 | } 105 | 106 | const newModule = new Modules()//构建module对象 107 | 108 | const fz = fn(newModule)//传递modules对象 109 | 110 | console.log('5+6=', fz.exports(5, 6))//使用modules 111 | 112 | ``` 113 | 114 | 稍微解释一下 ```const fn = vm.runInThisContext(source)```这行代码是最重要的一个环节,```vm.runInThisContext```函数会将javascript代码传递到V8中去跑,并且因为我们刚刚使用了```立即执行```函数,因此,返回的就是我们刚刚的 115 | 116 | ```js 117 | //fz.js 118 | ;(function FzModule(modules) { 119 | function Add(a, b) { 120 | return a + b 121 | } 122 | modules.exports = Add 123 | return modules 124 | }) 125 | ``` 126 | 我们将```const fz = fn(newModule)//传递modules对象```丢进去,就获得了我们的模块 127 | 128 | ### 再简单一点 129 | ```js 130 | //fz.js 131 | ;(function FzModule(modules) { 132 | function Add(a, b) { 133 | return a + b 134 | } 135 | modules.exports = Add 136 | return modules 137 | }) 138 | ``` 139 | 我们再回头看看我们的模块化,这一部分代码有重复的地方 140 | 1. 我们每写一个模块,就要用一个```function```和```立即执行函数```去包裹住我们的函数 141 | 2. 每次都要返回 142 | 3. 饮用的时候,我们必须``` fs.readFileSync('./fz.js','utf-8')```读取一下源码,再使用```const fn = vm.runInThisContext(source)```跑一遍, 143 | 4. 最后,再通过构建一个```module```对象,传递构建出```const fz = fn(newModule)``` 144 | 5. 浪费时间和精力 145 | 146 | 根据DRY原则,这一部分我们进行封装,使得我们使用模块更加简单。 147 | 148 | ### 最后的封装! 149 | 150 | 我们简化我们的```fz.js``` 151 | ```js 152 | function Add(a, b) { 153 | return a + b 154 | } 155 | 156 | modules.exports = Add 157 | ``` 158 | 注意,```modules```是我自己构建的,和```nodejs```中的```module.exports```不同! 159 | 160 | 改造一下```main.js``` 161 | ```js 162 | const vm = require('vm')//引入vm 163 | const fs = require('fs') 164 | 165 | function Modules() { 166 | //不用多说,我们自己创建的module对象 167 | this.exports = {} 168 | } 169 | 170 | Modules.prototype._compile = function(src) { 171 | 172 | const wrap = source => { 173 | return `(function(modules){${source}\nreturn modules})` //一个包囊函数,纯粹就把字符串封装进来 174 | } 175 | 176 | const source = fs.readFileSync(src, 'utf-8') //我们读取fz.js中的源码字符串 177 | const moduleWrap = wrap(source) 178 | const fn = vm.runInThisContext(moduleWrap) //注意,这一行代码是编译我们fz.js中的字符串 179 | return fn 180 | } 181 | 182 | function _Require(src) { 183 | const newModule = new Modules() //构建module对象 184 | 185 | const fn = newModule._compile(src) //编译源码 186 | return fn(newModule).exports//返回 187 | } 188 | 189 | ``` 190 | 191 | 最后,见证奇迹的时刻!!! 192 | ```js 193 | const fz = _Require('./fz.js') //使用我们的_Require函数 194 | 195 | console.log('5+6=', fz(5, 6)) //使用modules 196 | 197 | //输出 5+6 = 11 198 | ``` 199 | 200 | - 30不到的代码,我们就实现了一个高可复用性的模块化_Require 201 | - 是不是非常像我们的nodejs了??????? 202 | - 现在能分清楚module.exports和exports的区别了吗? 203 | 204 | 你没理解错,这就是Node.js的模块机制做法,当然这么个模块机制还是会有很多的bug 205 | - 循环引用问题 206 | - 重复引用导致内存过大 207 | 208 | 那么Node.js是如何解决这一块内容的,在下一节内容中我们将揭开这个秘密! 209 | 210 | -------------------------------------------------------------------------------- /docs/modulejs2.md: -------------------------------------------------------------------------------- 1 | # Node.js模块化 (二) 2 | 3 | 上一节内容中我们讲到了Node.js的模块化机制采用的是闭包实现,我们也非常简洁的用```30行代码```实现了一个简陋版本的 Node.js 模块化,这一节,我们将深入到模块化的内部,一探究竟 4 | 5 | # Node.js 模块分类 6 | 7 | - ```文件模块```:用户编写的模块,包括npm包,本地文件等等. 8 | - ```Native_module```:我们叫他们做```核心模块```,但是一般来说指的是由```JS``` + ```c/c++```混合编写的模块,如```http```,```fs```等 9 | - ```内建模块```:一些纯粹的c++模块,毫无JS代码,已经编译好,启动时直接加载进内存,用户一般不直接调用。 10 | 11 | # Native_module 模块的引入 12 | 13 | 想要搞明白```Native_module```模块的引入,我们还得从一个例子,开始走起: 14 | ```js 15 | const http = require('http') 16 | ``` 17 | 我们不妨思考一个问题,这个```require```哪里来的?跟着调试走,我们来到以下的代码: 18 | ```js 19 | (function (exports, require, module, __filename, __dirname) { 'use strict'; 20 | 21 | // Invoke with makeRequireFunction(module) where |module| is the Module object 22 | // to use as the context for the require() function. 23 | function makeRequireFunction(mod) { 24 | const Module = mod.constructor; 25 | 26 | function require(path) { 27 | try { 28 | exports.requireDepth += 1; 29 | return mod.require(path); 30 | } finally { 31 | exports.requireDepth -= 1; 32 | } 33 | } 34 | ..... 35 | ) 36 | ``` 37 | 如果你还记得上一节我们讲述的代码,那你已经不会对这样的代码陌生。我们的Node.js在启动的时候,就已经为我们注入了一个```require函数```,在接下去的所有```module```中,都会传递```function (exports, require, module, __filename, __dirname)```这么一些参数进来。稍微有经验的同学已经发现,这些参数,我们都可以在任何一个```.js```文件中直接实用,究其原因就是因为Node.js帮我们做了一个注入。 38 | 39 | 往下走,我们来到require的定义: 40 | ```js 41 | // Loads a module at the given file path. Returns that module's 42 | // `exports` property. 43 | Module.prototype.require = function(path) { 44 | assert(path, 'missing path'); 45 | assert(typeof path === 'string', 'path must be a string'); 46 | return Module._load(path, this, /* isMain */ false); 47 | }; 48 | ``` 49 | 实际上调用require是为了引出```Module._load```这个方法,看名字我们就知道,是装载的模块的意思。 50 | 51 | ```js 52 | Module._load = function(request, parent, isMain) { 53 | ///省略了一些废话.... 54 | var filename = Module._resolveFilename(request, parent, isMain); 55 | //这一步非常的关键,如果模块已经被导入,那么就直接会被返回 56 | //这一招巧妙的解决了:重复引用,以及重复加载的问题 57 | //使得一个模块不会被加载多次,而且第二次加载的时候是从内存里直接拿的 58 | var cachedModule = Module._cache[filename]; 59 | if (cachedModule) { 60 | updateChildren(parent, cachedModule, true); 61 | return cachedModule.exports; 62 | } 63 | 64 | if (NativeModule.nonInternalExists(filename)) { 65 | //如果在缓存中没有找到模块 66 | //那么则来到这个方法去加载 67 | return NativeModule.require(filename); 68 | } 69 | ///再次省略了一些无关代码... 70 | }; 71 | ``` 72 | 在注释中我说得很明白,缓存机制在Node.js系统中大量运用,为的就是提速。使用空间换时间的做法在工业上非常常见而且有效,相比于性能来说,内存实在不贵。接下去我们看看,第一次加载核心模块时做了什么 73 | 74 | ```js 75 | NativeModule.require = function(id) { 76 | 77 | //去除了一些废话 78 | 79 | //这里的id='http',也就是我们要引入的模块 80 | //这个NativeModule的构造函数我们放在下面 81 | //这里就理解为新建一个Native模块用于存储我们的http 82 | const nativeModule = new NativeModule(id); 83 | 84 | //创建以后,马上把这个已经加载的模块缓存起来 85 | //下次实用时,可以快速拿出来 86 | nativeModule.cache(); 87 | 88 | //这一行,是核心模块的编译函数,也是整个模块导入的核心 89 | nativeModule.compile(); 90 | //我们 91 | return nativeModule.exports; 92 | }; 93 | 94 | //NativeModule 的构造函数 95 | function NativeModule(id) { 96 | this.filename = `${id}.js`; 97 | this.id = id; 98 | this.exports = {}; 99 | this.loaded = false; 100 | this.loading = false; 101 | } 102 | 103 | ``` 104 | ```nativeModule.compile();```这一行函数,是核心,我们追进去,看看到底做了什么。 105 | 106 | ```js 107 | NativeModule.prototype.compile = function() { 108 | //这一行函数的意思是获取源码 109 | //注意并不是我们可执行的js,而是纯粹的字符串 110 | var source = NativeModule.getSource(this.id); 111 | //这个wrap函数也是非常经典了,我已经在上一章节中演示 112 | //作用就是把模块包装成一个函数表达式,提供V8编译 113 | source = NativeModule.wrap(source); 114 | try { 115 | //终于,我们看到了老朋友runInThisContext 116 | const fn = runInThisContext(source, { 117 | filename: this.filename, 118 | lineOffset: 0, 119 | displayErrors: true 120 | }); 121 | //编译出来的function,调用,丢进去模块的exports,require,模块本身,以及_filename 122 | //注意,这里没有__dirname,因为这些模块本身就在nodejs控制范围内,无需知道路径了 123 | fn(this.exports, NativeModule.require, this, this.filename); 124 | //标记已经记载 125 | this.loaded = true; 126 | } finally { 127 | this.loading = false; 128 | } 129 | }; 130 | ``` 131 | 到此,JavaScript层的模块引入已经完毕,非常简单轻松。老朋友```runInThisContext```再次出现,毫无意外,这一部分的内容涉及到c++,会在之后的c++篇幅中讲解,其实这个函数也就是调用V8的一个函数编译并执行,就如```eval```类似。 132 | 133 | # 文件 模块的引入 134 | 135 | 文件模块的引入非常的类似,值得注意的是,核心模块被存储在NativeModule._cache对象上,而Module._cache对象上存储的是文件模块.更有趣的事情是,文件模块会被缓存在```require```这个函数的cache属性上,通过操作```require.cache```属性,我们能够实现热加载配置等等方案。 136 | 137 | 138 | 但是```require.cache```并没有被官方所提及,人们发现这个api早在2015-2016年之间的一个issue中,之后就被大量实用...我觉得官方并没有提及这个api的原因```是不想被人所知道```,因为node.js模块加载机制的原因,操纵这个cache是非常危险的事情。一个不小心就会导致内存泄漏。最近看到一个团队,也因为这件事情栽了跟斗。具体地址贴一下,非常好的案例,提供大家学习:[一行 delete require.cache 引发的内存泄漏血案](https://zhuanlan.zhihu.com/p/34702356) 139 | 140 | 引以为戒: 141 | ```bash 142 | 特别提醒 143 | delete require.cache 这个操作是许多希望做 Node.js 热更新的同学喜欢写的代码,根据上面的分析要完整这个模块引入缓存的完整去除需要所有引用到此模块的 parent.children 数组里面的引用。 144 | 145 | 遗憾的是,module.parent 只是保存了第一次引用该模块的父模块,而其它引用到此文件的父模块需要开发者自己去构建载入模块间的相互依赖关系,所以在 Node.js 下做热更新并不是一条传统意义上的生路 146 | ``` 147 | 148 | 149 | 150 | 到此,JS层的已经讲得7788,剩下来的c++模块,之后到了c++时在进行讲解 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /docs/timer.md: -------------------------------------------------------------------------------- 1 | # Timer模块 2 | 3 | 在我们的概念中,Timer一词很容易让我们想到Javascript中的几个全局API 4 | 5 | ```javascript 6 | setTimeout()//一定时间过期 7 | setInterval()://无限循环,直到停止 8 | ``` 9 | 10 | 大家或多或少的都已经使用过这两个API,也不陌生,也很直观。 11 | 12 | # 使用场景1:HTTP返回头 13 | 14 | 我在知乎的一个问题中([方正知乎回答](https://www.zhihu.com/question/266029860/answer/348784731))提问到:**HTTP RFC中,明确要求response Header定义Date字段,Nodejs 如何高效的获取时间戳而不影响性能的?** 15 | 16 | 看一个HTTP返回头: 17 | ```javascript 18 | Response Headers 19 | Connection: keep-alive 20 | Content-Length: 577 21 | Content-Type: text/html 22 | Date: Sat, 24 Mar 2018 00:19:46 GMT //注意这里 23 | Server: openresty/1.9.15.1 24 | ``` 25 | 26 | 我们每次发起HTTP请求时,服务器端都会生成这么一个时间戳返回。 27 | 28 | 大家觉得:切,这个有什么,老夫```Date.now()```,转换一下,马上给你搞出来。 29 | 30 | 我只能说,你太肤浅了。这么说的原因是因为```任何一个底层时间获取函数都是一次严重的系统调用```. 31 | 32 | 熟悉系统调用这个词的同学一定知道,每一个System Call的消耗都是非常巨大的。 33 | 34 | 假设,我们请求成千成万,你每次都去调用System Call你的系统并发量会下降大约30-40%左右! 35 | 36 | 或许你会想,那老子不提供了,怎么样?当然不行,规范是这么写的,如果你按规范走,那你就时邪教了。 37 | 38 | ```javascript 39 | 题外话:这数据不是我凭空想象出来的,而是我半年前造的一个Python写的Async/await服务器时,发现的。 40 | [show me the code:luya服务器](https://github.com/215566435/LuyWeb/blob/master/luya/response.py#L116) 41 | ``` 42 | 43 | # 使用场景2:HTTP Request header Keep-Alive 44 | 45 | 在蛮荒的http 1.0时代,人们并不关心什么并发,当时并发也少。所以"应答"模式的HTTP协议,采用的流程 46 | ```js 47 | request->服务器开启socket迎接->处理request,拼装回复->response->关闭socket 48 | ``` 49 | 这样的一个流程看起来很舒坦,但实际上隐含着巨大的性能问题。 50 | 51 | 每次开启和关闭socket的操作,属于System Call,非常的重,就跟妈妈用锤子锤了你电脑一样重。 52 | 53 | 大量请求到来时,开开关关,性能下降20-30%,非常巨大。 54 | 55 | ```javascript 56 | 我又怎么懂那么精确的,依旧是我之前造轮子的时候碰上的..... 57 | [show me the code:luya服务器](https://github.com/215566435/LuyWeb) 58 | ``` 59 | 60 | 为了解决这个问题,人们在HTTP中加入了Keep-Alive字段,在1.0时代,默认关闭,开启的时候需要"Connection: Keep-Alive"。 61 | 62 | 而HTTP 1.1以后,则是默认就开启,关闭:"Connection: close". 63 | 64 | 当然了,一个socket不能一直开着,会消耗内存(linux下每个网络socket消耗大约3-4kb的内存). 65 | 66 | 因此,在一段时间(默认120s)内客户端没有什么新的请求,这个socket就会关闭了。 67 | 68 | 由此我们可以看到,我们必须引入某种计时器机制,去应对这种情况。Node.js的HTTP模块对于每一个新的连接创建一个 socket 对象,调用socket.setTimeout设置一个定时器用于超时后自动断开连接。 69 | 70 | 71 | # Timer模块的引入和问题 72 | 73 | ## 模块 74 | Timer模块也是一个c++和javascript集合模块,具体具体调用可以这么玩. 75 | 模块源码[timer_wrap](https://github.com/nodejs/node/blob/master/src/timer_wrap.cc) 76 | 77 | ```javascript 78 | const Timer = process.binding('timer_wrap').Timer;//引入c++模块 79 | const kOnTimeout = Timer.kOnTimeout | 0; 80 | 81 | function DIYsetTimeout(fn, ms) { 82 | var timer = new Timer(); // 创建计时器对象 83 | timer.start(ms, 0); // 触发时间 84 | timer[kOnTimeout] = fn; // 设置回调函数 85 | return timer; // 返回定时器 86 | } 87 | 88 | // 使用我们自己的DIYsetTimeout 89 | DIYsetTimeout(() => console.log('DIYsetTimeout timeout!'), 1000); 90 | ``` 91 | 92 | 如果我们使用这样的一个DIYsetTimeout去实现我刚刚说的HTTP,那就会有新的问题 93 | 94 | ## 问题 95 | ```javascript 96 | var timer = new Timer(); // 创建计时器对象,用于计时 97 | ``` 98 | 这句话就是问题的所在,当我们创建N条链接的时候,就会创建N个Timer对象,构建对象同样是Javascript中比较重的事情,多了性能就差。 99 | 100 | 101 | # Node.js内部优化思路:分级时间轮 102 | 103 | 在正常的HTTP请求中,每一个请求都是120s之后关闭,在这种情况下**我们完全没必要创建多个Timer去跑**,而是只用一个timer就可以完成计时的工作。 104 | 105 | 而这样的一个问题,抽象出来就是: 106 | - 以触发时间作为key,存在哈希表中,每一个key,我们叫时间槽 107 | - 同一个时间槽内,将所有任务安排在一条直线上(链表) 108 | - 每个任务记录一个 startTime 和 endTime, 109 | - 在新增任务的同时记录下startTime和endTime 110 | - 在endTime时触发回调 111 | - 删除任务,回到最开始的Timer中,计算下一个任务的触发时间 112 | 113 | 114 | 这就是一个时间轮的算法,说起来贼特么难懂,但是做起来却很简单. 115 | 116 | # 伪代码设计一个Timer时间轮 117 | 118 | 119 | 首先我们准备一个对象,用于按key存储Timer: 120 | ```javascript 121 | const timerWheel = {}; 122 | ``` 123 | 124 | 搞定了,我们注册几个任务 125 | ```javascript 126 | const timer1 = setTimeout(() => {}, 120*1000);//任务1 127 | //中间干了什么事等了10s 128 | const timer2 = setTimeout(() => {}, 120*1000);//任务2 129 | //中间干了什么事等了10s 130 | const timer3 = setTimeout(() => {}, 120*1000);//任务3 131 | ``` 132 | 133 | ```javascript 134 | //底层会这么做 135 | var L = timerWheel[120*1000] 136 | if(!L) L = new TimersList(xxx) 137 | L.push(任务1) 138 | L.push(任务2) 139 | L.push(任务3) 140 | ``` 141 | ![时间轮的图](https://github.com/215566435/Fz-node/blob/master/docs/assets/time-wheel.png?raw=true) 142 | 143 | # 依次触发同为key120*1000的Timer 144 | 145 | 同为```120*1000```的Timer一共有三个,插入的时间分别不同,为了方便,我们可以进行假设,第一个Timer启动的时间是 146 | 147 | ```javascript 148 | timer1._idleStart === 0 149 | timer2._idleStart === 10000 150 | timer3._idleStart === 20000 151 | ``` 152 | 153 | 1. 首先 ***_idleStart为0的timer*** 进入TimersList中(里面有一个C实现的timer计时器),计时结束后,进行回调,然后删除```_idleStart为0的timer``` 154 | 2. 然后```_idleStart为10000的timer```进入TimersList中(里面有一个C实现的timer计时器),计时结束后,进行回调,然后删除```_idleStart为10000的timer``` 155 | 3. .... 156 | 157 | 由此可见,我们三个都是120秒的定时器,依次触发,通过这种巧妙的设计,使得一个Timer对象得到了最大的复用,从而极大的提升了timer模块的性能。 158 | 159 | 160 | 161 | # 回到最初的使用场景:HTTP Request header Keep-Alive 162 | 163 | ```javascript 164 | 303 if (self.timeout) 165 | 304 socket.setTimeout(self.timeout); 166 | 305 socket.on('timeout', function() { 167 | 306 var req = socket.parser && socket.parser.incoming; 168 | 307 var reqTimeout = req && !req.complete && req.emit('timeout', socket); 169 | 308 var res = socket._httpMessage; 170 | 309 var resTimeout = res && res.emit('timeout', socket); 171 | 310 var serverTimeout = self.emit('timeout', socket); 172 | 311 173 | 312 if (!reqTimeout && !resTimeout && !serverTimeout) 174 | 313 socket.destroy(); 175 | 314 }); 176 | ``` 177 | 178 | 179 | - 默认的 timeout 为this.timeout = 2 * 60 * 1000; 也就是 120s。 180 | - L313,超时则销毁 socket。 181 | 182 | # 参考文档 183 | 1. https://yjhjstz.gitbooks.io/deep-into-node/content/chapter3/chapter3-1.html 184 | 2. https://zhuanlan.zhihu.com/p/30763470 185 | 3. https://link.zhihu.com/?target=http%3A//www.cnblogs.com/hust/p/4809208.html 186 | 187 | # 源码 188 | 1.[timer.js模块](https://github.com/nodejs/node/blob/master/lib/timers.js) 189 | 2.[timer_wrap](https://github.com/nodejs/node/blob/master/src/timer_wrap.cc) 190 | -------------------------------------------------------------------------------- /docs/cluster.md: -------------------------------------------------------------------------------- 1 | # Node.js源码阅读:多进程架构的演进之路2 2 | 3 | 在讲cluster之前,我们要明白我们所遇到的困境还有哪些。上一节我们讲到了: 4 | 1. 单进程不稳,我们就多进程 5 | 2. 多进程我们碰到了端口被多个worker占据报错 6 | 3. 代理模式,但是会导致文件描述符消耗翻倍 7 | 4. 通过发送句柄的方式,我们轻松的解决了以上所有问题 8 | 9 | 看似非常完美的解决方案,实际上我们的服务器依旧是脆弱不堪的。 10 | 11 | 1. 性能问题:到底开几个worker 12 | 2. 管理多个工作进程状态 13 | 3. 平滑重启:用户无感知 14 | 4. 模块、配置、静态数据的热加载 15 | 16 | 针对上述的问题,我们想让我们 node.js 跑得更稳健,这些事情都是我们必须考虑的。 17 | 18 | ```js 19 | //社区中有比较成熟的方案,如forever和pm2模块 20 | //这些都是非常优秀的模块 21 | //但是对于企业级解决方案来说,pm2和forever复杂度太高,不易于拓展,后文会以实例讲明 22 | //因此,我们必须要熟悉cluster的一切,这样我们才能写出极易拓展,健壮的服务端程序 23 | ``` 24 | 25 | # 一段简单的cluster代码 26 | 27 | 之前我们process讲解中,讲述了各种情况,但是自从有了cluster模块以后,上述的一些神经病烧脑状态就不用我们费力去思考,我们的代码也变得极其简单 28 | ```javascript 29 | const cluster = require('cluster'); // | | 30 | const http = require('http'); // | | 31 | const numCPUs = require('os').cpus().length; // | | 都执行了 32 | // | | 33 | if (cluster.isMaster) { // |-|----------------- 34 | // Fork workers. // | 35 | for (var i = 0; i < numCPUs; i++) { // | 36 | cluster.fork(); // | 37 | } // | 仅父进程执行 (a.js) 38 | cluster.on('exit', (worker) => { // | 39 | console.log(`${worker.process.pid} died`); // | 40 | }); // | 41 | } else { // |------------------- 42 | // Workers can share any TCP connection // | 43 | // In this case it is an HTTP server // | 44 | http.createServer((req, res) => { // | 45 | res.writeHead(200); // | 仅子进程执行 (b.js) 46 | res.end('hello world\n'); // | 47 | }).listen(8000); // | 48 | } // |------------------- 49 | // | | 50 | console.log('hello'); // | | 都执行了 51 | ``` 52 | 不再需要把句柄传递来传递去,在先有cluster方案中,我们的服务器代码「一行不用改」,这极其方便了我们部署程序。上述代码实现的效果,就跟我们在process里一样,根据cpu核心个数创建多个子进程,并且可以监听同一个端口,其内部的做法是差不多的,也是通过fork进程来做。但是,cluster厉害的地方就在于:无需修改任何代码就可以获得集群 53 | 54 | # 一探究竟cluster源码 55 | 56 | 想要代码原封不动,最重要的是拦截,最后的这个listen调用, 57 | ```js 58 | http.createServer((req, res) => { // | 59 | res.writeHead(200); // | 仅子进程执行 (b.js) 60 | res.end('hello world\n'); // | 61 | }).listen(8000); 62 | 63 | ``` 64 | 一般来说,http.createServer().listen() 就会创建一个socket,监听我们预设的端口,导致我们的监听失败。 65 | 66 | 具体在哪里做的呢,[cluster child](https://github.com/nodejs/node/blob/master/lib/internal/cluster/child.js) 67 | ```javascript 68 | function listen(backlog) { 69 | // TODO(bnoordhuis) Send a message to the master that tells it to 70 | // update the backlog size. The actual backlog should probably be 71 | // the largest requested size by any worker. 72 | return 0; 73 | } 74 | ``` 75 | 这是一个hack函数,当cluster fork出来子进程只要调用listen方法,他就给你屏蔽掉了. 76 | 77 | cluster为我们做了这件事: 78 | - 端口仅由master进程中的内部TCP服务器监听了一次。 79 | - 不会出现端口被重复监听报错,是由于,worker进程中,最后执行监听端口操作的方法,已被cluster模块主动覆盖。 80 | 81 | # 重启不稳定的worker 82 | 83 | 由于某种原因,我们的进程发生了严重的bug,但是开发者并不知道,甚至都没捕捉到。这时候,这个worker就进入了不稳定的状态。对于这种不稳定状态的worker,我们应该将其杀死,然后用cluster重启。 84 | 85 | 想要做到平滑重启,我们需要捕获 ```uncaughtException```,意思是 没有捕获的错误。代码很简单: 86 | 87 | ```js 88 | 89 | // Workers can share any TCP connection // | 90 | // In this case it is an HTTP server // | 91 | const server = http.createServer((req, res) => { // | 92 | res.writeHead(200); // | 仅子进程执行 (b.js) 93 | res.end('hello world\n'); // | 94 | }).listen(8000); // | 95 | 96 | process.on('uncaughtException',(err)=>{ 97 | log(err)//记录致命原因 98 | //#最好再写几行代码,通过进程通信,通知cluster,这个进程马上要自杀了 99 | server.close(()=>{//调用close方法,停止接收所有新的链接 100 | //当已有链接全部断开后,退出进程 101 | process.exit(1); 102 | }) 103 | }) 104 | ``` 105 | 106 | # egg.js Agent机制 107 | 108 | 在这里,阿里的egg.js文档中已经解释得非常清除了。在这里,我就稍微引用,以做标记,作为我学习的笔记. 109 | 110 | 111 | 说到这里,Node.js 多进程方案貌似已经成型,这也是我们早期线上使用的方案。但后来我们发现有些工作其实不需要每个 Worker 都去做,如果都做,一来是浪费资源,更重要的是可能会导致多进程间资源访问冲突。举个例子:生产环境的日志文件我们一般会按照日期进行归档,在单进程模型下这再简单不过了: 112 | 113 | 每天凌晨 0 点,将当前日志文件按照日期进行重命名 114 | 销毁以前的文件句柄,并创建新的日志文件继续写入 115 | 试想如果现在是 4 个进程来做同样的事情,是不是就乱套了。所以,对于这一类后台运行的逻辑,我们希望将它们放到一个单独的进程上去执行,这个进程就叫 Agent Worker,简称 Agent。Agent 好比是 Master 给其他 Worker 请的一个『秘书』,它不对外提供服务,只给 App Worker 打工,专门处理一些公共事务。现在我们的多进程模型就变成下面这个样子了 116 | 117 | ```javascript 118 | 119 | +--------+ +-------+ 120 | | Master |<-------->| Agent | 121 | +--------+ +-------+ 122 | ^ ^ ^ 123 | / | \ 124 | / | \ 125 | / | \ 126 | v v v 127 | +----------+ +----------+ +----------+ 128 | | Worker 1 | | Worker 2 | | Worker 3 | 129 | +----------+ +----------+ +----------+ 130 | 那我们框架的启动时序如下: 131 | 132 | +---------+ +---------+ +---------+ 133 | | Master | | Agent | | Worker | 134 | +---------+ +----+----+ +----+----+ 135 | | fork agent | | 136 | +-------------------->| | 137 | | agent ready | | 138 | |<--------------------+ | 139 | | | fork worker | 140 | +----------------------------------------->| 141 | | worker ready | | 142 | |<-----------------------------------------+ 143 | | Egg ready | | 144 | +-------------------->| | 145 | | Egg ready | | 146 | +----------------------------------------->| 147 | 148 | ``` 149 | 150 | - Master 启动后先 fork Agent 进程 151 | - Agent 初始化成功后,通过 IPC 通道通知 Master 152 | - Master 再 fork 多个 App Worker 153 | - App Worker 初始化成功,通知 Master 154 | - 所有的进程初始化成功后,Master 通知 Agent 和 Worker 应用启动成功 155 | 156 | # egg.js的集群方案 157 | 158 | [egg.js集群方案](https://eggjs.org/zh-cn/core/cluster-and-ipc.html) 159 | 160 | egg.js集群方案已经囊括上面我说的所有点,以及对其进行了完整的封装 161 | 162 | 1. 完善的cluster模块(egg-cluster) 163 | 2. agent机制,处理一些杂碎 164 | 3. 封装好的IPC接口,方便开发者调用 165 | 4. IPC实战演练 166 | 5. [IPC的高级应用](https://eggjs.org/zh-cn/advanced/cluster-client.html) 167 | 168 | 169 | # 参考资料 170 | - [egg.js集群方案](https://eggjs.org/zh-cn/core/cluster-and-ipc.html) 171 | - 深入浅出node.js -朴灵 -------------------------------------------------------------------------------- /docs/process.md: -------------------------------------------------------------------------------- 1 | # Node.js源码阅读:多进程架构的演进之路 2 | 3 | 采用[事件循环](https://zhuanlan.zhihu.com/p/31410589)最大的毛病就是一个 Node.js进程实例,就是一个单线程、单进程的。在现代工业生产中,这会导致两个极其严重的问题: 4 | 5 | - 单进程不稳,服务器脆弱不堪 6 | - 无法利用多核CPU并行处理 7 | 8 | 从性能和稳定性来看,Node.js 如果不出现可行的解决方案,那Node.js终究是一个玩具。幸好,在社区中有很多强大的实现方案,今天就让我们来揭秘一下,Node.js多进程架构的演进之路吧。 9 | 10 | # child_process 模块 11 | 12 | child_process是最早的多进程架构解决方案之一,child_process.fork这个经典函数也给予了我们复制进程的能力。 13 | ```js 14 | //它和POSIX标准的fork函数不同的是,POSIX-fork当复制出来的子进程挂掉之后 15 | //我们手动的回收这个进程的尸体(waitpid). 16 | //而child_process.fork则不需要 17 | ``` 18 | 19 | 我们按照cpu个数,启动相应数量的worker进程 20 | ```js 21 | //master 22 | var fork = require('child_process').fork 23 | 24 | var cpu = require('os').cpus() 25 | 26 | for (var i = 0; i < cpu.length; i++) { 27 | fork('./worker.js') 28 | } 29 | ``` 30 | 31 | 每个worker进程再分别监听不同的端口 32 | ```js 33 | //worker.js 34 | var http = require('http') 35 | 36 | const port = Math.round(1 + Math.random() * 10000) 37 | http 38 | .createServer((req, res) => { 39 | res.end('hahahaha') 40 | }) 41 | .listen(port, '127.0.0.1', () => { 42 | console.log(`我是pid:${process.pid},我监听:${port}`) 43 | }) 44 | ``` 45 | 46 | 这样我们就很轻松的获得了一组由master-worker组成的小集群。 47 | 48 | # 简陋的child_process.fork会导致的问题 49 | 50 | 首先,最大的问题就是我们的字进程都分别监听不同的端口,我们一个网站对外都是统一的一个端口,这不太符合我们需求。其次就是如果我们让所有的子进程同时监听一个端口,就会报错。 51 | 52 | 我们来实验一下 53 | 54 | ```js 55 | //master 56 | var fork = require('child_process').fork 57 | for (var i = 0; i < 2; i++) {//注意我们改为2 58 | fork('./worker.js') 59 | } 60 | ``` 61 | 62 | 63 | ```js 64 | //worker.js 65 | var http = require('http') 66 | 67 | const port = 8080 68 | http 69 | .createServer((req, res) => { 70 | res.end('hahahaha') 71 | }) 72 | .listen(port, '127.0.0.1', () => { 73 | console.log(`我是pid:${process.pid},我监听:${port}`) 74 | }) 75 | ``` 76 | 77 | 恭喜,你获得了一个***Error: listen EADDRINUSE 127.0.0.1:8080**错误 78 | ```bash 79 | 我是pid:84555,我监听:8080 80 | events.js:183 81 | throw er; // Unhandled 'error' event 82 | ^ 83 | 84 | Error: listen EADDRINUSE 127.0.0.1:8080 85 | at Object._errnoException (util.js:1024:11) 86 | at _exceptionWithHostPort (util.js:1046:20) 87 | at Server.setupListenHandle [as _listen2] (net.js:1351:14) 88 | at listenInCluster (net.js:1392:12) 89 | at doListen (net.js:1501:7) 90 | at _combinedTickCallback (internal/process/next_tick.js:141:11) 91 | at process._tickCallback (internal/process/next_tick.js:180:9) 92 | at Function.Module.runMain (module.js:678:11) 93 | at startup (bootstrap_node.js:187:16) 94 | at bootstrap_node.js:608:3 95 | ^C 96 | ``` 97 | 98 | 虽然在Node.js底层已经设置了每个端口都设置了SO_REUSERADDR,但是依旧报错,原因是因为当我们每启动一个进程的时候,我们的HTTP都会重新开启一个socket套接字,其文件描述符各不相同,每个描述符都跑去监听同一个接口,那就悲剧了。 99 | 100 | 这里值得注意的一个细节就是,虽然我们同时监听了一个接口报错了,但是仍然有 **第一个服务器开启成功了**,这也印证了之前我们的想法:***我们的HTTP都会重新开启一个socket套接字,其文件描述符各不相同,每个描述符都跑去监听同一个接口,那就悲剧了。*** 101 | 102 | 同图来表示 103 | ![](https://github.com/215566435/Fz-node/blob/master/docs/assets/diifer-socket.png?raw=true) 104 | 105 | # 进程间通信:代理模式 106 | 107 | 108 | 通过进程间通信(IPC)的手段,我们可以用最简单的方式去解决同一个端口不能被多个描述符监听的问题。我们可以设计 master 进程接受请求,然后开启一个或者多个 socket 发送消息给 worker ,这种模式称为代理模式。具体如下: 109 | 110 | ```js 111 | |-------| 112 | |master | 113 | | 80 | 114 | |-------| 115 | / \ 116 | / \ 转发消息 117 | / \ 118 | V V 119 | |-------| |-------| 120 | |worker | |worker | 121 | | 8000 | | 8001 | 122 | |-------| |-------| 123 | 124 | ``` 125 | 但是,这么做是有严重的性能问题的,在之后的高级应用中,我们也会见到这种一种蛋疼的代理模式如何去规避。这么做不好的地方就在于,用户来了一个请求,然后发送给其他的worker的同时,必须消耗 客户-master, master-worker 两倍的描述符。这样以来系统的文件描述符就很快被耗尽。 126 | 127 | 庆幸的是,Node.js社区都是老油条,在目前的版本中用直接发送句柄的办法解决了这个问题。 128 | 129 | # 进程间通信:发送句柄 130 | 131 | 废话不多说,我们来看一个简单的例子 132 | ```js 133 | //master 134 | var fork = require('child_process').fork 135 | var server = require('net').createServer() 136 | server.listen(8888, () => { //master监听8888 137 | console.log('master on :', 8888) 138 | }) 139 | 140 | 141 | var workers = {} 142 | for (var i = 0; i < 2; i++) { 143 | var worker = fork('./worker.js') 144 | worker.send('server', server)//发送句柄给worker 145 | worker[worker.pid] = worker 146 | console.log('worker create pid:', worker.pid) 147 | } 148 | ``` 149 | 150 | ```js 151 | //worker 152 | var http = require('http') 153 | 154 | var server = http.createServer((req, res) => { 155 | res.end('hahahaha') 156 | })//不监听 157 | 158 | process.on('message', (msg, handler) => { 159 | 160 | if (msg === 'server') { 161 | const handler = tcp 162 | handler.on('connection', socket => {//代表有链接 163 | server.emit('connection', socket)//emit方法触发 worker服务器的connection 164 | }) 165 | } 166 | }) 167 | ``` 168 | 169 | 上述两端代码其实做的就是 170 | 171 | - master 172 | 1. master创建一个tcp服务器,监听端口 173 | 2. master fork worker 174 | 3. master 把句柄发送给子进程 **worker.send('server', server)//发送句柄给worker** 175 | 176 | - worker 177 | 1. 创建一个http服务器,处理请求逻辑,不监听端口 178 | 2. process.on('message') 用于接收 master 的信息,回调函数的第一个参数就是信息,第二个参数就是所谓的句柄 179 | 3. 每个进程通过监听 handler 上的connection事件,通过emit触发 http 服务器的内部逻辑并且返回 180 | 181 | # 句柄还原 182 | 183 | 什么是句柄? 184 | ```html 185 | 句柄是一种可以用来标示资源的引用,它的内部包含了指向对象的文件描述符。 186 | 187 | 比如句柄可以用来标示一个服务器socket对象等 188 | ``` 189 | 190 | 那什么又是句柄还原呢? 191 | ```js 192 | //master js 193 | var server = require('net').createServer() 194 | server.listen(8888, () => { //master监听8888 195 | console.log('master on :', 8888) 196 | }) 197 | .... 198 | 199 | worker.send('server', server)//发送句柄给worker 200 | ``` 201 | 这行代码中我们看到,我们的worker.send()函数发送了一个服务器server对象,在worker中会被收到,然后worker调用他的监听方法,就可以触发worker内部http服务器的逻辑 202 | 203 | 我们看一下worker.send()能填入的参数 204 | 205 | - 参数1:字符串,标示事件名称 206 | 207 | 参数2: 208 | - net.Socket对象: TCP套接字 209 | - net.Server对象: TCP服务器 210 | - net.Native: C++层面的TCP套接字和IPC管道 211 | - dgram.Socket: UDP套接字 212 | - dgram.Native: C++层面的UDP套接字 213 | 214 | 刚刚我们发送的就是 net.Server对象: TCP服务器 215 | 216 | 传递过程是这样: 217 | 218 | master: 219 | - 传递消息和句柄。(worker.send()...) 220 | - 将消息包装成内部消息,使用 JSON.stringify 序列化为字符串。(send的内部做的事情) 221 | - 通过对应的 handleConversion[message.type].send 方法序列化句柄。(用于告诉worker到底发送了什么类型的参数) 222 | - 将序列化后的字符串和句柄发入 IPC channel 。(完成序列化,进入发送阶段) 223 | 224 | worker: 225 | - 使用 JSON.parse 反序列化消息字符串为消息对象。(刚刚序列化了,现在反序列化) 226 | - 触发内部消息事件(internalMessage)监听器。 227 | - 将传递来的句柄使用 handleConversion[message.type].got 方法反序列化为 JavaScript 对象。(获取到底是什么参数传过来) 228 | - 带着消息对象中的具体消息内容和反序列化后的句柄对象,触发用户级别事件。 229 | 230 | 发送TCP服务器为例,worker是这样还原句柄的: 231 | 232 | ```js 233 | Convertion(message,handle,emit)=>{ 234 | var server = new handleConversion[message.type].got()//其实就是获取tcp服务器类型 235 | server.listen(handle,()=>{ 236 | emit(server); 237 | }) 238 | } 239 | ``` 240 | # 总结 241 | 到此,通过种种手段我们已经构建出了一个多进程架构的Node.js服务器,重头看看我们到底解决了多少个问题: 242 | 1. 单进程不稳,我们就多进程 243 | 2. 多进程我们碰到了端口被多个worker占据报错 244 | 3. 代理模式,但是会导致文件描述符消耗翻倍 245 | 4. 通过发送句柄的方式,我们轻松的解决了以上所有问题 246 | 247 | 248 | -------------------------------------------------------------------------------- /docs/modulec++.md: -------------------------------------------------------------------------------- 1 | # 深入底层:Node.js启动和模块加载 2 | 3 | 之前已经从js层面看了 nodejs 代码,那么今天不多说,直接来看深入至 c++底层的源码. 4 | 5 | # 从大名鼎鼎的main开始 6 | 7 | ```js 8 | //node_main.cc 9 | int main(int argc, char *argv[]) { 10 | return node::Start(argc, argv); 11 | } 12 | ``` 13 | 除去一些平台判断代码,我们来到了最出名的c语言函数,这个函数其实只是为了引出```node::Start(argc, argv);```,我们继续深入进去看 14 | ```js 15 | int Start(int argc, char** argv) { 16 | //... 17 | V8::Initialize(); 18 | const int exit_code = Start(uv_default_loop(), argc, argv, exec_argc, exec_argv); 19 | //.... 20 | V8::Dispose(); 21 | 22 | v8_platform.Dispose(); 23 | 24 | //.... 25 | return exit_code; 26 | } 27 | ``` 28 | 29 | ```V8::Initialize();```是V8的初始化,然后获取到```libuv```的```uv_default_loop()```之后,又来到了一个```start```函数。 30 | ```js 31 | //.... 32 | LoadEnvironment(&env); 33 | 34 | { 35 | //.... 36 | do { 37 | //事件循环在这里才开始 38 | uv_run(env.event_loop(), UV_RUN_DEFAULT); 39 | 40 | more = uv_loop_alive(env.event_loop()); 41 | } while (more == true); 42 | 43 | ``` 44 | 在深度遍历了几个Start之后,我们来到第一个重要的函数。这个函数做的事情其实就是加载我们```node.js```的乱七八糟模块以及跑我们的一开始的代码了。什么意思呢?其实就是```node hello.js```,第一遍跑我们的代码没有进入事件循环时,就会跑这个代码,再一次证明了,我们的代码执行一开始,并不会进入事件循环,而是跑完所有同步代码以后,才会开始。 45 | 46 | ```js 47 | /* 48 | 在最近的版本中,bootstrap.js被拆分成了loader.js和node.js 49 | 再这里我们看到了v8加载javascript的方法 50 | */ 51 | Local loaders_name = 52 | FIXED_ONE_BYTE_STRING(env->isolate(), "internal/bootstrap/loaders.js"); 53 | 54 | Local loaders_bootstrapper = 55 | GetBootstrapper(env, LoadersBootstrapperSource(env), loaders_name); 56 | 57 | Local node_name = 58 | FIXED_ONE_BYTE_STRING(env->isolate(), "internal/bootstrap/node.js"); 59 | 60 | Local node_bootstrapper = 61 | GetBootstrapper(env, NodeBootstrapperSource(env), node_name); 62 | 63 | // Add a reference to the global object 64 | Local global = env->context()->Global(); 65 | 66 | // Bootstrap internal loaders 67 | Local bootstrapped_loaders; 68 | if (!ExecuteBootstrapper(env, loaders_bootstrapper, 69 | arraysize(loaders_bootstrapper_args), 70 | loaders_bootstrapper_args, 71 | &bootstrapped_loaders)) { 72 | return; 73 | } 74 | 75 | // Bootstrap Node.js 76 | Local bootstrapped_node; 77 | Local node_bootstrapper_args[] = { 78 | env->process_object(), 79 | bootstrapped_loaders 80 | }; 81 | if (!ExecuteBootstrapper(env, node_bootstrapper, 82 | arraysize(node_bootstrapper_args), 83 | node_bootstrapper_args, 84 | &bootstrapped_node)) { 85 | ``` 86 | 以上代码其实就是做了几件事情: 87 | 1. 初始化global对象 88 | 2. 加载bootstrap中的两个模块 89 | 3. 挂载bootstrap初始化之后的东西到global对象中 90 | 91 | 那么,神秘的bootstrap两个模块到底是什么呢?让我们一探究竟 92 | 93 | # bootstrap/loader.js 94 | 95 | ```js 96 | (function bootstrapInternalLoaders(process, getBinding, getLinkedBinding, 97 | getInternalBinding) { 98 | }); 99 | ``` 100 | loader是一个函数表达式,注意这里使用了```(function(){})```的方式将源码包住,究其原因是为了让V8解析的时候,告诉V8把这段代码解析成一个c++的函数表达式,具体映射到c++,代码主要是 101 | ```js 102 | //加载源码字符串,你看这是String类型 103 | Local loaders_name = 104 | FIXED_ONE_BYTE_STRING(env->isolate(), "internal/bootstrap/loaders.js"); 105 | //这一步使用v8的函数,获取一个function类型 106 | //说明loaders_bootstrapper就是一个函数 107 | Local loaders_bootstrapper = 108 | GetBootstrapper(env, LoadersBootstrapperSource(env), loaders_name); 109 | 110 | //注意看,这里是的参数其实就是对应了bootstrapInternalLoaders中的4个参数 111 | Local loaders_bootstrapper_args[] = { 112 | env->process_object(), 113 | get_binding_fn, 114 | get_linked_binding_fn, 115 | get_internal_binding_fn 116 | }; 117 | 118 | //执行ExecuteBootstrapper 119 | Local bootstrapped_loaders; 120 | if (!ExecuteBootstrapper(env, loaders_bootstrapper, 121 | arraysize(loaders_bootstrapper_args), 122 | loaders_bootstrapper_args, 123 | &bootstrapped_loaders)) { 124 | ``` 125 | 思路很简单,就是代码一大坨而已,接下来我们看看bootstrapInternalLoaders中四个重要的参数是什么 126 | ```js 127 | (function bootstrapInternalLoaders(process, getBinding, getLinkedBinding, 128 | getInternalBinding) { 129 | }); 130 | //process你没看错,就是我们所用的全局process对象 131 | //getBinding其实就是之后的process.binding 132 | //getLinkedBinding用于绑定在process._getLinkedBinding上用于载入c++模块,比如用户写的c++ addon 133 | //getInternalBinding用于获取nodejs c++的内置模块 134 | ``` 135 | 我们往下遍历代码就会看到我们的老朋友 136 | ```js 137 | 138 | 139 | // Set up NativeModule 140 | function NativeModule(id) { 141 | this.filename = `${id}.js`; 142 | this.id = id; 143 | this.exports = {}; 144 | this.loaded = false; 145 | this.loading = false; 146 | } 147 | //构造一个loader,之后用于导出 148 | const loaderExports = { internalBinding, NativeModule }; 149 | NativeModule.require = function(id) { 150 | const nativeModule = new NativeModule(id); 151 | 152 | nativeModule.cache(); 153 | nativeModule.compile(); 154 | 155 | return nativeModule.exports; 156 | } 157 | 158 | return loaderExports; 159 | ``` 160 | NativeModule模块。这个模块其实就是我们用在Node.js中的```module```定义了,可以看见,里面的```this.exports```.我们往下看,终于见到了我们的老朋友```require```,所以在我们一开始调用```node hello.js```时的require是在这里被创建的,也就是Node.js启动的时候。而require之后的结果,永远是```被编译过后的eports对象```. 161 | 162 | 在最后,导出这个loader,还给c++层,然后将loader和process,传递给```bootstrap/node.js```. 163 | 164 | # bootstrap/node.js 165 | 166 | ```js 167 | (function bootstrapNodeJSCore(process, { internalBinding, NativeModule }){ 168 | NativeModule.require('internal/process/warning').setup(); 169 | NativeModule.require('internal/process/next_tick').setup(); 170 | NativeModule.require('internal/process/stdio').setup(); 171 | //..... 172 | evalScript(xxx)//执行我们的代码 173 | } 174 | ``` 175 | ```bootstrapNodeJSCore```就是我们的启动函数了,这个脚本跑完,所有的同步代码会被执行完毕我,我们看到这里传递进来了```process```和我们需要的第一个```NativeModule``` 176 | 177 | 在这段函数中,其实做的就是初始化,比如加载console,记载next_tick等等...我们用户指定的代码由```evalScript``进行调用 178 | ```js 179 | function evalScript(name) { 180 | const CJSModule = NativeModule.require('internal/modules/cjs/loader'); 181 | const path = NativeModule.require('path'); 182 | const cwd = tryGetCwd(path); 183 | 184 | const module = new CJSModule(name); 185 | module.filename = path.join(cwd, name); 186 | module.paths = CJSModule._nodeModulePaths(cwd); 187 | const body = wrapForBreakOnFirstLine(process._eval); 188 | const script = `global.__filename = ${JSON.stringify(name)};\n` + 189 | 'global.exports = exports;\n' + 190 | 'global.module = module;\n' + 191 | 'global.__dirname = __dirname;\n' + 192 | 'global.require = require;\n' + 193 | 'return require("vm").runInThisContext(' + 194 | `${JSON.stringify(body)}, { filename: ` + 195 | `${JSON.stringify(name)}, displayErrors: true });\n`; 196 | const result = module._compile(script, `${name}-wrapper`); 197 | if (process._print_eval) console.log(result); 198 | // Handle any nextTicks added in the first tick of the program. 199 | process._tickCallback(); 200 | } 201 | ``` 202 | 至此,我们揭开了所有谜团,在v8编译这段代码运行的时候,就会被```evalScript```,以字符串拼接的方式,将我们的代码拼接到这里,然后使用```vm.runInThisContext()```的办法去跑我们的code,那么我们的第一次跑就会执行了。在这里值得注意的是,在初始化的时候就会调用,```_tickCallback```方法。 203 | 204 | 205 | # 从头梳理 206 | 207 | - node.js启动的时候,会从c++层面先启动,然后走到bootstrap里面初始化 208 | - 流程是```初始化global对象```->```加载编译内置模块(process,c++等等)```->```运行用户指定脚本```->跑一次```nextTick```->进入事件循环 209 | 210 | 本章内容最好和之前的一起看 211 | 1. [模块化js层:实现一个简单模块加载](https://215566435.github.io/Fz-node/#/home) 212 | 2. [模块化js层2:模块加载之谜](https://215566435.github.io/Fz-node/#/home) 213 | -------------------------------------------------------------------------------- /docs/a-nextTick.md: -------------------------------------------------------------------------------- 1 | # 深入nextTick源码:使用ES6优化数组操作 2 | 3 | ```nextTick```是 node 中非常出名的一个函数,其运行周期在微任务之前,又在一次事件循环末尾。这个函数在工业中被大量使用,今天我们就深度剖析一下这个函数的实现极其node开发者为其做的优化. 4 | 5 | # 最开始 6 | 7 | 在比较早的版本中,nexTick回调函数会被,在本轮事件循环末尾,Promise(微任务)之前推进一个队列,我们叫这个队列: 8 | ```js 9 | var nexTickCallback = []; 10 | ``` 11 | 12 | 早期实现中,这并没有什么问题,直到后来这个pr的出现:[#13446](https://github.com/nodejs/node/pull/13446),这个哥们发现了一个神奇的现象:使用```es6```构造一个数组,手动添加clear,push,shift等方法,比原生的```[]```要快接近```20%```.以下是结果: 13 | ```js 14 | improvement confidence p.value 15 | process/next-tick-breadth-args.js millions=2 27.75 % *** 1.271176e-20 16 | process/next-tick-breadth.js millions=2 7.71 % *** 4.155765e-13 17 | process/next-tick-depth-args.js millions=12 47.78 % *** 4.150674e-52 18 | process/next-tick-depth.js millions=12 47.32 % *** 7.742778e-31 19 | 20 | ``` 21 | 那么他给nextTick添加了一个什么代码呢?没有错,他只是简单的使用es6的class自己写了一个function. 22 | ```js 23 | class NextTickQueue { 24 | constructor() { 25 | this.head = null 26 | this.tail = null 27 | this.length = 0 28 | } 29 | 30 | push(v) { 31 | const entry = { data: v, next: null } 32 | if (this.length > 0) this.tail.next = entry 33 | else this.head = entry 34 | this.tail = entry 35 | ++this.length 36 | } 37 | 38 | shift() { 39 | if (this.length === 0) return 40 | const ret = this.head.data 41 | if (this.length === 1) this.head = this.tail = null 42 | else this.head = this.head.next 43 | --this.length 44 | return ret 45 | } 46 | 47 | clear() { 48 | this.head = null 49 | this.tail = null 50 | this.length = 0 51 | } 52 | } 53 | ``` 54 | 这个函数是有通用性的,也就是说我们可以运用到现实生活中去优化我们队列的大量操作。为此,我特地写了一份测试函数。得到的结果真实让人有点兴奋,哈哈. 55 | ``` 56 | ➜ 57 | 使用构造函数的array: 192 58 | 普通array: 196 59 | ➜ 60 | 使用构造函数的array: 186 61 | 普通array: 185 62 | ➜ 63 | 使用构造函数的array: 161 64 | 普通array: 188 65 | ➜ 66 | 使用构造函数的array: 162 67 | 普通array: 189 68 | ➜ 69 | 使用构造函数的array: 152 70 | 普通array: 188 71 | ➜ 72 | 使用构造函数的array: 169 73 | 普通array: 185 74 | ➜ 75 | 使用构造函数的array: 163 76 | 普通array: 191 77 | ➜ 78 | 使用构造函数的array: 162 79 | 普通array: 186 80 | ➜ 81 | 使用构造函数的array: 225 82 | 普通array: 208 83 | ➜ 84 | 使用构造函数的array: 201 85 | 普通array: 205 86 | ➜ 87 | 使用构造函数的array: 186 88 | 普通array: 189 89 | ➜ 90 | 使用构造函数的array: 165 91 | 普通array: 191 92 | ➜ 93 | 使用构造函数的array: 153 94 | 普通array: 182 95 | ➜ 96 | 使用构造函数的array: 212 97 | 普通array: 196 98 | ➜ 99 | 使用构造函数的array: 184 100 | 普通array: 257 101 | ➜ 102 | 使用构造函数的array: 174 103 | 普通array: 207 104 | ➜ 105 | 使用构造函数的array: 167 106 | 普通array: 187 107 | ➜ 108 | 使用构造函数的array: 157 109 | 普通array: 193 110 | ➜ 111 | 使用构造函数的array: 228 112 | 普通array: 192 113 | ➜ 114 | 使用构造函数的array: 178 115 | 普通array: 218 116 | ➜ 117 | 使用构造函数的array: 163 118 | 普通array: 215 119 | ➜ 120 | 使用构造函数的array: 157 121 | 普通array: 181 122 | ➜ 123 | 使用构造函数的array: 158 124 | 普通array: 183 125 | ➜ 126 | 使用构造函数的array: 237 127 | 普通array: 197 128 | ➜ 129 | 使用构造函数的array: 209 130 | 普通array: 192 131 | ➜ 132 | 使用构造函数的array: 163 133 | 普通array: 189 134 | ➜ 135 | 使用构造函数的array: 169 136 | 普通array: 210 137 | ``` 138 | 在上述的测试中,我们可以发现,大部分情况下,使用es6构造的数组,要比普通的```[]```要快上很多,最大的差距到达了50ms. 139 | 140 | # 使用一个特殊的可重用单向链表去优化速度 141 | 142 | 又过了一段时间,nextTick的实现再次被踢翻,具体的pr再这里:[pr:#18617](https://github.com/nodejs/node/pull/18617),这位哥们的做法更加变态:他的思路其实很简单,我们```push```操作的时候,系统都会申请一块新的空间来存储,清理的时候会将一大块内存都清理掉,那么这样实在是有点浪费,不如一次性申请好一堆内存,push的时候按位置放进去不就完了?于是有了现在的实现: 143 | ```js 144 | // 现在的设计变成了这样子:是一个单项链表,每个链表中的元素,都有一个固定为2048长度的数组 145 | // 如果单次注册回调的次数少于2048次,那么只会一次性分出2048个长度的array提供使用 146 | //这2048长度的数组中的内存是可以重复使用的 147 | // 148 | // head tail 149 | // | | 150 | // v v 151 | // +-----------+ <-----\ +-----------+ <------\ +-----------+ 152 | // | [null] | \----- | next | \------- | next | 153 | // +-----------+ +-----------+ +-----------+ 154 | // | tick | <-- bottom | tick | <-- bottom | [empty] | 155 | // | tick | | tick | | [empty] | 156 | // | tick | | tick | | [empty] | 157 | // | tick | | tick | | [empty] | 158 | // | tick | | tick | bottom --> | tick | 159 | // | tick | | tick | | tick | 160 | // | ... | | ... | | ... | 161 | // | tick | | tick | | tick | 162 | // | tick | | tick | | tick | 163 | // | [empty] | <-- top | tick | | tick | 164 | // | [empty] | | tick | | tick | 165 | // | [empty] | | tick | | tick | 166 | // +-----------+ +-----------+ <-- top top --> +-----------+ 167 | // 168 | //回调比较少的情况 169 | // head tail head tail 170 | // | | | | 171 | // v v v v 172 | // +-----------+ +-----------+ 173 | // | [null] | | [null] | 174 | // +-----------+ +-----------+ 175 | // | [empty] | | tick | 176 | // | [empty] | | tick | 177 | // | tick | <-- bottom top --> | [empty] | 178 | // | tick | | [empty] | 179 | // | [empty] | <-- top bottom --> | tick | 180 | // | [empty] | | tick | 181 | // +-----------+ +-----------+ 182 | // 183 | //当往队列中插入一个callback的时候,top就会往下走一个格子 184 | //当从中取出的时候,bottom也会从中取出一个,如果不为空,则直接返回,调整bottom的位置往下走 185 | // 186 | // 187 | //判断一个表是否满了或者全空非常简单(2048),当top===bottom的时候,list[top] !== undefine 那就是满了 188 | //会重新生成一个表 189 | //如果top===bottom && list[top] === void 666 190 | //那就证明,这个表已经空了 191 | ``` 192 | 经过测试,总体性能又拔高了```40%```. 193 | 194 | ```bash 195 | confidence improvement accuracy (*) (**) (***) 196 | process/next-tick-breadth-args.js millions=4 *** 40.11 % ±1.23% ±1.64% ±2.14% 197 | process/next-tick-breadth.js millions=4 *** 7.16 % ±3.50% ±4.67% ±6.11% 198 | process/next-tick-depth-args.js millions=12 *** 5.46 % ±0.91% ±1.22% ±1.59% 199 | process/next-tick-depth.js millions=12 *** 23.26 % ±2.51% ±3.36% ±4.40% 200 | process/next-tick-exec-args.js millions=5 *** 38.64 % ±1.16% ±1.55% ±2.01% 201 | process/next-tick-exec.js millions=5 *** 77.20 % ±1.63% ±2.18% ±2.88% 202 | 203 | Be aware that when doing many comparisions the risk of a false-positive 204 | result increases. In this case there are 6 comparisions, you can thus 205 | expect the following amount of false-positive results: 206 | 0.30 false positives, when considering a 5% risk acceptance (*, **, ***), 207 | 0.06 false positives, when considering a 1% risk acceptance (**, ***), 208 | 0.01 false positives, when considering a 0.1% risk acceptance (***) 209 | ``` 210 | 211 | # 总结 212 | 实际上,这个nextTick依旧有优化的空间可以发挥:使用类似 node.js bufferlist,不过很容易导致内存泄漏。 213 | 214 | - 如果有大量操作列表的操作,我们可以使用以上的优化方法 215 | - 本篇作为nextTrick队列实现的附录,并不涉及事件循环等要素 216 | 217 | 更多章节可以在:[不伤眼的版本](https://github.com/215566435/Fz-node)中找到。 218 | 219 | -------------------------------------------------------------------------------- /docs/stream.md: -------------------------------------------------------------------------------- 1 | # Stream 模块解读前言 2 | 本章节网上已经出现很多的解析教程,无论是从源码还是从原理。我也是通过读源码与看别人教程去理解这一块代码是如何书写的,因此本文是对网上教程的一点补充说明,并且加入我的理解。Stream模块比较庞大,需要的知识可能包括: 3 | 1. 理解事件循环机制 4 | 2. 明白stream出现痛点 5 | 3. 流的使用场景 6 | 7 | 最后,本文是对以下三篇文章的补充,主要讲解了,读写流中,异步push和异步next的情况: 8 | 1. https://tech.meituan.com/stream-basics.html 9 | 2. https://tech.meituan.com/stream-internals.html 10 | 3. https://tech.meituan.com/stream-in-action.html 11 | 12 | 13 | # Stream 模块解读补充 14 | 想要看到```Stream```模块的解读,可能需要比较多的实践功底,因为里面其中很多的概念是需要**用过**才会明白的,如果大家没有好的练手方案我提供三个: 15 | 16 | 1. 日志系统:日志系统非常适合用流去做,比如,日志的流试输出,为了高性能需要每隔1s间隔才去写文件,日志的转换(切分,格式转换等等操作) 17 | 2. 反向代理:流试转发,后端数据流试拼接(省内存)等等 18 | 19 | # 流的几种概念 20 | 21 | Node 为我们提供了4种可读的流,分别是下面几种: 22 | 23 | ```js 24 | var Stream = require('stream') 25 | 26 | var Readable = Stream.Readable 27 | var Writable = Stream.Writable 28 | var Duplex = Stream.Duplex 29 | var Transform = Stream.Transform 30 | ``` 31 | 32 | # Readable 33 | 这是一种可读流,可读流一般代表的是数据的来源,这个流指定了「如何去读取数据」,以下是简单的例子: 34 | ```js 35 | const Readable = require('stream').Readable 36 | 37 | class FzReadable extends Readable { 38 | constructor(iterator) { 39 | super() 40 | this.iterator = iterator 41 | } 42 | 43 | //子类必须实现的方法 44 | _read() { 45 | const res = this.iterator.next() 46 | if (res.done) { 47 | //当收到null的时候,流就停止了 48 | return this.push(null) 49 | } 50 | 51 | setTimeout(() => { 52 | this.push(`${res.value}\n`) 53 | }, 0) 54 | } 55 | } 56 | 57 | const iterator = (function(limit) { 58 | return { 59 | next: function() { 60 | if (limit--) { 61 | return { done: false, value: limit + Math.random() } 62 | } 63 | return { done: true } 64 | } 65 | } 66 | })(10) 67 | 68 | const readable = new FzReadable(iterator) 69 | 70 | //调用on('data')就会进入流动模式,数据会自发地通过data事件输出,不需要消耗方反复调用read(n)。 71 | //调用on('data')会在nexttick中使用read(0)方法去请求数据 72 | readable.on('data', data => process.stdout.write(data)) 73 | readable.on('end', () => process.stdout.write('DONE')) 74 | ``` 75 | 76 | 上述的代码会创建一个我们自己写的可读流,然后从迭代器```iterator```缓慢取出数据,输出到stdout上。 77 | 78 | 继承可读流,需要重写```_read()```方法,当流开始读取的时候,就会调用这个函数,去数据源中获取数据,```this.push()```方法就是会将数据源推入可读流中的缓存队列中,然后在输出数据时,再从缓存队列中取出流的```chunk(分片)```。 79 | 80 | ```_read()```函数是有大文章的函数,还有的是,我们看到```this.push()```是一个异步调用的方法,这么做的原因是:将数据推入缓存并且输出的过程加入事件循环队列中,使得事件循环不会阻塞。具体的,我们后文看源码进行分析。 81 | 82 | # Writable 83 | 一个可写流,可写流的比较简单,我们来看看代码: 84 | ```js 85 | const Writable = require('stream').Writable 86 | 87 | const writable = Writable() 88 | // 实现`_write`方法 89 | // 这是将数据写入底层的逻辑 90 | writable._write = function(data, enc, next) { 91 | 92 | // 将流中的数据写入底层 93 | process.stdout.write(data.toString().toUpperCase()) 94 | // 写入完成时,调用`next()`方法通知流传入下一个数据 95 | process.nextTick(next) 96 | } 97 | 98 | // 所有的数据都写完了 99 | writable.on('finish', () => process.stdout.write('DONE')) 100 | 101 | //数据源 102 | const data = [1, 2, 3, 4, 5, 6, 7] 103 | while (true) { 104 | // 将一个数据写入流中 105 | writable.write(data.shift() + '\n') 106 | //数据空的时候退出 107 | if(data.length ===0)break; 108 | } 109 | // 再无数据写入流时,需要调用`end`方法 110 | writable.end() 111 | ``` 112 | 上面例子中,可写流需要实现```_write```方法制定写的方式,我们可以注意到这个函数有3个参数: 113 | 114 | - ```data```:顾名思义,就是每次外部调用```write```时的方法 115 | - ```enc```:数据类型,一般值是buffer 116 | - ```next```:```next```除了是***通知流传入下一个数据***之外,还有另外一个作用就是冲刷内部缓存,这个函数是可以异步调用,也是同步调用的,异步调用是为了让读流能够加入事件循环,不会阻塞 117 | 118 | 119 | # 异步读和异步写 120 | 121 | 在上面我们一直说到无论是读的```push```还是写的```next```,都提到了一个相当重要的概念:**这个函数是可以异步调用,也是同步调用的,异步调用是为了让读流能够加入事件循环,不会阻塞**,为了更好的理解其中的奥妙,我们用一段代码进行解释: 122 | ```js 123 | const Readable = require('stream').Readable 124 | 125 | class FzReadable extends Readable { 126 | constructor(iterator) { 127 | super() 128 | this.iterator = iterator 129 | } 130 | _read() { 131 | const res = this.iterator.next() 132 | if (res.done) { 133 | return this.push(null) 134 | } 135 | 136 | setTimeout(() => { 137 | this.push(`${res.value}\n`) 138 | }, 0) 139 | } 140 | } 141 | 142 | const iterator = (function(limit) { 143 | return { 144 | next: function() { 145 | if (limit--) { 146 | return { done: false, value: limit + Math.random() } 147 | } 148 | return { done: true } 149 | } 150 | } 151 | })(10) 152 | 153 | const readable = new FzReadable(iterator) 154 | 155 | 156 | readable.on('data', data => process.stdout.write(data)) 157 | 158 | const timer = setInterval(() => { 159 | console.log('哈哈哈') 160 | }, 0) 161 | 162 | readable.on('end', () => { 163 | process.stdout.write('DONE') 164 | clearInterval(timer) 165 | }) 166 | ``` 167 | 在上述这个例子中,我们使用了异步的方式```push```缓存,然后我们在末尾加入一个定时器,在每个事件循环```timer```阶段,我们打印一个```哈哈哈```,我们看看效果: 168 | ``` 169 | 哈哈哈 170 | 9.769079623794424 171 | 哈哈哈 172 | 8.078209992424412 173 | 哈哈哈 174 | 7.089890373541063 175 | 哈哈哈 176 | 6.2639577098912795 177 | 哈哈哈 178 | 5.512806573260017 179 | 哈哈哈 180 | 4.843215892958035 181 | 哈哈哈 182 | 3.3624366732744377 183 | 哈哈哈 184 | 2.074261391179594 185 | 哈哈哈 186 | 1.651073865927396 187 | 哈哈哈 188 | 0.9808023604686888 189 | 哈哈哈 190 | DONE 191 | ``` 192 | 这么做的优势可能还没现实出来,我们把代码改一下,改成同步push试试: 193 | ```js 194 | 195 | setTimeout(() => { 196 | this.push(`${res.value}\n`) 197 | }, 0) 198 | /** 199 | * | 200 | * |我们将this.push从中拿出来,看看结果 201 | * | 202 | * V 203 | */ 204 | this.push(`${res.value}\n`) 205 | /**输出 206 | * 9.296256613905719 207 | 8.800861871409845 208 | 7.930317062766291 209 | 6.694684450371433 210 | 5.354302950365952 211 | 4.035660878052062 212 | 3.1199030341724496 213 | 2.8361767840214345 214 | 1.872803351560663 215 | 0.9070988743704766 216 | DONE% 217 | * 218 | */ 219 | ``` 220 | 我们实际上只改了一行代码,但是```哈哈哈```没了,要解释为什么造成这样差异的原因我们需要很多知识,但是总结来说就是:如果我们使用同步的读数据和写数据,不管你是不是```流```那都会阻塞事件循环,因此,不要以为使用流以后性能就好,我们还得将流的读写,安排到事件循环中去,形成了「绝对不阻塞事件循环」的作用。 221 | 222 | # 双工流和转换流 223 | 在这里我们进行简单的介绍即可 224 | - 双工流:代表可读可写,这种流可以作为读流,也可以作为写流,但是读和写是分裂开来的。 225 | - 转换流:代表一种可读可写的流,但是他们的读和写并不分裂开,通过实现```_transform```方法,可以做到,读数据->转变数据->输出数据 226 | 227 | 228 | # 可读流工作原理 229 | 230 | 我们在完成一个可读流,并且注入迭代器以后,调用```readable.on('data', data => process.stdout.write(data))```,方法就能**拉起流动**,内部的机制是一个大```while```循环调用```read```去读取数据,这也就是为什么同步会阻塞的**根本原因**,具体的代码: 231 | ```js 232 | //_stream_readable.js 233 | function flow(stream) { 234 | const state = stream._readableState; 235 | debug('flow', state.flowing); 236 | while (state.flowing && stream.read() !== null); 237 | } 238 | ``` 239 | 当我们异步调用```this.push```的时候,```stream.read()```永远会返回```null```。但是,在同步的情况下大循环```while```会不断的跑read函数,read函数会触发更底层用户定制的```_read()```函数,然后又会触发同步的```push```,这样就形成了一个持续产生数据的循环。 240 | 241 | # 探究异步调用push 242 | 在介绍异步push的机制之前,我们需要搞清楚,流是有```流动模式```和```暂停模式```的。流动模式比较简单,就是我们上面一直在讲的。在流动模式下,调用```on('data')```,无论你的```push```是否异步,就会使得流自动的进入自动循环读取数据的模式,如果不做其他任何操作,异步和同步的如下: 243 | 244 | - 无论是异步还是同步push,stream中均维持一个缓存队列,数据的大小超过缓存队列的大小(hightWaterMark)就会被自动切分 245 | - 异步push能够加入事件循环,不会导致进程阻塞,但是不会进入缓存,而是直接输出到```on('data')```中 246 | - 同步的push,会阻塞线程,但是会加入缓存中,使得缓存队列变长 247 | 248 | 249 | 具体看下面代码 250 | ```js 251 | if (state.flowing && state.length === 0 && !state.sync) { 252 | stream.emit('data', chunk); 253 | stream.read(0); 254 | } else { 255 | // update the buffer info. 256 | state.length += state.objectMode ? 1 : chunk.length; 257 | if (addToFront) 258 | state.buffer.unshift(chunk); 259 | else 260 | state.buffer.push(chunk); 261 | 262 | if (state.needReadable) 263 | emitReadable(stream); 264 | } 265 | maybeReadMore(stream, state); 266 | ``` 267 | 这段代码就是主要的push函数分支,```state.flowing===true```的时候,就是流动模式,```!state.sync```判断是否异步,如果是流动模式而且又是异步,那么就会直接到```emit('data')```分支中,并不会加入到```state.buffer```这个缓存中去。 268 | 269 | # readable 事件 270 | 当```push```是异步的时候,而又是暂停模式的时候,我们就会进入下面的分支,把数据加入缓存中,设置缓存长度。注意,如果此时读进来的```chunk```太大,缓存的长度已经满了,那么```read```的时候就会从缓存中读取数据而不会从原始数据区```_read()```取数据。具体的流程如下: 271 | ```js 272 | // 读数据 273 | // | 274 | // | 275 | // V 276 | // 缓存池满了吗 277 | // | \ 278 | // 没有 \ 279 | // | 满了 280 | // V V 281 | // _read() 从缓存state.buffer中读 282 | // | 283 | // | 284 | // V 285 | // 放入缓存state.buffer 286 | ``` 287 | 可见,通过这么一来一回,我们的stream模块就具备了一个能自动切分读取数据+异步功能. 288 | 289 | 290 | -------------------------------------------------------------------------------- /libuv-simple/libuv-simple.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | B9592908206AFDFA0087B1F4 /* main.c in Sources */ = {isa = PBXBuildFile; fileRef = B9592907206AFDFA0087B1F4 /* main.c */; }; 11 | B9592910206AFF650087B1F4 /* libuv.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B959290F206AFF650087B1F4 /* libuv.a */; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXCopyFilesBuildPhase section */ 15 | B9592902206AFDFA0087B1F4 /* CopyFiles */ = { 16 | isa = PBXCopyFilesBuildPhase; 17 | buildActionMask = 2147483647; 18 | dstPath = /usr/share/man/man1/; 19 | dstSubfolderSpec = 0; 20 | files = ( 21 | ); 22 | runOnlyForDeploymentPostprocessing = 1; 23 | }; 24 | /* End PBXCopyFilesBuildPhase section */ 25 | 26 | /* Begin PBXFileReference section */ 27 | B9592904206AFDFA0087B1F4 /* libuv-simple */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "libuv-simple"; sourceTree = BUILT_PRODUCTS_DIR; }; 28 | B9592907206AFDFA0087B1F4 /* main.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = main.c; sourceTree = ""; }; 29 | B959290F206AFF650087B1F4 /* libuv.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libuv.a; path = "../../../../../usr/local/Cellar/libuv/HEAD-edf05b9/lib/libuv.a"; sourceTree = ""; }; 30 | B9592911206B07E80087B1F4 /* text.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = text.txt; sourceTree = ""; }; 31 | /* End PBXFileReference section */ 32 | 33 | /* Begin PBXFrameworksBuildPhase section */ 34 | B9592901206AFDFA0087B1F4 /* Frameworks */ = { 35 | isa = PBXFrameworksBuildPhase; 36 | buildActionMask = 2147483647; 37 | files = ( 38 | B9592910206AFF650087B1F4 /* libuv.a in Frameworks */, 39 | ); 40 | runOnlyForDeploymentPostprocessing = 0; 41 | }; 42 | /* End PBXFrameworksBuildPhase section */ 43 | 44 | /* Begin PBXGroup section */ 45 | B95928FB206AFDFA0087B1F4 = { 46 | isa = PBXGroup; 47 | children = ( 48 | B9592906206AFDFA0087B1F4 /* libuv-simple */, 49 | B9592905206AFDFA0087B1F4 /* Products */, 50 | B959290E206AFF650087B1F4 /* Frameworks */, 51 | ); 52 | sourceTree = ""; 53 | }; 54 | B9592905206AFDFA0087B1F4 /* Products */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | B9592904206AFDFA0087B1F4 /* libuv-simple */, 58 | ); 59 | name = Products; 60 | sourceTree = ""; 61 | }; 62 | B9592906206AFDFA0087B1F4 /* libuv-simple */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | B9592907206AFDFA0087B1F4 /* main.c */, 66 | B9592911206B07E80087B1F4 /* text.txt */, 67 | ); 68 | path = "libuv-simple"; 69 | sourceTree = ""; 70 | }; 71 | B959290E206AFF650087B1F4 /* Frameworks */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | B959290F206AFF650087B1F4 /* libuv.a */, 75 | ); 76 | name = Frameworks; 77 | sourceTree = ""; 78 | }; 79 | /* End PBXGroup section */ 80 | 81 | /* Begin PBXNativeTarget section */ 82 | B9592903206AFDFA0087B1F4 /* libuv-simple */ = { 83 | isa = PBXNativeTarget; 84 | buildConfigurationList = B959290B206AFDFA0087B1F4 /* Build configuration list for PBXNativeTarget "libuv-simple" */; 85 | buildPhases = ( 86 | B9592900206AFDFA0087B1F4 /* Sources */, 87 | B9592902206AFDFA0087B1F4 /* CopyFiles */, 88 | B9592901206AFDFA0087B1F4 /* Frameworks */, 89 | ); 90 | buildRules = ( 91 | ); 92 | dependencies = ( 93 | ); 94 | name = "libuv-simple"; 95 | productName = "libuv-simple"; 96 | productReference = B9592904206AFDFA0087B1F4 /* libuv-simple */; 97 | productType = "com.apple.product-type.tool"; 98 | }; 99 | /* End PBXNativeTarget section */ 100 | 101 | /* Begin PBXProject section */ 102 | B95928FC206AFDFA0087B1F4 /* Project object */ = { 103 | isa = PBXProject; 104 | attributes = { 105 | LastUpgradeCheck = 0920; 106 | ORGANIZATIONNAME = "Z F"; 107 | TargetAttributes = { 108 | B9592903206AFDFA0087B1F4 = { 109 | CreatedOnToolsVersion = 9.2; 110 | ProvisioningStyle = Automatic; 111 | }; 112 | }; 113 | }; 114 | buildConfigurationList = B95928FF206AFDFA0087B1F4 /* Build configuration list for PBXProject "libuv-simple" */; 115 | compatibilityVersion = "Xcode 8.0"; 116 | developmentRegion = en; 117 | hasScannedForEncodings = 0; 118 | knownRegions = ( 119 | en, 120 | ); 121 | mainGroup = B95928FB206AFDFA0087B1F4; 122 | productRefGroup = B9592905206AFDFA0087B1F4 /* Products */; 123 | projectDirPath = ""; 124 | projectRoot = ""; 125 | targets = ( 126 | B9592903206AFDFA0087B1F4 /* libuv-simple */, 127 | ); 128 | }; 129 | /* End PBXProject section */ 130 | 131 | /* Begin PBXSourcesBuildPhase section */ 132 | B9592900206AFDFA0087B1F4 /* Sources */ = { 133 | isa = PBXSourcesBuildPhase; 134 | buildActionMask = 2147483647; 135 | files = ( 136 | B9592908206AFDFA0087B1F4 /* main.c in Sources */, 137 | ); 138 | runOnlyForDeploymentPostprocessing = 0; 139 | }; 140 | /* End PBXSourcesBuildPhase section */ 141 | 142 | /* Begin XCBuildConfiguration section */ 143 | B9592909206AFDFA0087B1F4 /* Debug */ = { 144 | isa = XCBuildConfiguration; 145 | buildSettings = { 146 | ALWAYS_SEARCH_USER_PATHS = NO; 147 | CLANG_ANALYZER_NONNULL = YES; 148 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 149 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 150 | CLANG_CXX_LIBRARY = "libc++"; 151 | CLANG_ENABLE_MODULES = YES; 152 | CLANG_ENABLE_OBJC_ARC = YES; 153 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 154 | CLANG_WARN_BOOL_CONVERSION = YES; 155 | CLANG_WARN_COMMA = YES; 156 | CLANG_WARN_CONSTANT_CONVERSION = YES; 157 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 158 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 159 | CLANG_WARN_EMPTY_BODY = YES; 160 | CLANG_WARN_ENUM_CONVERSION = YES; 161 | CLANG_WARN_INFINITE_RECURSION = YES; 162 | CLANG_WARN_INT_CONVERSION = YES; 163 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 164 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 165 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 166 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 167 | CLANG_WARN_STRICT_PROTOTYPES = YES; 168 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 169 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 170 | CLANG_WARN_UNREACHABLE_CODE = YES; 171 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 172 | CODE_SIGN_IDENTITY = "Mac Developer"; 173 | COPY_PHASE_STRIP = NO; 174 | DEBUG_INFORMATION_FORMAT = dwarf; 175 | ENABLE_STRICT_OBJC_MSGSEND = YES; 176 | ENABLE_TESTABILITY = YES; 177 | GCC_C_LANGUAGE_STANDARD = gnu11; 178 | GCC_DYNAMIC_NO_PIC = NO; 179 | GCC_NO_COMMON_BLOCKS = YES; 180 | GCC_OPTIMIZATION_LEVEL = 0; 181 | GCC_PREPROCESSOR_DEFINITIONS = ( 182 | "DEBUG=1", 183 | "$(inherited)", 184 | ); 185 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 186 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 187 | GCC_WARN_UNDECLARED_SELECTOR = YES; 188 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 189 | GCC_WARN_UNUSED_FUNCTION = YES; 190 | GCC_WARN_UNUSED_VARIABLE = YES; 191 | MACOSX_DEPLOYMENT_TARGET = 10.12; 192 | MTL_ENABLE_DEBUG_INFO = YES; 193 | ONLY_ACTIVE_ARCH = YES; 194 | SDKROOT = macosx; 195 | }; 196 | name = Debug; 197 | }; 198 | B959290A206AFDFA0087B1F4 /* Release */ = { 199 | isa = XCBuildConfiguration; 200 | buildSettings = { 201 | ALWAYS_SEARCH_USER_PATHS = NO; 202 | CLANG_ANALYZER_NONNULL = YES; 203 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 204 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 205 | CLANG_CXX_LIBRARY = "libc++"; 206 | CLANG_ENABLE_MODULES = YES; 207 | CLANG_ENABLE_OBJC_ARC = YES; 208 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 209 | CLANG_WARN_BOOL_CONVERSION = YES; 210 | CLANG_WARN_COMMA = YES; 211 | CLANG_WARN_CONSTANT_CONVERSION = YES; 212 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 213 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 214 | CLANG_WARN_EMPTY_BODY = YES; 215 | CLANG_WARN_ENUM_CONVERSION = YES; 216 | CLANG_WARN_INFINITE_RECURSION = YES; 217 | CLANG_WARN_INT_CONVERSION = YES; 218 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 219 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 220 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 221 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 222 | CLANG_WARN_STRICT_PROTOTYPES = YES; 223 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 224 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 225 | CLANG_WARN_UNREACHABLE_CODE = YES; 226 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 227 | CODE_SIGN_IDENTITY = "Mac Developer"; 228 | COPY_PHASE_STRIP = NO; 229 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 230 | ENABLE_NS_ASSERTIONS = NO; 231 | ENABLE_STRICT_OBJC_MSGSEND = YES; 232 | GCC_C_LANGUAGE_STANDARD = gnu11; 233 | GCC_NO_COMMON_BLOCKS = YES; 234 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 235 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 236 | GCC_WARN_UNDECLARED_SELECTOR = YES; 237 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 238 | GCC_WARN_UNUSED_FUNCTION = YES; 239 | GCC_WARN_UNUSED_VARIABLE = YES; 240 | MACOSX_DEPLOYMENT_TARGET = 10.12; 241 | MTL_ENABLE_DEBUG_INFO = NO; 242 | SDKROOT = macosx; 243 | }; 244 | name = Release; 245 | }; 246 | B959290C206AFDFA0087B1F4 /* Debug */ = { 247 | isa = XCBuildConfiguration; 248 | buildSettings = { 249 | ALWAYS_SEARCH_USER_PATHS = NO; 250 | CODE_SIGN_STYLE = Automatic; 251 | DEVELOPMENT_TEAM = LHQ6R5P9F7; 252 | HEADER_SEARCH_PATHS = ( 253 | /usr/local/, 254 | /usr/local/Cellar/, 255 | "/usr/local/Cellar/libuv/HEAD-edf05b9/", 256 | "/usr/local/Cellar/libuv/HEAD-edf05b9/include", 257 | /usr/local/include, 258 | /usr/local/include/node, 259 | ); 260 | LIBRARY_SEARCH_PATHS = "/usr/local/Cellar/libuv/HEAD-edf05b9/lib"; 261 | PRODUCT_NAME = "$(TARGET_NAME)"; 262 | }; 263 | name = Debug; 264 | }; 265 | B959290D206AFDFA0087B1F4 /* Release */ = { 266 | isa = XCBuildConfiguration; 267 | buildSettings = { 268 | ALWAYS_SEARCH_USER_PATHS = NO; 269 | CODE_SIGN_STYLE = Automatic; 270 | DEVELOPMENT_TEAM = LHQ6R5P9F7; 271 | HEADER_SEARCH_PATHS = ( 272 | /usr/local/, 273 | /usr/local/Cellar/, 274 | "/usr/local/Cellar/libuv/HEAD-edf05b9/", 275 | "/usr/local/Cellar/libuv/HEAD-edf05b9/include", 276 | /usr/local/include, 277 | /usr/local/include/node, 278 | ); 279 | LIBRARY_SEARCH_PATHS = "/usr/local/Cellar/libuv/HEAD-edf05b9/lib"; 280 | PRODUCT_NAME = "$(TARGET_NAME)"; 281 | }; 282 | name = Release; 283 | }; 284 | /* End XCBuildConfiguration section */ 285 | 286 | /* Begin XCConfigurationList section */ 287 | B95928FF206AFDFA0087B1F4 /* Build configuration list for PBXProject "libuv-simple" */ = { 288 | isa = XCConfigurationList; 289 | buildConfigurations = ( 290 | B9592909206AFDFA0087B1F4 /* Debug */, 291 | B959290A206AFDFA0087B1F4 /* Release */, 292 | ); 293 | defaultConfigurationIsVisible = 0; 294 | defaultConfigurationName = Release; 295 | }; 296 | B959290B206AFDFA0087B1F4 /* Build configuration list for PBXNativeTarget "libuv-simple" */ = { 297 | isa = XCConfigurationList; 298 | buildConfigurations = ( 299 | B959290C206AFDFA0087B1F4 /* Debug */, 300 | B959290D206AFDFA0087B1F4 /* Release */, 301 | ); 302 | defaultConfigurationIsVisible = 0; 303 | defaultConfigurationName = Release; 304 | }; 305 | /* End XCConfigurationList section */ 306 | }; 307 | rootObject = B95928FC206AFDFA0087B1F4 /* Project object */; 308 | } 309 | -------------------------------------------------------------------------------- /docs/eventloop-libuv.md: -------------------------------------------------------------------------------- 1 | # 事件循环libuv入门 2 | 3 | 想要详细了解 node 中的事件循环是如何运作的,通过看网上的文章我依旧觉得不是很稳。究其原因就是我对libuv的不熟悉,不管是内部原理,还是外部API都是所知甚少。导致在看文章的时候,诸如```uv_loop_open()```等等API甚是陌生。这对于理解事件循环的本质并不是很有帮助,所以我打算花一点时间,对其进行入门。 4 | 5 | 6 | 幸亏当年写过一年多的c/c++程序,如今只是半吊子,还是能够快速入门libuv,熟悉API。 7 | 8 | 9 | # 搭建libuv开发环境 10 | 11 | - [libuv的仓库](https://github.com/libuv/libuv) 12 | 13 | 仓库里有很详细的各个平台的安装方法: 14 | - [windows](https://github.com/libuv/libuv#windows) 15 | - [*nix](https://github.com/libuv/libuv#unix) 16 | - [mac](https://github.com/libuv/libuv#os-x) 17 | 18 | # Hello world 19 | 20 | 安装完毕以后,引入头文件,最简单的hello world 21 | ```js 22 | #include 23 | #include 24 | #include 25 | 26 | int main() { 27 | printf("Hello world.\n"); 28 | uv_loop_t * loop = uv_default_loop(); 29 | uv_run(loop, UV_RUN_DEFAULT); 30 | 31 | uv_loop_close(loop); 32 | return 0; 33 | } 34 | //output:Hello world. 35 | ``` 36 | 以上的几行代码我们熟悉一下: 37 | - ```v_loop_t * loop = uv_default_loop();```:初始化loop,使用默认loop来跑。node中也是使用默认的loop。 38 | - ```uv_run(loop, UV_RUN_DEFAULT);```:跑loop。 39 | - ``` uv_loop_close(loop);```:关闭loop和释放loop分配的内存 40 | 41 | 到此,我们对```libuv```有了第一认识。 42 | 43 | # 尝试读取一下文件 44 | 45 | 读写文件使用的是```uv_fs_**```这种样式的函数 46 | 47 | 在libuv中,文件操作同时提供了```同步 synchronous``` 和 ```异步 asynchronous.```的接口,这和我们的node非常像,调用方式更加像。异步版本API的接口使用的是内部的线程池模型去驱动异步。废话不多说,我们来看看一个api 48 | 49 | ```js 50 | int uv_fs_open(uv_loop_t* loop, uv_fs_t* req, const char* path, int flags, int mode, uv_fs_cb cb) 51 | ``` 52 | c语言的参数都很长,解释下参数 53 | - ```loop```:loop变量 54 | - ```req```:类型是```uv_fs_t```结构体实例的一个指针,这个参数会在完成io之后,往最后的```cb```中,传入 55 | - ```path```:明显,就是文件的地址了 56 | - ```flags```和```mode```:参数flags与mode和标准的 Unix flags 相同,具体可以查看unix read api 的flags和mode 57 | - ```cb```:这个就是我们的callback函数了,这个函数必须是接受```uv_fs_t* ```为参数的一个函数 58 | 59 | 创建一个文件,```text.txt``` 60 | ```js 61 | i m file 62 | ``` 63 | 64 | 我们快速使用一下这个api 65 | ```js 66 | #include 67 | #include 68 | 69 | uv_fs_t open_req; 70 | 71 | void on_open(uv_fs_t *req) { 72 | printf("%zd\n",req->result);//输出10 73 | } 74 | 75 | int main() { 76 | const char* path = "/Users/zf/Desktop/Fz-node/libuv-simple/libuv-simple/text.txt"; 77 | uv_fs_open(uv_default_loop(), &open_req,path, O_RDONLY, 0, on_open); 78 | uv_run(uv_default_loop(), UV_RUN_DEFAULT); 79 | uv_fs_req_cleanup(&open_req); 80 | return 0; 81 | } 82 | ``` 83 | 这么一来,我们的思路一目了然,填写path之后,调用```uv_fs_open```,然后跑loop,当打开文件结束之后,我们就会到达```on_open```这个callback中。值得注意的是,在c中,打开文件和读文件属于分开的逻辑,两步回调,也是够蛋疼的,但是为了获得极限的性能,异步进行到底。 84 | 85 | 我们得到的结果会存储在全局变量```open_req```中,实际上on_open中的```*req```就是指向这个全局变量。接下来我们要进行一下读操作: 86 | ```js 87 | #include 88 | #include 89 | 90 | uv_fs_t open_req; 91 | uv_fs_t _read; 92 | 93 | static char buffer[1024]; 94 | static uv_buf_t iov; 95 | 96 | void on_read(uv_fs_t *req) { 97 | printf("%s\n",iov.base); 98 | } 99 | void on_open(uv_fs_t *req) { 100 | printf("%zd\n",req->result); 101 | iov = uv_buf_init(buffer, sizeof(buffer)); 102 | uv_fs_read(uv_default_loop(), &_read, (int)req->result, 103 | &iov, 1, -1, on_read); 104 | } 105 | int main() { 106 | const char* path = "/Users/zf/Desktop/Fz-node/libuv-simple/libuv-simple/text.txt"; 107 | uv_fs_open(uv_default_loop(), &open_req,path, O_RDONLY, 0, on_open); 108 | uv_run(uv_default_loop(), UV_RUN_DEFAULT); 109 | uv_fs_req_cleanup(&open_req); 110 | return 0; 111 | } 112 | ``` 113 | 通过两步callback,我们终于获得文件中的内容,打印出来```i m file```。在```on_open``中做了以下几个事: 114 | - req->result 是用于判断读取成功与否的标志位,分别有三种值:大于0,小于0,以及等于0。大于0成功,小于0失败 115 | - uv_buf_init 将一个全局变量```buffer```初始化成```uv_buf_t```的类型 116 | - uv_fs_read 读取函数,跟open函数很类似,注意多了一个参数:iov,read函数会把读到的数据塞进iov中 117 | - 读取完毕以后,来到```on_read```函数,结果放在```iov.base```中,我们就可以我们刚刚文件里写的东西了。 118 | 119 | 120 | # 事件循环什么时候开始的? 121 | 122 | 这个问题,我相信很多人都没想过,甚至想过的人,可能也开始觉得纳闷:```理解事件循环什么时候开始的这对我们理解事件循环本身有什么帮助?```,这并不是我一人钻牛角尖,而是只有搞明白这些,才能真正理解事件循环。 123 | 124 | ```js 125 | int main() { 126 | const char* path = "/Users/zf/Desktop/Fz-node/libuv-simple/libuv-simple/text.txt"; 127 | uv_fs_open(uv_default_loop(), &open_req,path, O_RDONLY, 0, on_open); 128 | uv_run(uv_default_loop(), UV_RUN_DEFAULT); 129 | uv_fs_req_cleanup(&open_req); 130 | return 0; 131 | } 132 | ``` 133 | 回顾刚刚的```main```函数,我们发现,读取的操作```uv_fs_open```有两个特殊的地方: 134 | 1. 在```uv_run```之前 135 | 2. 竟然需要```uv_default_loop()```作为参数 136 | 137 | 其实从这里我们已经可以看出诡异之处,```事件循环是在所有的同步操作之前```。也就是说,无论是libuv还是node都是完成了以下步骤才会进入循环: 138 | - 所有同步任务 139 | - 同步任务中的异步操作发出请求 140 | - 规划好同步任务中的定时器 141 | - 最后process.nextTrick()等等 142 | 143 | 用js代码表明的话 144 | ```js 145 | const http = require('http') //同步任务 146 | const port = 3000 //同步任务 147 | http 148 | .createServer() 149 | .listen(port, () => console.log('我是第一轮事件循环')) //同步任务中的异步请求 150 | console.log('准备进入循环') 151 | ``` 152 | 直到最后一行的```console.log('准备进入循环')```跑完,才会开始准备进入事件循环。 153 | 154 | # 事件循环的7个主要阶段 155 | 156 | - update_time 157 | - timers 158 | - I/O callbacks 159 | - idle, prepare 160 | - I/O poll 161 | - check 162 | - close callbacks 163 | 164 | 也就是说,事件循环必须跑完这6个阶段,才算一个轮回。这一点一定要深刻记住。 165 | 166 | ### 1.update_time 167 | 在事件循环的开头,这一步的作用实际上是为了获取一下系统事件,以保证之后的timer有个计时的标准。这个动作会在每次事件循环的时候都发生,确保了之后timer触发的准确性。(其实也不太准确....) 168 | 169 | ### 2. timers 170 | 事件循环跑到这个阶段的时候,要检查是否有```到期的timer```,其实也就是```setTimeout```和```setInterval```这种类型的timer,到期了,就会执行他们的回调。 171 | 172 | ### 3. I/O callbacks 173 | 处理异步事件的回调,比如网络I/O,比如文件读取I/O。当这些I/O动作都***结束***的时候,在这个阶段会触发它们的回调。我特别指出了结束这个限定语。 174 | 175 | ### 4. idle, prepare 176 | 这个阶段内部做一些动作,与理解事件循环没啥关系 177 | 178 | 179 | ### 5. I/O poll阶段 180 | 这个阶段相当有意思,也是事件循环设计的一个有趣的点。这个阶段是***选择运行***的。选择运行的意思就是不一定会运行。在这里,我先卖一个关子,后问详细深入讨论。 181 | 182 | ### 6. check 183 | 执行```setImmediate```操作 184 | 185 | ### 7. close callbacks 186 | 关闭I/O的动作,比如文件描述符的关闭,链接断开,等等等 187 | 188 | 189 | # 核心函数uv_run 190 | 191 | 上述的七个阶段其实已经很明确,多看几遍就能记住,我们重点来分析一下,libuv源码是怎么写的。看看这个神奇的```uv_run```: 192 | 193 | [源码](https://github.com/libuv/libuv/blob/v1.x/src/unix/core.c) 194 | ```js 195 | int uv_run(uv_loop_t* loop, uv_run_mode mode) { 196 | int timeout; 197 | int r; 198 | int ran_pending; 199 | 200 | //首先检查我们的loop还是否活着 201 | //活着的意思代表loop中是否有异步任务 202 | //如果没有直接就结束 203 | r = uv__loop_alive(loop); 204 | if (!r) 205 | uv__update_time(loop); 206 | 207 | //传说中的事件循环,你没看错了啊!就是一个大while 208 | while (r != 0 && loop->stop_flag == 0) { 209 | //更新事件阶段 210 | uv__update_time(loop); 211 | 212 | //处理timer回调 213 | uv__run_timers(loop); 214 | 215 | //处理异步任务回调 216 | ran_pending = uv__run_pending(loop); 217 | 218 | //没什么用的阶段 219 | uv__run_idle(loop); 220 | uv__run_prepare(loop); 221 | 222 | //这里值得注意了 223 | //从这里到后面的uv__io_poll都是非常的不好懂的 224 | //先记住timeout是一个时间 225 | //uv_backend_timeout计算完毕后,传递给uv__io_poll 226 | //如果timeout = 0,则uv__io_poll会直接跳过 227 | timeout = 0; 228 | if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT) 229 | timeout = uv_backend_timeout(loop); 230 | 231 | uv__io_poll(loop, timeout); 232 | 233 | //就是跑setImmediate 234 | uv__run_check(loop); 235 | 236 | //关闭文件描述符等操作 237 | uv__run_closing_handles(loop); 238 | 239 | //再次检查是否活着 240 | //如果没有任何任务了,就推出 241 | r = uv__loop_alive(loop); 242 | if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT) 243 | break; 244 | } 245 | return r; 246 | } 247 | ``` 248 | 代码中我已经写得很详细了,相信不熟悉c代码的各位也能轻易搞懂,没错,事件循环就是一个大```while```而已!神秘的面纱就此揭开。 249 | 250 | # poll阶段 251 | 252 | 这个阶段设计得非常巧妙,这个函数第二个参数是一个```timeout```参数,而这个```timeOut```由来自```uv_backend_timeout```函数,我们进去一探究竟! 253 | [源码](https://github.com/libuv/libuv/blob/v1.x/src/unix/core.c) 254 | ```js 255 | int uv_backend_timeout(const uv_loop_t* loop) { 256 | if (loop->stop_flag != 0) 257 | return 0; 258 | 259 | if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop)) 260 | return 0; 261 | 262 | if (!QUEUE_EMPTY(&loop->idle_handles)) 263 | return 0; 264 | 265 | if (!QUEUE_EMPTY(&loop->pending_queue)) 266 | return 0; 267 | 268 | if (loop->closing_handles) 269 | return 0; 270 | 271 | return uv__next_timeout(loop); 272 | } 273 | ``` 274 | 原来是一个多步if函数,这代码写得真让人好懂!我们一个一个分析 275 | 1. ```stop_flag```:这个标记是 ```0```的时候,意味着事件循环跑完这一轮就退出了,返回的时间是0 276 | 2. ```!uv__has_active_handles```和```!uv__has_active_reqs```:看名字都知道,如果没有任何的异步任务(包括timer和异步I/O),那```timeOut```时间一定就是0了 277 | 3. ```QUEUE_EMPTY(idle_handles)```和```QUEUE_EMPTY(pending_queue)```:异步任务是通过注册的方式放进了```pending_queue```中,无论是否成功,都已经被注册,如果什么都没有,这两个队列就是空,所以没必要等了。 278 | 4. ```closing_handles```:我们的循环进入了关闭阶段,没必要等待了 279 | 280 | 以上所有条件啰啰嗦嗦,判断来判断去,为的就是等这句话```return uv__next_timeout(loop);```,这句话,告诉了```uv__io_poll```说:你到底停多久,接下来,我们继续看这个神奇的```uv__next_timeout```是怎么获取时间的。 281 | 282 | ```js 283 | int uv__next_timeout(const uv_loop_t* loop) { 284 | const struct heap_node* heap_node; 285 | const uv_timer_t* handle; 286 | uint64_t diff; 287 | 288 | heap_node = heap_min((const struct heap*) &loop->timer_heap); 289 | if (heap_node == NULL) 290 | return -1; /* block indefinitely */ 291 | 292 | handle = container_of(heap_node, uv_timer_t, heap_node); 293 | if (handle->timeout <= loop->time) 294 | return 0; 295 | 296 | //这句代码给出了关键性的指导 297 | diff = handle->timeout - loop->time; 298 | 299 | //不能大于最大的INT_MAX 300 | if (diff > INT_MAX) 301 | diff = INT_MAX; 302 | 303 | return diff; 304 | } 305 | 306 | ``` 307 | 308 | 上述函数做了一件非常简单的事情 309 | 1. 对比当前```loop```设置的时间,还记得一开头我们的```update_time```吗,这里用上了,保存在```loop->time```中 310 | 2. 获取到```距离此时此刻,loop中,最先到期的一个timer的时间```,不懂就多读几遍.... 311 | 312 | 至此,我们就知道,这个```timeout```如果有值,那就一定是```距离此时此刻,loop中,最先到期的一个timer的时间```,如果这个timer时间太长,则以```INT_MAX``` 这个常数时间为基准。在(unix)c++头文件```#include ```中定义得到这个常量是:```32767```(不确定,单位应该是32.767毫秒). 313 | 314 | # 得到Timeout以后poll做了什么? 315 | 316 | ```uv__io_poll```获得了一个最多是```32767```的一个等待时间,那么他等待什么呢?等等,你不觉得奇怪吗?事件循环竟然卡住了,再等等,node也会阻塞了? 317 | 318 | 不要担心,还记得我们刚刚一堆的判断吗?其实```只要有任务需要马上执行的时候```,这个函数是不会被调用的。那么被调用的时候则是:所有被注册的异步任务都没有完成(返回)的时候,这时候等一下其实没什么所谓,```等的就是这些异步任务会不会在这么极其短暂的时间内发生I/O完毕!```,至于等待的时间会根据每个系统的实现而不同,其实现原理就是epoll_wait函数做一个定时器.. 319 | 320 | 等待结束以后,就会进入```check```阶段. 321 | 322 | 323 | # nextTick去哪里了? 324 | 325 | 纵观整个事件循环,我们都没有发现,神秘的nextTick去哪里了。我们继续肛到nextTick中的源码中: 326 | ```js 327 | startup.processNextTick = function() { 328 | var nextTickQueue = []; 329 | var pendingUnhandledRejections = []; 330 | var microtasksScheduled = false; 331 | 332 | // Used to run V8's micro task queue. 333 | var _runMicrotasks = {}; 334 | 335 | // *Must* match Environment::TickInfo::Fields in src/env.h. 336 | var kIndex = 0; 337 | var kLength = 1; 338 | 339 | process.nextTick = nextTick; 340 | // Needs to be accessible from beyond this scope. 341 | process._tickCallback = _tickCallback; 342 | process._tickDomainCallback = _tickDomainCallback; 343 | 344 | //这里真正的调用了c++层的 345 | const tickInfo = process._setupNextTick(_tickCallback, _runMicrotasks); 346 | // 省略... 347 | } 348 | ``` 349 | 在胶水层```src/async_wrap.cc```中,我们可以看到: 350 | 351 | ```js 352 | Local AsyncWrap::MakeCallback(const Local cb, 353 | int argc, 354 | Local* argv) { 355 | // ... 356 | Environment::TickInfo* tick_info = env()->tick_info(); 357 | 358 | if (tick_info->in_tick()) { 359 | return ret; 360 | } 361 | 362 | //如果没有的话直接执行promise这种微任务 363 | if (tick_info->length() == 0) { 364 | env()->isolate()->RunMicrotasks(); 365 | } 366 | 367 | if (tick_info->length() == 0) { 368 | tick_info->set_index(0); 369 | return ret; 370 | } 371 | 372 | tick_info->set_in_tick(true); 373 | //如果有nextTick,promise这种微任务会被放在nextTick之后,先执行nextTick 374 | env()->tick_callback_function()->Call(process, 0, nullptr); 375 | 376 | tick_info->set_in_tick(false); 377 | ``` 378 | 379 | 我们写一段代码来看看 380 | ```js 381 | //无论你怎么调整Promise和nextTick的顺序,永远输出的是1和2 382 | Promise.resolve().then(() => console.log(2)) 383 | process.nextTick(() => console.log(1)) 384 | //Promise.resolve().then(() => console.log(2))放在这里也一样 385 | ``` 386 | 387 | Node 规定,```process.nextTick```和```Promise```的回调函数,追加在本轮循环,即同步任务一旦执行完成,就开始执行它们。而```setTimeout```、```setInterval```、```setImmediate```的回调函数,追加在次轮循环。 388 | 389 | ```js 390 | // 下面两行,次轮循环执行 391 | setTimeout(() => console.log(1)); 392 | setImmediate(() => console.log(2)); 393 | // 下面两行,本轮循环执行 394 | process.nextTick(() => console.log(3)); 395 | Promise.resolve().then(() => console.log(4)); 396 | ``` 397 | 398 | 因为是源码解析,所以具体的我就不多说,大家只可以看文档:[node官方文档](https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/) 399 | 400 | 401 | # 总结 402 | 403 | - 事件循环的开始,在所有同步代码第一次注册完以后开始(如果有异步任务的话) 404 | - 事件循环分为7个阶段,其中```uv__io_poll```阶段最难懂。 405 | - ```process.nextTick```的操作,会在每一轮事件循环的最后执行 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | --------------------------------------------------------------------------------