18

0x01: 也谈nodejs

nodeJS的出现,让很多主攻页面交互的前端有种后端原来也不难的错觉。抛开这点不谈,在当前的众多关于nodeJS的事件循环机制的博文总结中。反正中文资料中能看得上的文章真是少之又少。

EventLoop

其实准确点来说,nodeJS中的EventLoop机制和浏览器端是相差不大的,毕竟脱胎于V8引擎。node的实现和浏览器的实现本质上还是一样的。

浏览器中的EventLoop

定义

event-loop 规范

为了协调事件,用户交互,脚本运行,渲染,网络等事件,用户代理必须使用事件循环机制,有两种类型的事件循环机制:一种是浏览上下文,一种是workers。在每个用户代理中至少需要有一个浏览上下文事件循环,并且最多只有一个与之相关的同源浏览上下文。Worker事件循环也是类似的,每个Worker都有一个事件循环,并且worker处理模型管理事件循环的生命周期。

处理模型

一个事件循环必须能够不断地运行在以下这些东西存在的时候:

  1. 让事件循环任务队列中的一个最古老的任务成为Oldest Task
  2. 将当前运行的任务队列设置为`Oldest Task
  3. 运行oldestTask,将当前任务队列设置为null,从任务队列中清除oldestTask
  4. 执行microtask的检查点。
  5. 更新render
  6. 如果有worker,但是在工作队列中没有任务并且WorkerGlobalScope对象的closing标志位是true,那么销毁这个worker事件循环。

microtask

微任务最初是被入队到microtask队列中中的而不是task队列中,有两种类型的微任务:单独的微任务和复合的微任务。当用户代理也就是浏览器执行微任务检查的时候,如果其标志位是false,那么用户代理在将标志位设置为true以后,会像task任务队列一样进行类似的处理:

  1. 设置 flag 为true;
  2. 当事件循环的微任务队列不为空的时候,执行与任务队列相似的逻辑
  3. 对于每个 environment settings object ,通知rejected promise
  4. 清除索引数据库事务
  5. 将flag设置为false;

如果一个复合的微服务正在运行,那么用户代理需要去执行一个复合微任务的子任务以运行以下步骤:

  1. 让parent为事件循环的当前运行的任务,也就是当前运行的复合微服务队列
  2. 让子任务称为一个新的任务
  3. 将当前运行的任务设置为子任务
  4. 运行子任务
  5. 将事件循环的当前运行任务设置为parent

task

task 是一个有序工作 列表的集合:

  • Events: 对一个特定的EventTarget调度特定的Event Object 通过使用专用的task
  • Parsing: HTML parser 符号化一个或者多个字节,然后处理这些符号
  • Callbacks: 调用一个回调函数
  • Using a resource: 如果以非阻塞的方式获取,那么一旦一个或者多个资源可用,那专用的task将会被执行
  • Reacting to DOM manipulation: 对 DOM 的操作。

node 环境下的event loop

其实在node环境下,因为是在服务端进行编程,上述的一些像HTML文件解析,对DOM的操作的实现都是不会有的,而在node环境中,新加了一些系统级的API,比如文件系统,网络等等一些浏览器中没有的东西。但是本质上,这两者之间是没有区别的。

需要在node中注意的一个是这个东西,process.nextTick() ,一旦当前的事件循环完成以后,那么在这个函数回调中的函数将会被执行,也就是说它是会在下一轮的开始之前执行这个函数中的回调。这个函数是优先于其它IO事件的。

异步解决方案

Promise

Promise的产生本质上是由于node或者浏览器端的异步所带来的良好特性,而大多数开发人员的思维是同步的,也就是正常情况下,开发者思考的逻辑顺序就是代码。由异步所带来的烦恼则是,由于很多的异步函数是通过回调函数产生的,那么不可避免的是会产生多个回调函数嵌套的情况,其实技术上还是有解决方案的,比如可以设置一个哨兵变量,一个公共操作的变量,这样在哨兵变量触发的时候,就将公共的操作变量传过去。

当然以上这种做法是异步编程中的一种解决方式,也就是利用事件发布/订阅模式来解决,在node中有原生的events模块,以及第三方库EventProxy模块,这是一种解决方式。当然了,另一种解决方式现在已经被各大浏览器所实现,promise/A 方案。

promise 规范的目的在于在实现上利用异步的优势,在开发使用上,使用同步的开发方式,一般来说,开发着在写规范时更习惯于同步的方式。promise的原理是将异步操作在promise对象内部自己控制。举个例子:

const fs = require('fs');
function readFile() {
    return new Promise(function(resolve, reject){
                fs.readFile('test.txt', function (error, data){
                    if (error) {
                        reject(error);                    
                    }
                    resolve(data);
                });            
    });
}

readFile()
    .then(data => {
        console.log('data: ', data);    
    })
    .catch(error => {
        console.log(error);    
    });

在上面的代码中,其实运行readFile()以后返回的是一个Promise对象,这个Promise对象的状态为pending,也就是说,等待处理中。当调用then方法的时候,在数据准备好之后,就会执行该回调函数。当然,promise的整个实现是在第一节中提到的event loop中的微服务基础上形成的。

events

在node中,有一个原生模块events,Node的核心模块是构建在异步驱动基础上的,这种基础就是特定的对象(也就是被称为触发源)会触发一个已经定义好的函数对象,这个函数对象是将会在将来被调用的。这个方式也是异步解决方案之一,如果再加上EventProxy事件处理模块,整个解决方案就很好了。

const em = require('events');
em.on('hello', function(){
// todo
});

em.emit('hello');
0x02: 也谈nodejs