node爱好者边边周日源码重读16.5.0

401 阅读10分钟

第一步先搭建一个node工程,当我们没有egg,express,midway 【方便我们来看源码】

再去github.com/nodejs/node 下载好node 源码 开工了

我的node 版本是 16.5.0 感慨升级速2021-07-14 latest
解压代码几个重要的 

── deps          # Node底层核心依赖; 最核心的两块V8 Engine和libuv事件驱动的异步I/O模型库
├── doc           
├── lib           # Node后端核心库
├── node.gyp      # Node编译任务配置文件 
├── node.gypi    
├── src           # C++内建模块
├── test          # 测试代码
├── tools         # 编译时用到的工具


var http = require('http');
 
var server = http.createServer(function (req, res) {
	res.writeHead(200, {'Content-Type': 'text/plain'});
	res.end('Hello World\n');
})
 
server.listen(9876,'127.0.0.1');
console.log('Server running at http://127.0.0.1:9876/');

nodejs 启动时会调用什么 【第一步注册 c++】


// Call _register<module_name> functions for all of
// the built-in modules. Because built-in modules don't
// use the __attribute__((constructor)). Need to
// explicitly call the _register* functions.
void RegisterBuiltinModules();
void GetInternalBinding(const v8::FunctionCallbackInfo<v8::Value>& args);
void GetLinkedBinding(const v8::FunctionCallbackInfo<v8::Value>& args);
void DLOpen(const v8::FunctionCallbackInfo<v8::Value>& args);


看代码 会调用registerBuiltinModules函数注册C++模块,这个函数会调用一系列registerxxx的函数


registerBuiltinModules()

// Call built-in modules' _register_<module name> function to
// do module registration explicitly.
void RegisterBuiltinModules() {
#define V(modname) _register_##modname();
  NODE_BUILTIN_MODULES(V)
#undef V
}

我们发现在Node.js源码里找不到这些函数,因为这些函数会在各个C++模块中,通过宏定义实现的

registerxxx函数的作用就是往C++模块的链表了插入一个节点,最后会形成一个链表

比如 tcp, udp file 等模块

看代码 怎么访问这些模块呢 是通过internalBinding访问C++模块的根据模块名从模块队列中找到对应模块。但是这个函数只能在Node.js内部使用,不能在用户js模块使用。用户可以通过 process.binding访问C++模块 【tip 1 】

  binding(bindingName) {
    try {
      const { internalBinding } = require('internal/test/binding');

      return internalBinding(bindingName);
    } catch {
      return process.binding(bindingName);
    }
  }

nodejs 【第二步创建env 在绑定context】

注册完C++模块后就开始创建Environment对象,Environment是Node.js执行时的环境对象,类似一个全局变量的作用,他记录了Node.js在运行时的一些公共数据。创建完Environment后 同时 v8 也需要知道env v8里面有context 和 isolate isolate 是什么【调度用的】? 可以看这个 zhuanlan.zhihu.com/p/186219660 我觉得写得还可以

通过v8 context 拿到对应 env
Environment *env = Environment:: GetCurrent(context)

nodejs 【第三步 初始化模块加载器】

loader.js主要是封装了c++模块加载器和原生js模块加载器


require('net')
require('./myModule')


分别加载了一个用户模块和原生js模块,我们看看加载过程

1 Node.js首先会判断是否是原生js模块,如果不是则直接加载用户模块,否则,会使用原生模块加载器加载原生js模块。

2 加载原生js模块的时候,如果用到了c++模块,则使用internalBinding去加载 【tip 1 】中有提到

nodejs 【第四步 执行用户JS代码,然后进入Libuv事件循环】--比较重要的

比如我们listen一个服务器的时候,就会在事件循环中新建一个tcp handle。Node.js就会在这个事件循环中一直运行

*** 事件循环必看 src -api -embed_helpers.cc

    do {
      if (env->is_stopping()) break;
      uv_run(env->event_loop(), UV_RUN_DEFAULT);
      if (env->is_stopping()) break;

      platform->DrainTasks(isolate);

      more = uv_loop_alive(env->event_loop());
      if (more && !env->is_stopping()) continue;

      if (EmitProcessBeforeExit(env).IsNothing())
        break;

      // Emit `beforeExit` if the loop became alive either after emitting
      // event, or after running some callbacks.
      more = uv_loop_alive(env->event_loop());
    } while (more == true && !env->is_stopping());

