体系架构

体系架构

Node.js主要分为四大部分,Node Standard Library,Node Bindings,V8,Libuv,架构图如下:体系架构_第1张图片

  • Node Standard Library 是我们每天都在用的标准库,如Http, Buffer 模块。
  • Node Bindings 是沟通JS 和 C++的桥梁,封装V8和Libuv的细节,向上层提供基础API服务。
  • 这一层是支撑 Node.js 运行的关键,由 C/C++ 实现。
    • V8 是Google开发的JavaScript引擎,提供JavaScript运行环境,可以说它就是 Node.js 的发动机。
    • Libuv 是专门为Node.js开发的一个封装库,提供跨平台的异步I/O能力.
    • C-ares:提供了异步处理 DNS 相关的能力。
    • http_parser、OpenSSL、zlib 等:提供包括 http 解析、SSL、数据压缩等其他的能力。

代码结构

树形结构查看,使用 tree 命令

➜  nodejs git:(master) tree -L 1
.
├── AUTHORS
├── BSDmakefile
├── BUILDING.md
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── COLLABORATOR_GUIDE.md
├── CONTRIBUTING.md
├── GOVERNANCE.md
├── LICENSE
├── Makefile
├── README.md
├── ROADMAP.md
├── WORKING_GROUPS.md
├── android-configure
├── benchmark
├── common.gypi
├── config.gypi
├── config.mk
├── configure
├── deps
├── doc
├── icu_config.gypi
├── lib
├── node.gyp
├── out
├── src
├── test
├── tools
└── vcbuild.bat

进一步查看 deps目录:

➜  nodejs git:(master) tree deps -L 1
deps
├── cares
├── gtest
├── http_parser
├── npm
├── openssl
├── uv
├── v8
└── zlib

libuv 提供异步支持

分类 操作 时间成本
缓存 L1缓存 1纳秒
  L2缓存 4纳秒
  主存储器 100 ns
  SSD 随机读取 16000 ns
I/O 往返在同一数据中心 500000 ns
  物理磁盘寻道 4,000,000 ns

我们看到即便是 SSD 的访问相较于高速的 CPU,仍然是慢速设备。于是基于事件驱动的 IO 模型就应运而生,解决了高速设备同步等待慢速设备或访问的问题。这不是 libuv 的独创,linux kernel 原生支持的 NIO也是这个思路。 但 libuv 统一了网络访问,文件访问,做到了跨平台。

libuv 架构

体系架构_第2张图片从左往右分为两部分,一部分是与网络I/O相关的请求,而另外一部分是由文件I/O, DNS Ops以及User code组成的请求。

从图中可以看出,对于Network I/O和以File I/O为代表的另一类请求,异步处理的底层支撑机制是完全不一样的。

对于Network I/O相关的请求, 根据OS平台不同,分别使用Linux上的epoll,OSX和BSD类OS上的kqueue,SunOS上的event ports以及Windows上的IOCP机制。

而对于File I/O为代表的请求,则使用thread pool。利用thread pool的方式实现异步请求处理,在各类OS上都能获得很好的支持。

架构图

体系架构_第3张图片

现在 JS 引擎的执行过程大致是:源代码 --->抽象语法树 --->字节码 --->JIT--->本地代码。

V8 更加直接的将抽象语法树通过 JIT 技术转换成本地代码,放弃了在字节码阶段可以进行的一些性能优化,但保证了执行速度。在 V8 生成本地代码后,也会通过 Profiler 采集一些信息,来优化本地代码。虽然,少了生成字节码这一阶段的性能优化,但极大减少了转换时间。

PS: Tuborfan 将逐步取代 Crankshaft

在使用 v8 引擎之前,先来了解一下几个基本概念:句柄(handle),作用域(scope),上下文环境(可以简单地理解为运行环境)。

Isolate

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.

一个 Isolate 是一个独立的虚拟机。对应一个或多个线程。但同一时刻 只能被一个线程进入。所有的 Isolate 彼此之间是完全隔离的, 它们不能够有任何共享的资源。如果不显示创建 Isolate, 会自动创建一个默认的 Isolate。

后面提到的 Context、Scope、Handle 的概念都是一个 Isolate 内部的, 如下图:体系架构_第4张图片

Handle 概念

