第一步先搭建一个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.js 的 Addon(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条件 须使用''这种写法;也可在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)
})
}