1 timer阶段: 用二叉堆实现,最快过期的在根节点。【自己的笔记 这个阶段执行 timer(setTimeout、setInterval)的回调】

2 pending阶段:处理poll io阶段回调里产生的回调 【 处理一些上一轮循环中的少数未执行的 I/O 回调】

3 prepare、idle阶段:每次事件循环都会被执行。 仅 node 内部使用

4 poll io阶段:处理文件描述符相关事件。【获取新的 I/O 事件, 适当的条件下 node 将阻塞在这里】

5 check、 执行 setImmediate() 的回调

5 closing阶段:执行调用uv_close函数时传入的回调

4-1 定时器 timer阶段

定时器的底层数据结构是二叉堆,最快到期的节点在最上面。在定时器阶段的时候,就会逐个节点遍历,如果节点超时了,那么就执行他的回调,如果没有超时,那么后面的节点也不用判断了,因为当前节点是最快过期的,如果他都没有过期,说明其他节点也没有过期。节点的回调被执行后,就会被删除,为了支持setInterval的场景,如果设置repeat标记,那么这个节点会被重新插入到二叉堆。 我们看到底层的实现稍微简单,但是Node.js的定时器模块实现就稍微复杂【libuv 定时器阶段执行回调】

reader.onprogress = t.step_func(function () {
    var newTime = new Date;
    var timeout = newTime - lastProgressEventTime;

    progressEventTimeList.push(timeout);
    lastProgressEventTime = newTime;
    progressEventCounter++;

    assert_less_than_equal(timeout, 50, "The progress event should be fired every 50ms.");
});

1 Node.js在js层维护了一个二叉堆。

2 堆的每个节点维护了一个链表,这个链表中,最久超时的排到后面。

3 另外Node.js还维护了一个map,map的key是相对超时时间,值就是对应的二叉堆节点。 4 堆的所有节点对应底层的一个超时节点。

当我们调用setTimeout的时候,首先根据setTimeout的入参,从map中找到二叉堆节点,然后插入链表的尾部。必要的时候,Node.js会根据js二叉堆的最快超时时间来更新底层节点的超时时间。当事件循环处理定时器阶段的时候,Node.js会遍历js二叉堆,然后拿到过期的节点,再遍历过期节点中的链表,逐个判断是否需要执行回调。必要的时候调整js二叉堆和底层的超时时间。

4-2 idle、prepare阶段

check、idle、prepare阶段相对比较简单,每个阶段维护一个队列,然后在处理对应阶段的时候,执行队列中每个节点的回调,不过这三个阶段比较特殊的是,队列中的节点被执行后不会被删除,而是会一直在队列里,除非显式删除。

4-3 pending、closing阶段

pending阶段:在poll io回调里产生的回调。 closing阶段:执行关闭handle的回调。 pending和closing阶段也是维护了一个队列,然后在对应阶段的时候执行每个节点的回调,最后删除对应的节点。

4-4 Poll io阶段

Poll io阶段是最重要和复杂的一个阶段 poll io阶段核心的数据结构:io观察者。io观察者是对文件描述符、感兴趣事件和回调的封装

typedef void (*uv__io_cb)(struct uv_loop_s* loop,
                          struct uv__io_s* w,
                          unsigned int events);
typedef struct uv__io_s uv__io_t;

struct uv__io_s {
  uv__io_cb cb;
  void* pending_queue[2];
  void* watcher_queue[2];
  unsigned int pevents; /* Pending event mask i.e. mask at next tick. */
  unsigned int events;  /* Current event mask. */
  int fd;
  UV_IO_PRIVATE_PLATFORM_FIELDS
};

当我们有一个文件描述符需要被epoll监听的时候

1 我们可以创建一个io观察者。

2 调用uv__io_start往事件循环中插入一个io观察者队列。

3 Libuv会记录文件描述符和io观察者的映射关系。

4 在poll io阶段的时候就会遍历io观察者队列,然后操作epoll去做相应的处理。[ tip2 evnet loop ]

5 等从epoll返回的时候,我们就可以拿到哪些文件描述符的事件触发了,最后根据文件描述符找到对应的io观察者并执行他的回调就行。

[tip 2]

Epoll 将“维护监视队列”和“进程阻塞”分离,也意味着需要有个数据结构来保存监视,至少要方便地添加和移除,还要便于搜索,以避免重复添加。