在 V8 中,内存分配都是在 V8 的 Heap 中进行分配的,JavaScript 的值和对象也都存放在 V8 的 Heap 中。这个 Heap 由 V8 独立的去维护,失去引用的对象将会被 V8 的 GC 掉并可以重新分配给其他对象。而 Handle 即是对 Heap 中对象的引用。V8 为了对内存分配进行管理,GC 需要对 V8 中的所有对象进行跟踪,而对象都是用 Handle 方式引用的,所以 GC 需要对 Handle 进行管理,这样 GC 就能知道 Heap 中一个对象的引用情况,当一个对象的 Handle 引用发生改变的时候,GC 即可对该对象进行回收或者移动。因此,V8 编程中必须使用 Handle 去引用一个对象,而不是直接通过 C++ 的方式去获取对象的引用,直接通过 C++ 的方式去引用一个对象,会使得该对象无法被 V8 管理。

Handle 分为 Local 和 Persistent 两种。

从字面上就能知道,Local 是局部的,它同时被 HandleScope 进行管理。persistent,类似与全局的,不受 HandleScope 的管理,其作用域可以延伸到不同的函数,而 Local 是局部的,作用域比较小。Persistent Handle 对象需要 Persistent::New, Persistent::Dispose 配对使用,类似于 C++ 中 new 和 delete。

Persistent::MakeWeak 可以用来弱化一个 Persistent Handle,如果一个对象的唯一引用 Handle 是一个 Persistent,则可以使用 MakeWeak 方法来弱化该引用,该方法可以触发 GC 对被引用对象的回收。

Scope

从概念上理解,作用域可以看成是一个句柄的容器,在一个作用域里面可以有很多很多个句柄(也就是说,一个 scope 里面可以包含很多很多个v8 引擎相关的对象),句柄指向的对象是可以一个一个单独地释放的,但是很多时候(真正开始写业务代码的时候),一个一个地释放句柄过于繁琐,取而代之的是,可以释放一个 scope,那么包含在这个 scope 中的所有 handle 就都会被统一释放掉了。

Scope 在 v8.h 中有这么几个:HandleScope,Context::Scope。

HandleScope 是用来管理 Handle 的,而 Context::Scope 仅仅用来管理 Context 对象。

代码像下面这样:

  // 在此函数中的 Handle 都会被 handleScope 管理
  HandleScope handleScope;
  // 创建一个 js 执行环境 Context
  Handle context = Context::New();
  Context::Scope contextScope(context);
  // 其它代码

一般情况下,函数的开始部分都放一个 HandleScope,这样此函数中的 Handle 就不需要再理会释放资源了。而 Context::Scope 仅仅做了:在构造中调用 context->Enter(),而在析构函数中调用 context->Leave()。

Context 概念 

从概念上讲,这个上下文环境也可以理解为运行环境。在执行 javascript 脚本的时候,总要有一些环境变量或者全局函数。我们如果要在自己的 c++ 代码中嵌入 v8 引擎,自然希望提供一些 c++ 编写的函数或者模块,让其他用户从脚本中直接调用,这样才会体现出 javascript 的强大。我们可以用 c++ 编写全局函数或者类,让其他人通过 javascript 进行调用,这样,就无形中扩展了 javascript 的功能。 

Context 可以嵌套,即当前函数有一个 Context,调用其它函数时如果又有一个 Context,则在被调用的函数中 javascript 是以最近的Context 为准的,当退出这个函数时,又恢复到了原来的 Context。

我们可以往不同的 Context 里 “导入” 不同的全局变量及函数,互不影响。据说设计 Context 的最初目的是为了让浏览器在解析 HTML 的 iframe时,让每个 iframe 都有独立的 javascript 执行环境,即一个 iframe 对应一个 Context。

同作用域下不同的执行上下文

体系架构_第5张图片

关系

体系架构_第6张图片

从这张图可以比较清楚的看到 Handle,HandleScope,以及被 Handle 引用的对象之间的关系。从图中可以看到,V8 的对象都是存在 V8 的 Heap 中,而 Handle 则是对该对象的引用。

垃圾回收

垃圾回收器是一把十足的双刃剑。好处是简化程序的内存管理,内存管理无需程序员来操作,由此也减少了长时间运转的程序的内存泄漏。然而无法预期的停顿,影响了交互体验。