红黑树是一种自平衡二叉查找树,搜索、插入和删除时间复杂度都是 O(log(N)),效率较好,Epoll 使用了红黑树作为索引结构RBR)

blog.csdn.net/armlinuxww/… [深入理解 Nginx:模块开发与架构解析(第二版)]

另外我们看到,poll io阶段会可能会阻塞,是否阻塞和阻塞多久取决于事件循环系统当前的状态。当发生阻塞的时候,为了保证定时器阶段按时执行,epoll阻塞的时间需要设置为等于最快到期定时器节点的时间。

nodejs 【第五步 创建进程--比较重要的】

Node.js中的进程是使用fork+exec模式创建的,fork就是复制主进程的数据,exec是加载新的程序执行。Node.js提供了异步和同步创建进程两种模式。

5-1 异步方式

异步方式就是创建一个人子进程后,主进程和子进程独立执行,互不干扰。在主进程的数据结构中如图所示,主进程会记录子进程的信息,子进程退出的时候会用到

/*
 * uv_process_t is a subclass of uv_handle_t.
 */
struct uv_process_s {
  UV_HANDLE_FIELDS
  uv_exit_cb exit_cb;
  int pid;
  UV_PROCESS_PRIVATE_FIELDS
};

UV_EXTERN int uv_spawn(uv_loop_t* loop,
                       uv_process_t* handle,
                       const uv_process_options_t* options);
UV_EXTERN int uv_process_kill(uv_process_t*, int signum);
UV_EXTERN int uv_kill(int pid, int signum);
UV_EXTERN uv_pid_t uv_process_get_pid(const uv_process_t*);

5-2 同步方式

同步创建子进程会导致主进程阻塞,具体的实现是

1 主进程中会新建一个新的事件循环结构体,然后基于这个新的事件循环创建一个子进程。

2 然后主进程就在新的事件循环中执行,旧的事件循环就被阻塞了。

3 子进程结束的时候,新的事件循环也就结束了,从而回到旧的事件循环。

int uv_spawn(uv_loop_t* loop,
             uv_process_t* process,
             const uv_process_options_t* options) {
#if defined(__APPLE__) && (TARGET_OS_TV || TARGET_OS_WATCH)
  /* fork is marked __WATCHOS_PROHIBITED __TVOS_PROHIBITED. */
  return UV_ENOSYS;
#else
  int signal_pipe[2] = { -1, -1 };
  int pipes_storage[8][2];
  int (*pipes)[2];
  int stdio_count;
  ssize_t r;
  pid_t pid;
  int err;
  int exec_errorno;
  int i;
  int status;

  assert(options->file != NULL);
  assert(!(options->flags & ~(UV_PROCESS_DETACHED |
                              UV_PROCESS_SETGID |
                              UV_PROCESS_SETUID |
                              UV_PROCESS_WINDOWS_HIDE |
                              UV_PROCESS_WINDOWS_HIDE_CONSOLE |
                              UV_PROCESS_WINDOWS_HIDE_GUI |
                              UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS)));

  uv__handle_init(loop, (uv_handle_t*)process, UV_PROCESS);
  QUEUE_INIT(&process->queue);

进程间通信 接下来我们看一下父子进程间怎么通信呢?在操作系统中,进程间的虚拟地址是独立的,所以没有办法基于进程内存直接通信,这时候需要借助内核提供的内存。进程间通信的方式有很多种,管道、信号、共享内存等等。


products.belongsTo(user, { foreignKey: 'userId', targetKey: 'userId', as: 'u' });

进程间通讯 【第六步】--比较重要的

在操作系统中,进程间的虚拟地址是独立的,所以没有办法基于进程内存直接通信,这时候需要借助内核提供的内存。进程间通信的方式有很多种,管道、信号、共享内存等等。

Node.js选取的进程间通信方式是Unix域,Node.js为什么会选取Unix域呢?因为只有Unix域支持文件描述符传递。文件描述符传递是一个非常重要的能力。

首先我们看一下文件系统和进程的关系,在操作系统中,当进程打开一个文件的时候,他就是形成一个fd file inode这样的关系,这种关系在fork子进程的时候会被继承。但是如果主进程在fork子进程之后,打开了一个文件,他想告诉子进程,那怎么办呢?如果仅仅是把文件描述符对应的数字传给子进程,子进程是没有办法知道这个数字对应的文件的。如果通过Unix域发送的话,系统会把文件描述符和文件的关系也复制到子进程中。

static int CreateSocketPair (int SocketFamily,
                             int SocketType,
                             int SocketProtocol,
                             int *SocketPair)
{
    struct dsc$descriptor AscTimeDesc = {0, DSC$K_DTYPE_T, DSC$K_CLASS_S, NULL};
    static const char* LocalHostAddr = {"127.0.0.1"};
    unsigned short TcpAcceptChan = 0,
        TcpDeviceChan = 0;
    unsigned long BinTimeBuff[2];
    struct sockaddr_in sin;
    char AscTimeBuff[32];
    short LocalHostPort;
    int status;
    unsigned int slen;

1 Node.js底层通过socketpair【tip3 如上代码 】创建两个文件描述符,主进程拿到其中一个文件描述符,并且封装send和on meesage方法进行进程间通信。

2 接着主进程通过环境变量把另一个文件描述符传给子进程。

3 子进程同样基于文件描述符封装发送和接收数据的接口。 这样两个进程就可以进行通信了

线程和线程间通信 【第七步】--比较重要的

Node.js是单线程的,为了方便用户处理耗时的操作,Node.js在支持多进程之后,又支持了多线程。Node.js中多线程的架构如下图所示。每个子线程本质上是一个独立的事件循环,但是所有的线程会共享底层的Libuv【tip 4】线程池

当我们调用new Worker创建线程的时候

1 主线程会首先创建创建两个通信的数据结构,接着往对端发送一个加载js文件的消息。

2 然后调用底层接口创建一个线程。

3 这时候子线程就被创建出来了,子线程被创建后首先初始化自己的执行环境和上下文。

4 接着从通信的数据结构中读取消息,然后加载对应的js文件执行,最后进入事件循环。

针对 tip4 聊一下 什么类型的请求 libuv 会把它放到线程池去执行?

主动通过 libuv 发起的操作被 libuv 称为请求( uv_req_t ),libuv 的线程池作用于以下 4 种枚举的异步请求:

UV_FS: fs 模块的异步函数(除了 uv_fs_req_cleanup ),fs.access、fs.stat 等。
UV_GETADDRINFO:dns 模块的异步函数,dns.lookup 等。
UV_GETNAMEINFO:dns 模块的异步函数,dns.lookupService 等。
UV_WORK:zlib 模块的 zlib.unzip、zlib.gzip 等;在 Node.jsAddon(C/C++) 中通过 uv_queue_work 创建的多线程请求。

tip4 -2 线程池是如何初始化的?

static void init_threads(void) {
  unsigned int i;
  const char* val;
  uv_sem_t sem;

  // 6-23 行初始化线程池大小
  nthreads = ARRAY_SIZE(default_threads);
  val = getenv("UV_THREADPOOL_SIZE"); // 根据环境变量设置线程池大小
  if (val != NULL)
    nthreads = atoi(val);
  if (nthreads == 0)
    nthreads = 1;
  if (nthreads > MAX_THREADPOOL_SIZE)
    nthreads = MAX_THREADPOOL_SIZE;

  threads = default_threads;
  if (nthreads > ARRAY_SIZE(default_threads)) {
    threads = uv__malloc(nthreads * sizeof(threads[0]));
    if (threads == NULL) {
      nthreads = ARRAY_SIZE(default_threads);
      threads = default_threads;
    }
  }
  // 初始化条件变量
  if (uv_cond_init(&cond))
    abort();

  // 初始化互斥量
  if (uv_mutex_init(&mutex))
    abort();

  // 初始化队列和节点
  QUEUE_INIT(&wq); // 工作队列
  QUEUE_INIT(&slow_io_pending_wq); // 慢 I/O 队列
  QUEUE_INIT(&run_slow_work_message); // 如果有慢 I/O 请求,将此节点作为标志位插入到 wq 中

  // 初始化信号量
  if (uv_sem_init(&sem, 0))
    abort(); // 后续线程同步需要依赖这个信号量,因此这个信号量创建失败了则终止进程

  // 创建 worker 线程
  for (i = 0; i < nthreads; i++)
    if (uv_thread_create(threads + i, worker, &sem)) // 初始化 worker 线程
      abort(); // woker 线程创建错误原因为 EAGAIN、EINVAL、EPERM 其中之一,具体请参考 man3

  // 等待 worker 创建完成
  for (i = 0; i < nthreads; i++)
    uv_sem_wait(&sem); // 等待 worker 线程创建完毕

  // 回收信号量资源
  uv_sem_destroy(&sem);
}

tip4 -3 请求是如何放到线程池去执行的

ibuv 有两个函数可以创建多线程请求:

uv_queue_work:开发者常用的创建多线程请求的函数。 uv__work_submit:libuv 内部创建多线程请求的函数,实际上 uv_queue_work 最终也是调用的这个函数。

uv__work_submit 函数主要做 2 件事:

调用 init_threads 初始化线程池,因为线程池的创建是惰性的,只有用到的时候才会创建。 调用内部的 post【tip5】 函数将请求插入到请求队列中。

void uv__work_submit(uv_loop_t* loop,
                     struct uv__work* w,
                     enum uv__work_kind kind,
                     void (*work)(struct uv__work* w),
                     void (*done)(struct uv__work* w, int status)) {
  uv_once(&once, init_once);
  w->loop = loop;
  w->work = work;
  w->done = done;
  post(&w->wq, kind);
}

【tip5】 post
判断请求的请求类型是否是 UV__WORK_SLOW_IO: 如果是,将这个请求插到慢 I/O 请求队列 slow_io_pending_wq 的尾部,同时在请求队列 wq 的尾部插入一个 run_slow_work_message 节点作为标志位,告知请求队列 wq 当前存在慢 I/O 请求。 如果不是,将请求插到请求队列 wq 尾部。 如果有空闲的线程,唤醒某一个去执行请求。 并发的慢 I/O 的请求数量不会超过线程池大小的一半,这样做的好处是避免多个慢 I/O 的请求在某段时间内把所有线程都占满,导致其它能够快速执行的请求需要排队。

请求在 worker 执行完后是如何同步 uv loop 所在的线程

回归到 线程之间的通讯

线程和进程不一样,进程的地址空间是独立的,不能直接通信,但是线程的地址是共享的,所以可以基于进程的内存直接进行通信

1 Message代表一个消息。

2 MessagePortData是对操作Message的封装和对消息的承载。

3 MessagePort是代表通信的端点,是对MessagePortData的封装。

4 MessageChannel是代表通信的两端,即两个MessagePort。

void Message::MemoryInfo(MemoryTracker* tracker) const {
  tracker->TrackField("array_buffers_", array_buffers_);
  tracker->TrackField("shared_array_buffers", shared_array_buffers_);
  tracker->TrackField("transferables", transferables_);
}

MessagePortData::MessagePortData(MessagePort* owner)
    : owner_(owner) {
}

MessagePortData::~MessagePortData() {
  CHECK_NULL(owner_);
  Disentangle();
}

void MessagePortData::MemoryInfo(MemoryTracker* tracker) const {
  Mutex::ScopedLock lock(mutex_);
  tracker->TrackField("incoming_messages", incoming_messages_);
}

void MessagePortData::AddToIncomingQueue(std::shared_ptr<Message> message) {
  // This function will be called by other threads.
  Mutex::ScopedLock lock(mutex_);
  incoming_messages_.emplace_back(std::move(message));

  if (owner_ != nullptr) {
    Debug(owner_, "Adding message to incoming queue");
    owner_->TriggerAsync();
  }
}

我们看到两个port是互相关联的,当需要给对端发送消息的时候,只需要往对端的消息队列插入一个节点就行。

我们来看看通信的具体过程

1 线程1调用postMessage发送消息。

2 postMessage会先对消息进行序列化。

3 然后拿到对端消息队列的锁,并把消息插入队列中。

4 成功发送消息后,还需要通知消息接收者所在的线程。

5 消息接收者会在事件循环的poll io阶段处理这个消息。

Cluster 【第八步】--比较重要的

Cluster模块使得Node.js支持多进程的服务器架构。支持轮询(主进程accept)和共享(子进程accept)两种模式。可以通过环境变量进行设置

tip6

【 1 主进程调用fork创建子进程。

2 子进程启动一个服务器。 通常来说,多个进程监听同一个端口会报错,我们看看Node.js里是怎么处理这个问题的?】

tip 6 -1 主进程accept 1 首先主进程fork多个子进程处理。

2 然后在每个子进程里调用listen。

3 调用listen函数的时候,子进程会给主进程发送一个消息。

4 这时候主进程就会创建一个socket,绑定地址,并置为监听状态。

5 当连接到来的时候,主进程负责接收连接,然后然后通过文件描述符传递的方式分发给子进程处理。

tip 6 -1 子进程accept 1 首先主进程fork多个子进程处理。

2 然后在每个子进程里调用listen。

3 调用listen函数的时候,子进程会给主进程发送一个消息。

4 这时候主进程就会创建一个socket,并绑定地址。但不会把它置为监听状态,而是把这个socket通过文件描述符【上面有描述~~】的方式返回给子进程。

5 当连接到来的时候,这个连接会被某一个子进程处理。

额外 【第九步 异步通信机制】

异步通信指的是Libuv主线程和其他子线程之间的通信机制。比如Libuv主线程正在执行回调,子线程同时完成了一个任务,那么如何通知主线程,这就需要用到异步通信机制

1 Libuv内部维护了一个异步通信的队列,需要异步通信的时候,就往里面插入一个async节点

2 同时Libuv还维护了一个异步通信相关的io观察者

3 当有异步任务完成的时候,就会设置对应async节点的pending字段为1,说明任务完成了。并且通知主线程。

4 主线程在poll io阶段就会执行处理异步通信的回调,在回调里会执行pending为1的节点的回调。

下面我们来看一下线程池的实现。

1 线程池维护了一个待处理任务队列,多个线程互斥地从队列中摘下任务进行处理。

2 当给线程池提交一个任务的时候,就是往这个队列里插入一个节点。

3 当子线程处理完任务后,就会把这个任务插入到事件循环本身维护到一个已完成任务队列中,并且通过异步通信的机制通知主线程。

4 主线程在poll io阶段就会执行任务对应的回调。

举个例子 DNS

因为通过域名查找ip或通过ip查找域名的api是阻塞式的,所以这两个功能是借助了Libuv的线程池实现的。发起一个查找操作的时候,Node.js会往线程池提及一个任务,然后就继续处理其他事情,同时,线程池的子线程会调用库函数做dns查询,查询结束后,子线程会把结果交给主线程。这就是整个查找过程。

其他的dns操作是通过cares实现的,cares是一个异步dns库,我们知道dns是一个应用层协议,cares就是实现了这个协议。我们看一下Node.js是怎么使用cares实现dns操作的。 1 首先Node.js初始化的时候,会初始化cares库,其中最重要的是设置socket变更的回调。我们一会可以看到这个回调的作用。

2 当我们发起一个dns操作的时候,Node.js会调用cares的接口,cares接口会创建一个socket并发起一个dns查询,接着通过状态变更回调把socket传给Node.js。

3 Node.js把这个socket注册到epoll中,等待查询结果,当查询结果返回的时候,Node.js会调用cares的函数进行解析。最后调用js回调通知用户。

全局来看--设计的逻辑

可以看到 uv loop 里面其实就是在不断的循环去更新计时器、处理各种类型的回调、轮询 I/O 事件,Node.js 的异步便是通过 uv loop 完成的。

libuv 的异步采用的是 Reactor 模型进行多路复用,在 uv__io_poll 中去处理 I/O 相关的事件, uv__io_poll 在不同的平台下通过 epoll、kqueue 等不同的方式实现。所以当往 async_wfd 写入内容时,在 uv__io_poll 中将会轮询到 async_wfd 可读的事件,这个事件仅仅是用来通知 uv loop 线程: 非 uv loop 线程有回调需要在 uv loop 线程执行。

当轮询到 async_wfd 可读后,uv__io_poll 会回调对应的函数 uv__async_io,它主要做了下面 2 件事:

读取数据,确认是否有 uv_async_send 调用,数据内容并不关心。 遍历 async_handles 句柄队列 ,判断是否有事件,如果有的话执行它的回调。

调用线程池的 h->async_cb 后会回到线程池的 uv__work_done 函数:

void uv__work_done(uv_async_t* handle) {
  struct uv__work* w;
  uv_loop_t* loop;
  QUEUE* q;
  QUEUE wq;
  int err;

  loop = container_of(handle, uv_loop_t, wq_async);
  uv_mutex_lock(&loop->wq_mutex);
  // 清空已完成的 loop->wq 队列
  QUEUE_MOVE(&loop->wq, &wq);
  uv_mutex_unlock(&loop->wq_mutex);

  while (!QUEUE_EMPTY(&wq)) {
    q = QUEUE_HEAD(&wq);
    QUEUE_REMOVE(q);

    w = container_of(q, struct uv__work, wq);
    // 如果在回调前调用了 uv_cancel 取消请求,则即使请求已经执行完,依旧算出错
    err = (w->work == uv__cancelled) ? UV_ECANCELED : 0;
    w->done(w, err);
  }
}

最后通过 w->done(w, err) 回调 uv__fs_done,并由 uv__fs_done 回调 JS 函数:


static void uv__fs_done(struct uv__work* w, int status) {
  uv_fs_t* req;

  req = container_of(w, uv_fs_t, work_req);
  uv__req_unregister(req->loop, req);

  // 如果取消了则抛出异常
  if (status == UV_ECANCELED) {
    assert(req->result == 0);
    req->result = UV_ECANCELED;
  }

  // 回调 JS
  req->cb(req);
}

【下面是一段 直接可复制使用的sequelize】


products.findAll({
    attributes: ['prdName', 'price'],
    include: [{
        model: user,
        as: 'u',
        attributes: ['userName']
    }],
    //raw:true
}).then(result => {
    console.log(JSON.stringify(result))
}).catch(err => {
    console.log(err)
})


####tableName 表名, u为别名。 输出结果如下

[    {        "prdName": "ipad",        "price": 4.99,        "u": { "userName": "张三" }    },    {        "prdName": "iphone",        "price": 3.658,        "u": { "userName": "张三" }    },    {        "prdName": "联想笔记本",        "price": 9.32,        "u": { "userName": "李四" }    }]

换个写法 让username 提取出来 放到第一层

products.findAll({
    attributes: [Sequelize.col('u.userName'),'prdName', 'price'],
    include: [{
        model: user,
        as: 'u',
        attributes: []
    }],
    raw:true
}).then(result => {
    console.log(JSON.stringify(result))
}).catch(err => {
    console.log(err)
})
[    {        "userName":"张三",        "prdName":"ipad",        "price":4.99    },    {        "userName":"张三",        "prdName":"iphone",        "price":3.658    },    {        "userName":"李四",        "prdName":"联想笔记本",        "price":9.32    }]

再比如我要筛选 user这张表的 userid 加条件的写法 如下

products.findAll({
    attributes: [Sequelize.col('u.userName'), 'prdName', 'price'],
    include: [{
        model: user,
        as: 'u',
        attributes: []
    }],
    where: {
        prdName: 'ipad',
        '$u.userId$': 1
    },
    raw: true
}).then(result => {
    console.log(JSON.stringify(result))
}).catch(err => {
    console.log(err)
})

对应sql: SELECT u.userName, p.prdName, p.price FROM products AS p LEFT OUTER JOIN user AS u ON p.userId = u.userId WHERE p.prdName = ‘ipad’ AND u.userId = 1;

思考 如果给include 表加where条件 须使用'u.userIdu.userId'这种写法;也可在include加where条件

事务的总结如下

function doit() {
    //启用事务(自动提交)
    return sequelize.transaction(function (t) {
        return user.create({
            userName: '黄晓明',
            birthDay: '1991-06-23',
            gender: 0
        }, {
                transaction: t
            }).then(result => {
                return user.update({
                    userName: '李四',
                }, {
                        where: { userId: result.userId },
                        transaction: t  //注意(事务transaction 须和where同级)second parameter is "options", so transaction must be in it
                    })
            })
    }).then(result => {
        // Transaction 会自动提交
        // result 是事务回调中使用promise链中执行结果
        // console.log(result.length)
        console.log("ok")
    }).catch(err => {
        // Transaction 会自动回滚
        // err 是事务回调中使用promise链中的异常结果
        console.log(err)
    })
}

配合事务使用 循环

const Op = Sequelize.Op;
const Promise = require('bluebird');
function recycle() {
    let tranArray = [];
    products.findAll({
        attributes: ['prdId', 'prdName', 'userId', 'price'],
        raw: true
    }).then(result => {
        result.forEach(rec => {
            tranArray.push(products.create({
                prdName: rec.prdName,
                userId: rec.userId,
                price: rec.price
            }))
        })
        return Promise.all(tranArray)
    }).then(result => {
        console.log('result' + result)
    }).catch(err => {
        console.log('err' + err)
    })
}