基本概念

垃圾回收器解决基本问题就是,识别需要回收的内存。一旦辨别完毕,这些内存区域即可在未来的分配中重用,或者是返还给操作系统。一个对象当它不是处于活跃状态的时候它就死了。一个对象处于活跃状态,当且仅当它被一个根对象或另一个活跃对象指向。根对象被定义为处于活跃状态,是浏览器或 V8 所引用的对象。比如说全局对象属于根对象,因为它们始终可被访问;浏览器对象,如 DOM 元素,也属于根对象,尽管在某些场合下它们只是弱引用。

堆的构成

在深入研究垃圾回收器的内部工作原理之前,首先来看看堆是如何组织的。V8 将堆分为了几个不同的区域:

** 新生区 **:大多数对象开始时被分配在这里。新生区是一个很小的区域,垃圾回收在这个区域非常频繁,与其他区域相独立。

** 老生指针区 **:包含大多数可能存在指向其他对象的指针的对象。大多数在新生区存活一段时间之后的对象都会被挪到这里。

** 老生数据区 **:这里存放只包含原始数据的对象(这些对象没有指向其他对象的指针)。字符串、封箱的数字以及未封箱的双精度数字数组,在新生区经历一次 Scavenge 后会被移动到这里。

** 大对象区 **:这里存放体积超过 1MB 大小的对象。每个对象有自己 mmap 产生的内存。垃圾回收器从不移动大对象。

**Code 区 **:代码对象,也就是包含 JIT 之后指令的对象,会被分配到这里。

**Cell 区、属性 Cell 区、Map 区 **:这些区域存放 Cell、属性 Cell 和 Map,每个区域因为都是存放相同大小的元素,因此内存结构很简单。

如上图:在 node-v4.x 之后,区域进行了合并为:新生区,老生区,大对象区,Map 区,Code 区

有了这些背景知识,我们可以来深入垃圾回收器了。

识别指针

垃圾回收器面临的第一个问题是,如何才能在堆中区分指针和数据,因为指针指向着活跃的对象。大多数垃圾回收算法会将对象在内存中挪动(以便减少内存碎片,使内存紧凑),因此即使不区分指针和数据,我们也常常需要对指针进行改写。V8 采用了标记指针法:这种方法需要在每个指针的末位预留一位来标记这个字代表的是指针或数据。

对象的晋升

当一个对象经过多次新生代的清理依旧幸存,这说明它的生存周期较长,也就会被移动到老生代,这称为对象的晋升。具体移动的标准有两种:

  • 对象从 From 空间复制到 To 空间时,会检查它的内存地址来判断这个对象是否已经活过一次新生代的清理,如果是,则复制到老生代中,否则复制到 To 空间中
  • 对象从 From 空间复制到 To 空间时,如果 To 空间已经被使用了超过 25%,那么这个对象直接被复制到老生代。

写屏障

如果新生区中某个对象,只有一个指向它的指针,而这个指针恰好是在老生区的对象当中,我们如何才能知道新生区中那个对象是活跃的呢? 为了解决这个问题,实际上在写缓冲区中有一个列表 store-buffer{.cc,.h,-inl.h},列表中记录了所有老生区对象指向新生区的情况。新对象诞生的时候,并不会有指向它的指针,而当有老生区中的对象出现指向新生区对象的指针时,我们便记录下来这样的跨区指向。由于这种记录行为总是发生在写操作时,它被称为写屏障.

垃圾回收三部曲

Stop-the-World 的 GC 包括三个主要步骤:

  1. 枚举根节点引用;
  2. 发现并标记活对象;
  3. 垃圾内存清理

分代回收在 V8 中分为 Scavenge, Mark-Sweep

  • Scavenge: 当分配指针达到了新生区的末尾,就会有一次清理。
  • Mark-Sweep: 对于活跃超过 2 个小周期的对象,则需将其移动至老生区, 当老生区有足够多的对象时才会触发。

C++ 和 JS 交互

数据及模板

由于 C++ 原生数据类型与 JavaScript 中数据类型有很大差异,因此 V8 提供了 Value 类,从 JavaScript 到 C++,从 C++ 到 JavaScrpt 都会用到这个类及其子类,比如:

Handle Add(const Arguments& args){
   int a = args[0]->Uint32Value(); 
   int b = args[1]->Uint32Value(); 

   return Integer::New(a+b); 
 }

Integer 即为 Value 的一个子类。

V8 中,有两个模板 (Template) 类 (并非 C++ 中的模板类):

  • 对象模板 (ObjectTemplate)
  • 函数模板 (FunctionTemplate)这两个模板类用以定义 JavaScript 对象和 JavaScript 函数。我们在后续的小节部分将会接触到模板类的实例。通过使用ObjectTemplate,可以将 C++ 中的对象暴露给脚本环境,类似的,FunctionTemplate 用以将 C++函数暴露给脚本环境,以供脚本使用。

JS 使用 C++ 变量

在 JavaScript 与 V8 间共享变量事实上是非常容易的,基本模板如下:

static char sname[512] = {0}; 

 static Handle NameGetter(Local name, const AccessorInfo& info) {
    return String::New((char*)&sname,strlen((char*)&sname)); 
 } 

 static void NameSetter(Local name, Local value, const AccessorInfo& info) {
   Local str = value->ToString(); 
   str->WriteAscii((char*)&sname); 
 }

定义了 NameGetter, NameSetter 之后,在 main 函数中,将其注册在 global 上:

 // Create a template for the global object. 
 Handle global = ObjectTemplate::New(); 
 //public the name variable to script 
 global->SetAccessor(String::New("name"), NameGetter, NameSetter); 

JS 调用 C++ 函数

在 JavaScript 中调用 C++ 函数是脚本化最常见的方式,通过使用 C++ 函数,可以极大程度的增强 JavaScript 脚本的能力,如文件读写,网络 / 数据库访问,图形 / 图像处理等等,类似于 JAVA 的 jni 技术。

在 C++ 代码中,定义以下原型的函数:

 Handle func(const Arguments& args){//return something}

然后,再将其公开给脚本:global->Set(String::New("func"),FunctionTemplate::New(func));

JS 使用 C++ 类

如果从面向对象的视角来分析,最合理的方式是将 C++ 类公开给 JavaScript,这样可以将 JavaScript内置的对象数量大大增加,从而尽可能少的使用宿主语言,而更大的利用动态语言的灵活性和扩展性。事实上,C++语言概念众多,内容繁复,学习曲线较 JavaScript 远为陡峭。最好的应用场景是:既有脚本语言的灵活性,又有 C/C++ 等系统语言的效率。使用 V8 引擎,可以很方便的将 C++ 类” 包装” 成可供 JavaScript 使用的资源。

我们这里举一个较为简单的例子,定义一个 Person 类,然后将这个类包装并暴露给 JavaScript 脚本,在脚本中新建 Person 类的对象,使用 Person 对象的方法。首先,我们在 C++ 中定义好类 Person:

class Person { 
 private: 
   unsigned int age; 
   char name[512]; 

 public: 
   Person(unsigned int age, char *name) {
     this->age = age; 
     strncpy(this->name, name, sizeof(this->name)); 
   } 

   unsigned int getAge() {
     return this->age;
   } 

   void setAge(unsigned int nage) {
     this->age = nage;
   } 

   char *getName() {
     return this->name;
   } 

   void setName(char *nname) {
     strncpy(this->name, nname, sizeof(this->name));
   } 
 };

Person 类的结构很简单,只包含两个字段 age 和 name,并定义了各自的 getter/setter. 然后我们来定义构造器的包装:

Handle PersonConstructor(const Arguments& args){
   Handle object = args.This(); 
   HandleScope handle_scope; 
   int age = args[0]->Uint32Value(); 

   String::Utf8Value str(args[1]); 
   char* name = ToCString(str); 

   Person *person = new Person(age, name); 
   object->SetInternalField(0, External::New(person)); 
   return object; 
 } 
   
  

从函数原型上可以看出,构造器的包装与上一小节中,函数的包装是一致的,因为构造函数在 V8 看来,也是一个函数。需要注意的是,从 args 中获取参数并转换为合适的类型之后,我们根据此参数来调用 Person 类实际的构造函数,并将其设置在 object的内部字段中。紧接着,我们需要包装 Person 类的 getter/setter:

 Handle PersonGetAge(const Arguments& args){
   Local self = args.Holder(); 
   Local wrap = Local::Cast(self->GetInternalField(0)); 

   void *ptr = wrap->Value(); 

   return Integer::New(static_cast(ptr)->getAge()); 
 } 

 Handle PersonSetAge(const Arguments& args) {
   Local self = args.Holder(); 
   Local wrap = Local::Cast(self->GetInternalField(0)); 

   void* ptr = wrap->Value(); 

   static_cast(ptr)->setAge(args[0]->Uint32Value()); 
   return Undefined();
 } 
   
  

而 getName 和 setName 的与上例类似。在对函数包装完成之后,需要将 Person 类暴露给脚本环境:首先,创建一个新的函数模板,将其与字符串”Person” 绑定,并放入 global:

 Handle person_template = FunctionTemplate::New(PersonConstructor); 
 person_template->SetClassName(String::New("Person")); 
 global->Set(String::New("Person"), person_template);

然后定义原型模板:

 Handle person_proto = person_template->PrototypeTemplate(); 

 person_proto->Set("getAge", FunctionTemplate::New(PersonGetAge)); 
 person_proto->Set("setAge", FunctionTemplate::New(PersonSetAge)); 

 person_proto->Set("getName", FunctionTemplate::New(PersonGetName)); 
 person_proto->Set("setName", FunctionTemplate::New(PersonSetName));

最后设置实例模板:

 Handle person_inst = person_template->InstanceTemplate(); 
 person_inst->SetInternalFieldCount(1);

C++ 调用 JS 函数

我们直接看下 src/timer_wrap.cc 的例子,V8 编译执行 timer.js, 构造了 Timer 对象。

static void OnTimeout(uv_timer_t* handle) {
    TimerWrap* wrap = static_cast(handle->data);
    Environment* env = wrap->env();
    HandleScope handle_scope(env->isolate());
    Context::Scope context_scope(env->context());
    wrap->MakeCallback(kOnTimeout, 0, nullptr);
}

inline v8::Local AsyncWrap::MakeCallback(uint32_t index, int argc, v8::Local* argv) {
    v8::Local cb_v = object()->Get(index);
    CHECK(cb_v->IsFunction());
    return MakeCallback(cb_v.As(), argc, argv);
}

TimerWrap 对象通过数组的索引寻址,找到 Timer 对象索引 0 的对象,而对其赋值的是在 lib/timer.js 里面的list._timer[kOnTimeout] = listOnTimeout; 。这边找到的对象是个 Function,后面忽略 domains 异常处理等,就是简单的调用 Function 对象的 Call 方法, 并且传人上文提到的 Context 和参数。

Local ret = callback->Call(recv, argc, argv);

模块加载准备操作

严格来讲,Node 里面分以下几种模块:

  • builtin module: Node 中以 c++ 形式提供的模块,如 tcp_wrap、contextify 等
  • constants module: Node 中定义常量的模块,用来导出如 signal, openssl 库、文件访问权限等常量的定义。如文件访问权限中的 O_RDONLY,O_CREAT、signal 中的 SIGHUP,SIGINT 等。
  • native module: Node 中以 JavaScript 形式提供的模块,如 http,https,fs 等。有些 native module 需要借助于 builtin module 实现背后的功能。如对于 native 模块 buffer , 还是需要借助 builtin node_buffer.cc 中提供的功能来实现大容量内存申请和管理,目的是能够脱离 V8 内存大小使用限制。
  • 3rd-party module: 以上模块可以统称 Node 内建模块,除此之外为第三方模块,典型的如 express 模块。

builtin module 和 native module 生成过程

体系架构_第7张图片

模块加载

我们仍旧从 var http = require('http'); 说起。

require 是怎么来的,为什么平白无故就能用呢,实际上都干了些什么?

  • lib/module.js 的中有如下代码。
// Loads a module at the given file path. Returns that module's
// `exports` property.
Module.prototype.require = function(path) {
  assert(path,'missing path');
  assert(typeof path ==='string','path must be a string');
  return Module._load(path, this);
};

首先 assert 模块进行简单的 path 变量的判断,需要传人的 path 是一个 string 类型。

// Check the cache for the requested file.
// 1. If a module already exists in the cache: return its exports object.
// 2. If the module is native: call `NativeModule.require()` with the
//    filename and return the result.
// 3. Otherwise, create a new module for the file and save it to the cache.
//    Then have it load  the file contents before returning its exports
//    object.
Module._load = function(request, parent, isMain) {
  if (parent) {
    debug('Module._load REQUEST %s parent: %s', request, parent.id);
  }

  var filename = Module._resolveFilename(request, parent);

  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    return cachedModule.exports;
  }

  if (NativeModule.nonInternalExists(filename)) {
    debug('load native module %s', request);
    return NativeModule.require(filename);
  }

  var module = new Module(filename, parent);

  if (isMain) {
    process.mainModule = module;
    module.id = '.';
  }

  Module._cache[filename] = module;

  var hadException = true;

  try {
    module.load(filename);
    hadException = false;
  } finally {
      if (hadException) {
        delete Module._cache[filename];
      }
  }

  return module.exports;
};
  • 如果模块在缓存中,返回它的 exports 对象。
  • 如果是原生的模块,通过调用 NativeModule.require() 返回结果。
  • 否则,创建一个新的模块,并保存到缓存中。

让我们再深度遍历的方式查看代码到 NativeModule.require.

  NativeModule.require = function(id) {
    if (id =='native_module') {
      return NativeModule;
    }

    var cached = NativeModule.getCached(id);
    if (cached) {
      return cached.exports;
    }

    if (!NativeModule.exists(id)) {
      throw new Error('No such native module '+ id);
    }

    process.moduleLoadList.push('NativeModule' + id);

    var nativeModule = new NativeModule(id);

    nativeModule.cache();
    nativeModule.compile();

    return nativeModule.exports;
  };

我们看到,缓存的策略这个贯穿在 node 的实现中。

  • 同样的,如果在 cache 中存在,则直接返回 exports 对象。
  • 如果不在,则加入到 moduleLoadList 数组中,创建新的 NativeModule 对象。

下面是最关键的一句

nativeModule.compile();

具体实现在 node.js 中:

NativeModule.getSource = function(id) {
  return NativeModule._source[id];
};

NativeModule.wrap = function(script) {
  return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};

NativeModule.wrapper = ['(function (exports, require, module, __filename, __dirname) {','\n});' ];

NativeModule.prototype.compile = function() {
  var source = NativeModule.getSource(this.id);
  source = NativeModule.wrap(source);

  var fn = runInThisContext(source, {
    filename: this.filename,
    lineOffset: 0
  });
  fn(this.exports, NativeModule.require, this, this.filename);

  this.loaded = true;
};

wrap 函数将 http.js 包裹起来, 交由 runInThisContext 编译源码,返回 fn 函数, 依次将参数传人。

process

先看看 node.js 的底层 C++ 传递给 javascript 的一个变量 process,在一开始运行 node.js 时,程序会先配置好 processHandleprocess = SetupProcessObject(argc, argv);

  • 然后把 process 作为参数去调用 js 主程序 src/node.js 返回的函数,这样 process 就传递到 javascript 里了。
//node.cc

// 通过 MainSource() 获取已转化的 src/node.js 源码,并执行它

Local f_value = ExecuteString(MainSource(), IMMUTABLE_STRING(“node.js”));
// 执行 src/node.js 后获得的是一个函数,从 node.js 源码可以看出:

//node.js

//(function(process) {

//    global = this;

//

//})

Local f = Local::Cast(f_value);
// 创建函数执行环境,调用函数,把 process 传入

Localglobal = v8::Context::GetCurrent()->Global();

Local args[1] = {
  Local::New(process) 
};

f->Call(global, 1, args);

vm

runInThisContext 又是怎么一回事呢?

  var ContextifyScript = process.binding('contextify').ContextifyScript;
  function runInThisContext(code, options) {
    var script = new ContextifyScript(code, options);
    return script.runInThisContext();
  }
  • node.cc 的 Binding 中有如下调用,对模块进行注册,mod->nm_context_register_func(exports, unused, env->context(), mod->nm_priv);

我们看下 node.h 中 mod 数据结构的定义:

struct node_module {
  int nm_version;
  unsigned int nm_flags;
  void* nm_dso_handle;
  const char* nm_filename;
  node::addon_register_func nm_register_func;
  node::addon_context_register_func nm_context_register_func;
  const char* nm_modname;
  void* nm_priv;
  struct node_module* nm_link;
};
  • node.h 中还有如下宏定义,接着往下看!
#define NODE_MODULE_CONTEXT_AWARE_X(modname, regfunc, priv, flags)    \
  extern "C" {                                                        \
    static node::node_module _module =                                \
    {                                                                 \
      NODE_MODULE_VERSION,                                            \
      flags,                                                          \
      NULL,                                                           \
      __FILE__,                                                       \
      NULL,                                                           \
      (node::addon_context_register_func) (regfunc),                  \
      NODE_STRINGIFY(modname),                                        \
      priv,                                                           \
      NULL                                                            \
    };                                                                \
    NODE_C_CTOR(_register_ ## modname) {                              \
      node_module_register(&_module);                                 \
    }                                                                 \
  }
  
#define NODE_MODULE_CONTEXT_AWARE_BUILTIN(modname, regfunc)           \
  NODE_MODULE_CONTEXT_AWARE_X(modname, regfunc, NULL, NM_F_BUILTIN)   \
  • node_contextify.cc 中有如下宏调用,终于看清楚了!结合前面几点,实际上就是把 node_module 的 nm_context_register_func 与 node::InitContextify 进行了绑定。
NODE_MODULE_CONTEXT_AWARE_BUILTIN(contextify, node::InitContextify);

我们回溯而上,通过 node_module_register(&_module);,process.binding('contextify')--> mod->nm_context_register_func(exports, unused, env->context(), mod->nm_priv); --> node::InitContextify().

这样通过 env->SetProtoMethod(script_tmpl,"runInThisContext", RunInThisContext);,绑定了『runInThisContext』 和 RunInThisContext.

runInThisContext 是将被包装后的源字符串转成可执行函数,(runInThisContext 来自 contextify 模块),runInThisContext 的作用,类似 eval,再执行这个被 eval 后的函数。

这样就成功加载了 native 模块, 标记 this.loaded = true;

Timer源码解读

定时器主要的使用场景或者说适用场景:

  • 定时任务,比如业务中定时检查状态等;
  • 超时控制,比如网络超时控制重传。

数据结构选择

一个Timer本质上是这样的一个数据结构:deadline越近的任务拥有越高优先级,提供以下3种基本操作:

  • schedule 新增任务
  • cancel 删除任务
  • expire 执行到期的任务
实现方式 schedule cancel expire
基于链表 O(1) O(n) O(n)
基于排序链表 O(n) O(1) O(1)
基于最小堆 O(lgn) O(1) O(1)
基于时间轮 O(1) O(1) O(1)

Generators

迭代器模式是很常用的设计模式,但是实现起来,很多东西是程序化的;当迭代规则比较复杂时,维护迭代器内的状态,是比较麻烦的。 于是有了generator,何为generator?

Generators: a better way to build Iterators.

借助 yield 关键字,可以更优雅的实现fibonacci数列。

function* fibonacci() {
  let a = 0, b = 1;

  while(true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

yield与异步

yield可以暂停运行流程,那么便为改变执行流程提供了可能。这和Python的coroutine类似。

Geneartor之所以可用来控制代码流程,就是通过yield来将两个或者多个Geneartor的执行路径互相切换。这种切换是语句级别的,而不是函数调用级别的。其本质是CPS变换。

yield之后,实际上本次调用就结束了,控制权实际上已经转到了外部调用了generator的next方法的函数,调用的过程中伴随着状态的改变。那么如果外部函数不继续调用next方法,那么yield所在函数就相当于停在yield那里了。所以把异步的东西做完,要函数继续执行,只要在合适的地方再次调用generator 的next就行,就好像函数在暂停后,继续执行。

cluster模块,让Node.js开发Web服务时,很方便的做到充分利用多核机器。

充分利用多核的思路是:使用多个进程处理业务。cluster模块封装了创建子进程、进程间通信、服务负载均衡。有两类进程,master进程和worker进程,master进程是主控进程,它负责启动worker进程,worker是子进程、干活的进程。

学习转载于:https://mp.csdn.net/postedit

你可能感兴趣的:(node.js)