Skip to content

Latest commit

 

History

History
952 lines (795 loc) · 60.6 KB

node-QA.md

File metadata and controls

952 lines (795 loc) · 60.6 KB

主要内容

在本章节,主要是一些Nodejs相关内容,牵涉的东西比较多,也是nodejs中比较难以理解的部分。大部分都是在开发中遇到的问题,然后提供的解决思路。如果你有任何问题欢迎issue,同时也欢迎star!

1.nodejs如何处理CPU密集型任务

大多数网站的服务器端都不会做太多的计算,它们只是接收请求,交给其它服务(比如文件系统或数据库),然后等着结果返回再发给客户端。所以聪明的Node.js针对这一事实采用了第二种策略,它不会为每个接入请求繁衍出一个线程,而是用一个主线程处理所有请求。避开了创建、销毁线程以及在线程间切换所需的开销和复杂性。这个主线程是一个非常快速的event loop,它接收请求,把需要长时间处理的操作交出去,然后继续接收新的请求,服务其他用户。下图描绘了Node.js程序的请求处理流程:

主线程event loop收到客户端的请求后,将请求对象响应对象以及回调函数交给与请求对应的函数处理。这个函数可以将需要长期运行的I/O或本地API调用交给内部线程池处理,在线程池中的线程处理完后,通过回调函数将结果返回给主线程,然后由主线程将响应发送给客户端。那么event loop是如何实现这一流程的呢?这要归功于Node.js平台的V8引擎和libuv。

2.nodejs中同步与异步,阻塞和非阻塞

2.1 同步与异步(指令执行顺序)

同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。典型的异步编程模型比如Node.js:

举个通俗的例子:你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下",然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。异步的方式在平时情况还是很多的,比如典型的Promise调用:

  Promise.all([getPersonDrawResult, getLotteryCount])
          .then(values => {})
          .catch(err => {
          });
  console.log('接口请求已经发送出去了!');

你会发现,我们的程序不会等待两个接口请求的返回值,而是直接返回,继续运行下面的代码。而当我们的网络I/O结束后,nodejs会通过事件循环通知主线程去执行回调。这种异步的方式在nodejs中非常常见,比如文件操作等等,但是对于异步有一点要注意:我们的所有回调函数都是串行执行的,即如果一个回调函数耗时太长,那么后续的回调函数也是需要等待的。而同步的方式也比较容易理解,比如函数调用:

function blockFunc(){
  for(let i=0;i<10000;i++){
     console.log(i);
  }
}
blockFunc();
console.log('end');

这里当我们的blockFunc函数没有执行结束之前,我们的end是不会打印出来的,这就是同步的情况!

2.2 阻塞与非阻塞(线程与进程)

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

还是上面的例子,你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果。在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关。

本段内容参考自怎样理解阻塞非阻塞与同步异步的区别?。总之,同步与异步一个是主动等待结果,而另一个是等待别人通知;而阻塞与非阻塞一个表示是否影响当前线程!

参考来自阻塞非阻塞与同步异步区别

3.nodejs中回调函数是否是阻塞的

Node.js中的回调函数一般是指异步操作完成之后调用的函数。基于异步事件模型的Node.js大致是这样运行的:

1.向Node.js提交异步操作,比如建立网络连接,读取网络流数据,向文件写入数据,请求数据库服务等,同时针对这些异步操作注册回调函数。这些异步操作会提交给IO线程池或者工作线程池。

2.在线程池中,操作是并发的执行,也就是读网络流和向文件写数据,或者请求数据库服务都是并发的(可能是这样子的,具体的操作怎么完成,是node的事) ,执行完毕后会将就绪事件放入完成队列中。

3.Node.js 在提交完操作请求之后,进入循环(或叫事件循环吧)。循环的过程如下:

a. 检查有没有计时器超时(setTimeout/setInterval)

b. 检查当前是否为空闲状态,执行空闲任务(process.nextTick)

c. 检查IO完成队列(各种网络流读写、文件读写、标准输入输出上的事件都会进入这个队列)是否有就绪事件, 若完成队列中有就绪事件,就把队列里的所有事件(可能有多个操作已经完成)信息都取出来,对这些事件信息,挨个地调用与其相关的回调函数。这个过程是同步的,执行“写数据完成”事件的回调函数完成之后,才会去调用“读到网络数据”事件的回调函数; 若是队列中没有就绪事件,而且没有空闲(idle)任务,就会做一段时间的等待(线程被阻塞在此处),等待的超时时间由计时器周期决定。(不能因为等待而耽误timer和idle的事件处理)。

d. 进入下一轮循环。从上面这个过程可以看出,你脚本中注册的所有回调函数都是在这个循环过程中被依次调用的。若有一个回调函数执行大的计算任务,很长时间不返回的话,就会让整个循环停顿下来,其它回调函数就不能在事件到来时即时被回调。因此,建议长任务处理过程中,即时将剩下的处理通过process.nextTick丢入下一轮循环中有idle事件中,或者process.spawn一个进程来执行。总之,除了你的代码是同步执行的以外,其它所有的事情都是并发的

4.Node.js中的I/O操作指的是什么

解答:I/O操作包括读写操作、输入输出(硬盘等)、请求响应(网络)等等。

5.for循环里面使用异步调用

var fs = require('fs');
var files = ['a.txt','b.txt','c.txt'];
for(var i=0; i < files.length; i++) {
    //读取文件是异步操作的
    fs.readFile(files[i], 'utf-8', function(err, contents) {
        console.log(files[i] + ': ' + contents);
    });
}

假设这三个文件的内容分别为:AAA、BBB、CCC,我们得到的结果是:

undefined: AAA
undefined: BBB
undefined: CCC

fs.readFile的回调函数中访问到的i值都是循环结束后的值,因此files[i]的值为undefined。可以通过闭包来完成包装,或者通过如下的方式:

var fs = require('fs');
var files = ['a.txt', 'b.txt', 'c.txt'];
files.forEach(function(filename) {
    fs.readFile(filename, 'utf-8', function(err, contents) {
        console.log(filename + ': ' + contents);
    });
});

详见node.js在遇到“循环+异步”时的注意事项

6.Nodejs中的yield顺序执行(for循环中的yield命令)

var co = require('co');
var fs = require('fs');
var readFile = function (fileName){
  return new Promise(function (resolve, reject){
    fs.readFile(fileName, function(error, data){
      if (error) reject(error);
       resolve(data);
    });
  });
};
const fileNames = ['./folder/1.txt','./folder/2.txt','./folder/3.txt'];
co(function*(){
    for(let i=0;i<3;i++){
        var a = yield readFile(fileNames[i]);
        //按顺序读取文件内容
        console.log(a.toString());
    }
})

此时你会发现三个文件中的内容是按顺序输出的,而且因为是co自动执行了Generator函数,他会将yield后文件读取的内容直接赋值给前面的变量,从而我们可以在后面的代码中直接打印文件的内容。总之:

Generator函数是协程在ES6的实现,最大特点就是可以交出函数的执行权(读取一个文件后没有返回结果时候暂停执行)。Generator 函数可以暂停执行和恢复执行(等待请求完成后再恢复执行),这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。 next 方法返回值的 value 属性,是 Generator 函数向外输出数据;next 方法还可以接受参数,这是向 Generator函数体内输入数据。详见Generator与其他异步处理方案

7.异步情况必须弄懂的事件循环

注意:下面内容原文摘自JavaScript 运行机制详解:再谈Event Loop【朴灵评注】JavaScript 运行机制详解:再谈Event Loop

7.1同步任务与异步任务

所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。异步执行的机制如下:

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。

如下图,只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。

7.2事件进入任务队列等待主线程读取

任务队列不是事件的队列,不是消息的队列。任务队列就是你在主线程上的一切调用。所谓的事件驱动,就是将一切抽象为事件。IO操作完成是一个事件,用户点击一次鼠标是事件,Ajax完成了是一个事件,一个图片加载完成是一个事件。一个任务不一定产生事件,比如获取当前时间。当产生事件后,这个事件会被放进任务队列中,等待被处理。

I/O设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。"任务队列"中的事件,除了I/O设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取

(1)主线程永远在执行中

主线程永远在执行中,主线程会不断检查事件队列。异步任务不一定要回调函数

(2)事件都在主线程中执行

先产生的事件,先被处理。永远在主线程上,没有返回主线程之说。某些事件也不是必须要在规定的时间执行,有时候没办法在规定的时间执行

(3)定时器事件和普通事件的区别

到达时间点后,会形成一个事件(timeout事件)。不同的是一般事件是靠底层系统或者线程池之类的产生事件,但定时器事件是靠事件循环不停检查系统时间来判定是否到达时间点来产生事件

(4)事件循环中的watcher

准确讲,使用事件驱动的系统中,必然有非常非常多的事件。如果事件产生,都要主循环去处理,必然会导致主线程繁忙。那对于应用层的代码而言,肯定有很多不关心的事件(比如只关心点击事件,不关心定时器事件)。这会导致一定浪费。所以才有了Watcher的概念。事实上,不是所有的事件都放置在一个队列里。不同的事件,放置在不同的队列。当我们没有使用定时器时,则完全不用关心定时器事件这个队列 。当我们进行定时器调用时,首先会设置一个定时器watcher。事件循环的过程中,会去调用该watcher,检查它的事件队列上是否产生事件(比对时间的方式)。 当我们进行磁盘IO的时候,则首先设置一个io watcher,磁盘IO完成后,会在该io watcher的事件队列上添加一个事件。事件循环的过程中从该watcher上处理事件。处理完已有的事件后,处理下一个watcher 。检查完所有watcher后,进入下一轮检查。对某类事件不关心时,则没有相关watcher

(5)异步与非阻塞关系

其实两者都是能够达到并行IO的目的。但是在计算机内核IO而言,异步/同步,阻塞/非阻塞其实完全是两回事。在nodejs中,我们通过主线程事件循环+线程池真实执行IO操作来达到了异步IO的目的,从而摆脱epoll存在的轮询时候没有检测到IO事件就会进入休眠,直到事件发生将它唤醒。epoll虽然真实的利用了事件通知,执行回调的方式,而不是遍历查询,从而不会浪费CPU,提高了执行效率。但是休眠期间CPU几乎是闲置的,所以对于当前线程而言利用率是不够的!

7.3 Event Loop

事件驱动的的实现过程主要靠事件循环完成。进程启动后就进入主循环。主循环的过程就是不停的从事件队列里读取事件。如果事件有关联的handle(也就是注册的callback),就执行handle。一个事件并不一定有callback。比如下图:

上图中(注意上面的callback queue,其实是event queue),主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。 执行栈中的代码(同步任务),总是在读取"任务队列"(异步任务)之前执行。请看下面这个例子。

 var req = new XMLHttpRequest();
  req.open('GET', url);    
  req.onload = function (){};    
  req.onerror = function (){};    
  req.send();

上面代码中的req.send方法是Ajax操作向服务器发送数据,它是一个异步任务,意味着只有当前脚本的所有代码执行完,系统才会去读取"任务队列"。所以,它与下面的写法等价:

  var req = new XMLHttpRequest();
  req.open('GET', url);
  req.send();
  req.onload = function (){};    
  req.onerror = function (){};   

也就是说,指定回调函数的部分(onload和onerror),在send()方法的前面或后面无关紧要,因为它们属于执行栈的一部分,系统总是执行完它们,才会去读取"任务队列"。但是大牛指出两者还是有区别的:

这个调用其实有个默认回调函数,Ajax结束后,执行回调函数,回调函数检查状态,决定调用onload还是onerror。所以只要在回调函数执行之前设置这两个属性就行(但是我目前还没有明白这个默认的回调函数在哪里设置的)。下面给出从调用nodejs的API到线程池中真实执行IO操作,再到事件循环从IO观察者获取到结果并执行的逻辑图:

7.4 异步任务与定时器setTimeout/setInterval

除了放置异步任务的事件,"任务队列"还可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做"定时器"(timer)功能,也就是定时执行的代码。 定时器功能主要由setTimeout()和setInterval()这两个函数来完成,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者则为反复执行。以下主要讨论setTimeout()。 setTimeout()接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数。

console.log(1);
setTimeout(function(){console.log(2);},1000);
console.log(3);

上面代码的执行结果是1,3,2,因为setTimeout()将第二行推迟到1000毫秒之后执行。 如果将setTimeout()的第二个参数设为0,就表示当前代码执行完(注意这里表示:执行栈清空)以后,立即执行(0毫秒间隔)指定的回调函数。

setTimeout(function(){console.log(1);}, 0);
console.log(2);

上面代码的执行结果总是2,1,因为只有在执行完第二行以后,系统才会去执行"任务队列"中的回调函数。 总之,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在"任务队列"的尾部添加一个事件,因此要等到同步任务和*"任务队列"现有的事件*都处理完,才会得到执行。

HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用requestAnimationFrame()的效果要好于setTimeout()。 需要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。具体你可以查看 我的这一篇文章。

7.5 nodejs中的事件循环与异步非阻塞I/O

如下图:

朴灵说上面的图有一点问题:OS Operation不在那个位置,而是在event loop的后面(个人理解:调用IOCP等都是通过liuv实现的,所以朴灵认为它在后面),event queue在event loop中间(来自朴灵评注阮一峰的文章)。

根据上图,Node.js的运行机制如下。

(1)V8引擎解析JavaScript脚本。
(2)解析后的代码,调用Node API。
(3)libuv库负责Node API的执行。它将不同的任务分配给不同的"线程"(其实只有磁盘IO操作才用到了线程池),形成一个Event
    Loop(事件循环),以异步(异步I/O由此而来)的方式将任务的执行结果返回给V8引擎。
(4)V8引擎再将结果返回给用户。

而磁盘IO的过程可以归结为如下流程:

【将调用封装成中间对象,交给event loop,然后直接返回】
【中间对象会被丢进线程池,等待执行】
【执行完成后,会将数据放进事件队列中,形成事件】
【循环执行,处理事件。拿到事件的关联函数(callback)和数据,将其执行】
【然后下一个事件,继续循环】
7.5.1 process.nextTick和setTimeout(0)

除了setTimeout和setInterval这两个方法,Node.js还提供了另外两个与"任务队列"有关的方法:process.nextTick和setImmediate。它们可以帮助我们加深对"任务队列"的理解。

process.nextTick方法可以在当前"执行栈"的尾部,下一次Event Loop(主线程读取"任务队列")之前触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之(我的理解为microtask)。setImmediate方法则是在当前"任务队列"(我的理解是macrotask)的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行,这与setTimeout(fn, 0)很像。请看下面的例子:

process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B(){console.log(2);});
});
setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED

上面代码中,由于process.nextTick方法指定的回调函数,总是在当前"执行栈"的尾部触发,所以不仅函数A比setTimeout指定的回调函数timeout先执行,而且函数B也比timeout先执行。这说明,如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前"执行栈"执行。

7.5.2 setImmediate与setTimeout(func,0)的不确定关系

现在,再看setImmediate。

setImmediate(function A() {
  console.log(1);
  setImmediate(function B(){console.log(2);});
});
setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0);

上面代码中,setImmediate与setTimeout(fn,0)各自添加了一个回调函数A和timeout,都是在下一次Event Loop触发。那么,哪个回调函数先执行呢?答案是不确定(node v6.9.5/v8.11.1上已经验证为不确定)!!。运行结果可能是1--TIMEOUT FIRED--2,也可能是TIMEOUT FIRED--1--2。 令人困惑的是,Node.js文档中称,setImmediate指定的回调函数,总是排在setTimeout前面。实际上,这种情况只发生在递归调用的时候(这也是阮一峰老师给的答案,但是我在node v6.9.5/v8.11.1上得出的结果和他完全相反)。

setImmediate(function (){
  setImmediate(function A() {
    console.log(1);
    setImmediate(function B(){console.log(2);});
  });
  setTimeout(function timeout() {
    console.log('TIMEOUT FIRED');
  }, 0);
});
// 1
// TIMEOUT FIRED
// 2

阮一峰老师说:上面的结果总是固定的,即"1,TIMEOUT FIRED,2"但是我得出的结果仍然是有可能为"TIMEOUT FIRED,1,2"!这也就是告诉我们setImmediate与setTimeout(func,0)的关系是不确定的,而process.nextTick与setTimeout(func,0)前者执行时间要早于后者。但是有一点比较蛋疼,那就是下面的代码输出结果却是确定的:

setImmediate(function(){
    console.log(1);
},0);
setTimeout(function(){
    console.log(2);
},0);
console.log(9);
//输出结果为固定的9,2,1,即setTimeout>setImmediate了(即setTimeout/setImmediate后面有console才行)
//注意必须是和面,中间和前面都会导致输出不确定!!!!

Node官网给出了setImmediate早于setTimeout(func,0)的情况:

// timeout_vs_immediate.js
const fs = require('fs');
fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});

此时输出结果始终是setImmediate早于setTimeout:

$ node timeout_vs_immediate.js
immediate
timeout

$ node timeout_vs_immediate.js
immediate
timeout

知乎上遇到的另外一个问题:

setImmediate(function() {
  console.log('setImmediate111');
});
setTimeout(function() {
  console.log('setTimeout111');
}, 0);
console.log('正常执行11');

作者运行了代码很多次,但是最终结果都是setTimeout早于setImmediate。此时我也不知道如何去解释它,只有按照官网的说法认为他们的关系是不确定的,只是运行的有限次数中,setTimeout早于setImmediate了

事实上,这正是Node.js v0.9.1版添加setImmediate方法的原因,否则像下面这样的递归调用process.nextTick,将会没完没了,主线程根本不会去读取"事件队列"!

process.nextTick(function foo() {
  process.nextTick(foo);
});

现在要是你写出递归的process.nextTick,Node.js会抛出一个警告,要求你改成setImmediate。另外,由于process.nextTick指定的回调函数是在本次"事件循环"触发,而setImmediate指定的是在下次"事件循环"触发,所以很显然,前者总是比后者发生得早,而且执行效率也高(因为不用检查"任务队列")。

7.5.3 process.nextTick与promise.then的关系

这里突然想到知乎上讨论的两个问题:

  • process.nextTick也会放入microtask队列,为什么优先级比promise.then高呢?

    process.nextTick永远大于promise.then原因其实很简单。在Node中,_tickCallback在每一次执行完**TaskQueue中的一个任务后(可以认为是macrotask)**被调用,而这个_tickCallback中实质上干了两件事:

    1. nextTickQueue中所有任务执行掉(长度最大1e4,Node版本v6.9.1)

    2.执行_runMicrotasks函数,执行microtask中的部分(promise.then注册的回调)所以很明显process.nextTick > promise.then”

7.5.4 setTimeout最小延迟4ms
  • 到底setTimeout有没有一个依赖实现的最小延迟?4ms?
1.IE8及更早版本的计时器精度为15.625ms
2.IE9及更晚版本的计时器精度为4ms
3.Firefox和Safari的计时器精度大约为10ms
4.Chrome的计时器精度为4ms
7.6 异步和事件循环Event Loop的区别

但是异步跟event loop其实没有关系。准确的讲,event loop是实现异步的一种机制。 一般而言,操作分为:发出调用和得到结果两步。发出调用,立即得到结果是为同步。发出调用,但无法立即得到结果,需要额外的操作才能得到预期的结果是为异步。同步就是调用之后一直等待,直到返回结果。异步则是调用之后,不能直接拿到结果,通过一系列的手段才最终拿到结果(调用之后,拿到结果中间的时间可以介入其他任务。上面提到的一系列的手段其实就是实现异步的方法,其中就包括event loop。以及轮询、事件等。所谓轮询:就是你在收银台付钱之后,坐到位置上不停的问服务员你的菜做好了没。 所谓(事件):就是你在收银台付钱之后,你不用不停的问,饭菜做好了服务员会自己告诉你。该回答来自【朴灵评注】JavaScript 运行机制详解:再谈Event Loop

8.Node.js中yield*与yield的区别

首先要注意:我们的return语句不会消耗一次next调用比如下面的例子:

function* outer(){
    yield 'begin';
    var ret = yield* inner();
    console.log(ret);
    yield 'end';
}

function * inner(){
    yield 'inner';
    return 'return from inner';
}
var it = outer(),v;
v = it.next().value;
console.log(v);
v = it.next().value;
//这里是第二次调用next方法,返回`inner`
console.log(v);
v = it.next().value;
//这里是第三次调用next方法,*inner这个generator函数直接返回了,而把这次next调用作用到了outer这个generator函数上
console.log(v)

即第三次调用next方法,*inner这个generator函数直接返回了,而把这次next调用作用到了outer这个generator函数上,从而"end"也被打印出来。打印的结果如下:

begin
inner
return from inner
end   

下面讲解下yield和yield*的不同,给出下面的例子:

function* outer(){
    yield 'begin';
    yield inner();
    //这里直接调用Generator函数相当于是返回一个Generator指针
    yield 'end';
}
function* inner(){
    yield 'inner';
}
var it = outer(),v;
v= it.next().value;
console.log(v);
v= it.next().value;
//此时第二次调用next方法直接得到调用inner()的返回值
console.log(v);
console.log(v.toString());
v = it.next().value;
console.log(v);

打印的结果如下:

1.begin
2.inner {[[GeneratorStatus]]: "suspended"}__proto__: Generator[[GeneratorStatus]]: "suspended"[[GeneratorFunction]]: ƒ* inner()[[GeneratorReceiver]]: Window[[GeneratorLocation]]: VM3678:8[[Scopes]]: Scopes[2]
3.[object Generator]
4.end   

所以直接调用yield得到的是一个指向Generator函数的指针,是一个对象。这个对象里面的yield是不会执行的,因为并没有调用他的next方法!

下面我们再给出通过co来运行Generator函数的例子:

var co = require('co');
co(function* (){
    var a = yield Promise.resolve(1);
    console.log(a);
    //1.打印1
    var b = yield later(10);
    //2.resolve的时候传入的是10,yield后返回的是一个Promise
    console.log(b);
    var c = yield fn;
    console.log(c);
    //3.yield一个Generator函数,返回fn_1
    var d = yield fn(5);
    console.log(d);
    //4.yield一个Generator函数调用的返回值,即指针。返回fn_5
    var e = yield [
        Promise.resolve('a'),
        later('b'),
        fn,
        fn(5)
    ];
    console.log(e);
     //5.yield后是一个数组,直接执行数组里面的每一个thunkify函数,[ 'a', 'b', 'fn_1', 'fn_5' ]
    var f = yield{
        'a':Promise.resolve('a'),
        'b':later('b'),
        'c':fn,
        'd':fn(5)
    };   
console.log(f);
  //5.yield后是一个对象,直接执行对象里面的每一个thunkify函数,{ a: 'a', b: 'b', c: 'fn_1', d: 'fn_5' }
function* fn(n){
    n = n || 1;
    var a = yield later(n);
    return 'fn_'+ a;
}
//co里面会将yield后的函数处理为如下内容
  function later(n,t){
        t = t || 1000;
        return function(done){
            setTimeout(function(){done(null,n)},t);
        };
    }
}).catch(function(e){
    console.error(e);
});

上面的例子展示了,在co执行Generator函数遇到yield后,将会进行下面的处理:

//将Object对象转化为Promise
function objectToPromise(obj){
  var results = new obj.constructor();
  var keys = Object.keys(obj);
  var promises = [];
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];
    var promise = toPromise.call(this, obj[key]);
    if (promise && isPromise(promise)) defer(promise, key);
    else results[key] = obj[key];
  }
  return Promise.all(promises).then(function () {
    return results;
  });
  function defer(promise, key) {
    // predefine the key in the result
    results[key] = undefined;
    promises.push(promise.then(function (res) {
      results[key] = res;
    }));
  }
}
//判断一个对象是否是promise就是看是否有then方法
function isPromise(obj) {
  return 'function' == typeof obj.then;
}
//将数组转化为promise处理
function arrayToPromise(obj) {
  return Promise.all(obj.map(toPromise, this));
}
//如果object.next是函数,同时throw也是函数那么就是Generator,比如调用Generator函数的返回值
//var it = outer()这里的it返回true
function isGenerator(obj) {
  return 'function' == typeof obj.next && 'function' == typeof obj.throw;
}
/**
 * Check if `obj` is a generator function.
 * 比如上面的outer.constructor.name就是GeneratorFunction() { [native code] }
 */
function isGeneratorFunction(obj) {
  var constructor = obj.constructor;
  if (!constructor) return false;
  //如果constructor.name或者displayName是GeneratorFunction返回true
  if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
  return isGenerator(constructor.prototype);
}

function toPromise(obj) {
  if (!obj) return obj;
  if (isPromise(obj)) return obj;
  //5.如果是Promise,那么直接返回,不做处理,因为co能处理yield一个Promise的情况!
  if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
  //1.如果是Generator函数或者是指向Generator函数的指针,那么直接包裹该函数并执行
  if ('function' == typeof obj) return thunkToPromise.call(this, obj);
  //2.如果是函数,那么直接将这个函数包裹成为Promise,也就是thunkify处理后接受一个唯一的参数
  //为function(err,res)这种nodejs常见的回调方式!
  if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
  //3.如果是数组,那么数组里面的每一个元素都经过Promise.all处理
  if (isObject(obj)) return objectToPromise.call(this, obj);
  //4.如果是对象,那么对对象中的每一个value都进行Promise处理
  return obj;
}
function thunkToPromise(fn) {
    var ctx = this;
    return new Promise(function (resolve, reject) {
      fn.call(ctx, function (err, res) {
        if (err) return reject(err);
        if (arguments.length > 2) res = slice.call(arguments, 1);
        resolve(res);
      });
    });
  }

9.javascript是单线程怎么理解?

下面的问题来自于知乎问答javascript既然是单线程语言 , 为什么会分主线程和消息线程(event loop) ?

9.1 主线程和工作线程之间如何通信?

实际上,浏览器是通过一个消息队列来实现,工作线程发出数据时,通过事件event,向js主线程发一个通知,然后将数据插入到消息队列中,这样,js在主线程就能收到通知,并从消息队列中去拿到数据(这是一个基本的过程,具体实现视浏览器而定)。

9.2 WebWorker引入之前ajax请求是在一个单独的线程中执行的?

在没有引入web worker之前,javascript确确实实是运行在一个单线程里面!那ajax 怎么说? 回忆一下调用ajax的过程,我们是需要把成功回调传递给xhr,典型的代码如下:

 xhr = new XMLHttpRequest();
 xhr.onreadystatechange=function(){} //传入我们的回调
 xhr.open(...)
 xhr.send(...)

浏览器虽然会在一个单独的线程去进行网络请求,但是我们是通过传递一个回调的方式去处理数据,浏览器在网络请求成功后,然后会切换回js线程来执行我们的回调,也就是说我们所有的js代码都是在js线程中运行的。所以javascript确实是在一个单线程中。

9.3 WebWorker为什么不能共享主线程的变量?

我们的js代码可以运行在js主线程之外,这也是为什么不能在web worker里面直接共享js主线程中定义的变量,不能操作ui (dom树)的原因,因为根本不在一个线程

9.4 WebWorker为什么不能操作UI?

无论是windows c开发,或者java 界面开发,一条黄金原则就是不要在其它线程中操作ui,android中如果发现在非ui线程中操作ui会直接抛出异常!线程之间同步是有开销的,并且面临着同步问题,如果所有的线程都能操作ui,一旦cpu发生线程切换,都会面临数据不完整的风险。例如a线程ui界面改了一半,cpu发生线程切换,b线程又去改同一处。而界面本质上来说只是操作系统对数据在显示器上的一个映射,各个线程操作的都是数据,这么一来,你改我也改,你还没改完我有改,你改了一半我接着改,那还怎么玩,所以要支持多线程,必须提供同步工具(让线程之间不会彼此发生冲突)。而如果在js中支持多线程,不仅会增加js虚拟机的复杂度,也会增加编码的复杂度(程序猿不得不自己处理好同步问题),所以,这才是js 为什么到现在还是一个主线程的本质原因(先忽略web worker)。

9.5 javascript所有的回调都在队列当中?

js vm会将我们所有的回调都会放在一个队列当中,比如我们监听的某个单击事件的回调,当用户单击了我们监听的元素,浏览器捕获到事件,然后就去执行我们的回调,而执行回调的环境都在同一个javasricpt线程中,其实也就是说event loop是在浏览器中的,而javascript是运行在同一个线程当中的!这也是js的特点-异步,node中也是延续了这个特点,当然,为了利用多核cpu,node 提供了child_process 。但这不是严格意义上的多线程,相当于起了多个node实例(在系统进程中可以看到),也就有多个js线程。

9.6 为什么JavaScript被设计成单线程的?

1.The user interface/用户接口: 除了网页显示区域以外的部分,比如地址栏、搜索栏、前进后退、书签菜单等窗口。
2.The brower engine/浏览器引擎: 查询与操作渲染引擎的接口,包含事件驱动引擎,提供浏览器进程及其线程之间的资源共享调度机制。
3.The rendering engine/渲染引擎: 负责显示请求的内容,比如请求到HTML, 它会负责解析HTML 与 CSS 并将结果显示到窗口中,也是后面几个线程或引擎的父级控制线程。
4.Networking/网络: 用于网络请求, 如HTTP请求,执行POST、GET等操作就是由它来处理的。
5.UI backend/UI后台:  绘制基础元件,如消息窗口(alert默认的样子)、下拉选项卡等等。
6.JavaScript interpreter/JavaScript解释器:也就是JavaScript引擎,用于解析和执行JavaScript代码。
7.Data storage/数据存储:数据持久层,在我们浏览页面时,浏览器需要把一些数据存到硬盘或内存上,如Cookies、localStorage、sessionStorage、webSql等。我们用浏览器看到的每一个页面,背后都是由以上的组件或功能来完成的。浏览器完成打开一个页面的整个过程,通俗地说这是页面“渲染”。这里的“渲染”,其实是一个组合概念,即浏览器的“渲染引擎”并不是单独工作的,必须依赖其他引擎(组件),经过某种协同机制联合起来完成页面的展示及交互。

因为JavaScript出生的时候,CPU和OS都不支持多线程,浏览器单进程在运作,渲染线程、JavaScript线程、网络线程等多个线程已经需要排队才能处理了,这种情况下如果将JavaScript设计成多线程有意义吗?考虑到当时的项目需求(运行在浏览器上)、周期(10天)、硬件环境以及软件环境等因素,换作其他人也都会将JavaScript设计成单线程的!完整内容查看知乎问答javascript既然是单线程语言 , 为什么会分主线程和消息线程(event loop) ?

9.7 javascript有哪些线程?

所谓单线程,是指负责解释并执行JS代码的线程只有一个(但是浏览器中还会有各种其他的线程,比如上面的渲染UI的线程),我们不妨叫它主线程。其实还有其他很多线程的,比如进行ajax请求的线程、监控用户事件的线程、定时器线程、读写文件的线程(例如在NodeJS中)等等。但是,所有的这些代码都是在同一个线程中运行的,这一点一定要注意!

10 阻塞IO与非阻塞IO的区别?

操作系统内核对于I/O只有两种方式:阻塞与非阻塞

阻塞I/O的一个特点是调用之后一定要等到系统内核层面完成所有操作后,调用才结束。以读取磁盘上的一段文件为例,系统内核在完成磁盘寻道、读取数据、复制数据到内存中之后,这个调用才结束。阻塞I/O造成CPU等待I/O,浪费等待时间,CPU的处理能力不能得到充分利用。为了提高性能,内核提供了非阻塞I/O。非阻塞I/O跟阻塞I/O的差别为调用之后会立即返回。

非阻塞I/O返回之后,CPU的时间片可以用来处理其他事务,此时的性能提升是明显的。但非阻塞I/O也存在一些问题。由于完整的I/O并没有完成,立即返回的并不是业务层期望的数据,而仅仅是当前调用的状态。为了获取完整的数据,应用程序需要重复调用I/O操作来确认是否完成。 这种重复调用判断操作是否完成的技术叫做轮询。轮询会让CPU处理状态判断,是对CPU资源的浪费,但是有多种方式来尽量减少I/O状态判断对CPU的损耗。现存的轮询技术有以下几种select/poll/epoll:

  • read(同步阻塞) 最原始,性能最低的一种轮询。通过重复调用来检测I/O的状态来完成完整数据的读取。在获取到最终数据之前,CPU一直耗用在等待上。

  • 同步非阻塞IO 同步阻塞I/O的一种效率稍低的变种是同步非阻塞I/O。在这种模型中,设备是以非阻塞的形式打开的。这意味着I/O 操作不会立即完成,read操作可能会返回一个错误代码,说明这个命令不能立即满足(EAGAIN或 EWOULDBLOCK),如如图所示:

  • select(异步阻塞) 它是在read的基础上改进的一种方式,通过对文件描述符上的事件状态(可以写数据、有读数据可用、是否发生错误)来进行判断。select轮询有一个较弱的限制,那就是它采用一个1024长度的数组来存储状态,所以它最多可以同时检查1024个文件描述符,通过文件描述符的状态判断哪些文件读取已经完成。select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。

非阻塞的实现是I/O命令可能并不会立即满足,需要应用程序调用许多次来等待操作完成(简单地说就是轮询)。这可能效率不高,因为在很多情况下,当内核执行这个命令时,应用程序必须要进行忙碌等待,直到数据可用为止,或者试图执行其他工作。正如上图所示的一样,这个方法可以引入I/O 操作的延时,因为数据在内核中变为可用到用户调用read返回数据之间存在一定的间隔,这会导致整体数据吞吐量的降低

  • poll(异步阻塞) 该方案较select有所改进,采用链表的方式来避免数组长度的限制,其次它能避免不需要的检查。但是当文件描述符比较多的时候,它的性能还是十分低下的,但是相对于select来说性能显示有所改进。poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发。

  • epoll(异步非阻塞,epoll的三大要素,mmap、红黑树、链表) 该方案是linux下效率最高的I/O事件通知机制,在进入轮询的时候如果没有检查到I/O事件将会进入休眠,直到事件将它唤醒。它是真实利用了事件通知,执行回调的方式,而不是遍历查询,所以不会浪费CPU,执行效率较高。epoll同样告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销

    另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

    epoll是linux提供的,BSD提供了kqueue,Solaris提供了/dev/poll作为替代方案。如果需要实现更高效的服务器程序,类似epoll这样的接口更被推荐。遗憾的是不同的操作系统特供的epoll接口有很大差异,所以使用类似于epoll的接口实现具有较好跨平台能力的服务器会比较困难。 幸运的是,有很多高效的事件驱动库可以屏蔽上述的困难,常见的事件驱动库有libevent库,还有作为libevent替代者的libev库。这些库会根据操作系统的特点选择最合适的事件探测接口,并且加入了信号(signal) 等技术以支持异步响应,这使得这些库成为构建事件驱动模型的不二选择。实际上,Linux内核从2.6开始,也引入了支持异步响应的IO操作,如aio_read, aio_write,这就是异步IO,AIO。

轮询技术满足了非阻塞I/O确保获取到完整数据的需求,但是相对于应用程序而言,它仍然只能算是一种同步,因为应用程序需要等待I/O完全返回,依旧花费了很长时间来等待,等待的事件CPU要么用于遍历文件描述符的状态,要么用于休眠等待事件发生。

尽管epoll已经利用事件来降低CPU的耗用,但是休眠期间CPU几乎是闲置的,对于当前线程而言利用率不够。那么是否有一种理想的异步IO呢?

我们期望的完美的异步IO应该是应用程序发起非阻塞调用,无需通过遍历或者事件唤起等方式轮询可以直接处理下一个任务,只需在I/O完成后通过信号或者回调函数将数据传递给应用程序即可。在linux下存在这种方式,它原生提供的一种异步I/O方式(AIO)就是通过信号或者回调来传递数据的。但是不幸的是:只有linux下有,而且它还有缺陷-AIO仅支持内核I/O中的O_DIRECT方式读取,到时无法利用系统缓存。

在Windows上异步I/O是通过IOCP完成的。调用异步方法,等待I/O完成后的通知,执行回调,用户无需考虑轮询。但是它的内部是线程池原理,不同之处在于这些线程由内核管理。由于windows平台与*nix平台的差异,Node提供了libuv作为抽象封装层,使得所有平台兼容性的判断都由这一层来完成,并保证上层的node与下层的自定义线程池与IOCP之间各自独立。Node在编译期间会判断平台条件,选择性编译unix目录或者win目录下的源文件到目标程序中。

Node中还存在一些与I/O无关的异步API:

setTimeout()
setInterval()
setImmediate()
process.nextTick()

setTimeout()和setInterval() 与浏览器中API是一致的,分别用于单次和多次定时执行任务,它们的实现原理和异步I/O比较类似,只是不需要I/O线程池的参与。本部分内容来自Node.js学习记录: 异步I/O

11.异步和非阻塞的区别?

在大多数情况下,他们是相同的,只是叫法不同而已,但是在特定的情况下,他们又是有区别的。因此,两者是否有区别视情况而定。

比如,在典型的socket的API中,一个非阻塞的socket表示的是立即返回一个"would block"的错误信息,而一个阻塞的socket却不是立即返回,而是直接进入阻塞状态。对于非阻塞的情况,你可能需要通过类似于'poll'这种方式来决定什么时候去重新连接。

对于异步的socket(windows支持),或者.NET中使用的异步的I/O模式是比较容易实现的。你调用一个方法开始一个操作,而框架在操作真实完成的时候会回调你的方法。但是两者还是有区别,在Win32中,异步的socket将他们的消息放在一个特定的GUI线程中,而.NET中的异步的I/O并不会局限于某一个特定的线程。

因此两者还是有区别的,区别如下:

1.同步和阻塞的含义相同。你调用某一个API,那么当前线程被挂起,直到某一个特定的结果被返回。
2.非阻塞表示:如果结果不能立即返回,那么我们调用的API会通过返回一个错误码立即返回,然后不做其他任何处理。此时必须有相关的方法(select/poll)去查询当前的API调用是否真实完成了。
3.异步表示API立即返回,然后在`后台`去完成这个真实请求,因此必须有相关的方法获取到结果并通知给调用者。

所以,异步和阻塞的主要区别是:异步是等待通知,比如libuv通知V8引擎;而我们的阻塞是主动轮询的过程!回答原文请点击这里

1.异步I/O
  在nodejs中,我们的libuv是总调度者。所以,调度完成以后,会将结果通知给我们的v8引擎,这个过程是异步的。
2.非阻塞I/O
  当调用nodejs的API时候,真实I/O的操作并没有完成,而是立即返回,进而继续运行下面的代码。因此一般需要select/poll方法查询当前的状态

12.在for循环中依赖上一次数据库的查询状态

假如有下面的伪代码:

var co = require('co');
co(function*(){
 for(let i=0;i<100;i++){
    //注意:在for循环里面查询没有问题,但是更新或者插入就会存在问题
    //而且yield后的代码必须等到IO完成后才会执行,这是一定要注意的!
    var componentMap =  yield scope.app.mysql.get("table_name", {
         name:'xxx'
    });
    //依赖上一次的查询状态
    if(componentMap){
       yield update();
    }else{
      yield insert();
    }
  }
})

此时会存在一个问题,当上一次insert/update还没有在多台数据库中同步,下一次for循环又去查询数据库的状态(多台数据库数据可能压根就没有同步),此时会存在数据不同步的问题。解决的方法是在for循环中记录那些记录是需要更新的,那些记录是需要插入的,然后一次性全部更新!

13.nodejs中如何写出同步的ajax请求

  var request = require("sync-request");
  const rawContent = request("GET", url,{}).getBody();

请查看sync-request组件的源码。其实他的原理如下:

child_process.spawnSync(command, [args], [options])这个方法不会立即返回,直到子线程完全被关闭。当出现了timeout或者收到了killSignal信号,这个方法也不会立即退出直到子线程完全退出才行。也就是说,如果当前线程在处理SIGTERM信号,还没有正常退出,那么当前进程就会一直等待它退出完成。sync-request也可以用于浏览器中,因为xhr本身就是支持同步执行的。但是,并不建议这么做,因为该方法会阻塞。

14.nodejs的require限制在特定的文件夹

const ROOT = process.cwd();
const DEFAULT_WEBPACK_MODULES_PATH = path.join(ROOT, "./node_modules");
const TEST = /^\//.test(fullPath[i])
        ? require.resolve(fullPath[i])
        : require.resolve(fullPath[i], {
            paths: [DEFAULT_WEBPACK_MODULES_PATH]
          });

上面的例子展示了,指定paths属性,那么当你require一个模块,比如lodash将会严格限制在process.cwd下的node_modules目录。

15.nodejs如何并发读取多个文件

其实还是通过async.parallel来完成:

const DEFAULT_FOLDER_PATH = path.join(process.cwd(), "./src");
// imports是相对于src目录的相对路径
 const indexExposeModules = imports.map((item, index) => {
      return path.join(DEFAULT_FOLDER_PATH, item.source);
  });
  const functionPools = [],componentInfoArray = [];
      // 产生函数数组
    for (let i = 0, len = indexExposeModules.length; i < len; i++) {
        const localFunc = function(callback) {
          return fs.readFile(indexExposeModules[i], "utf8", (err, data) => {
            if (err) {
              console.log(`读取文件${indexExposeModules[i]}失败!`);
            }
            callback(null, data);
          });
        };
        functionPools.push(localFunc);
      }
      // 并行执行,里面全部是异步操作,所以对于componentInfoArray的打印必须是回调中
      async.parallel(functionPools, (err, results) => {
        if (err) {
          console.log(`并发读取文件失败,程序将会退出!`);
        }
        // react-docgen解析js文件产生内容
        results.reduce((prev, cur) => {
          const documentation = reactDocs.parse(cur);
          componentInfoArray.push(documentation);
        });
         console.log("内容为:" + JSON.stringify(componentInfoArray));
      });
      // 准备生成表格内容
    });

但是有一点需要注意:async.parallel里面封装的是异步的文件读取操作,所以必须在callback里面才能获取到完整的处理后的数据内容,然后对每一个文件的内容进行react-docgen操作。即下面的代码是不行的:

 async.parallel(functionPools, (err, results) => {
    if (err) {
      console.log(`并发读取文件失败,程序将会退出!`);
    }
    // react-docgen解析js文件产生内容
    results.reduce((prev, cur) => {
      const documentation = reactDocs.parse(cur);
      componentInfoArray.push(documentation);
    });
 });
 // callback里面才能获取到componentInfoArray完整内容
 // 这句代码会立即执行
console.log("内容为:" + JSON.stringify(componentInfoArray));

16.nodejs的fs.readFile返回值

因为fs.readFile是异步的,其返回值为undefined,这和fs.readFileSync是不一样的,后者返回的是一个Buffer对象:

const fs = require('fs');
const path = require('path');
const util = require('util');
const INDEX =  path.join(process.cwd(),'./src/lib/AutoSeekCtr/index.js');
const reactDocs = require("react-docgen");
const fsReturnValue = fs.readFile(INDEX,'utf8',(err,data)=>{
  const documentation = reactDocs.parse(data);
});
// 其中fsReturnValue为undefined,如果是readFileSync将会得到完整的Buffer对象
console.log('fsReturnValue=='+util.inspect(fsReturnValue,{showHidden:true,depth:2}));

17.nodejs中打包工具Generator中的require.resolve

使用nodejs开发了一个silk命令,那么在silk的某个generator里面调用了require.resolve方法:

const indexTestJS = path.join(process.cwd(), "./src/index.test.js");
// silk命令打包处理的当前工程的index.test.js
entry = {
  main: require.resolve(indexTestJS)
};
// webpack配置的entry

如果require.resolve本身是一个绝对路径,那么没有问题,然而如果require.resolve是一个第三方的类库,那么此时require.resolve将会相对当前Generator来解析,而不是silk打包的当前工程,所以很可能出现类库找不到的情况。所以就要用到第二个参数:

const resolve = require("resolve");
 rules.push({
    test: require.resolve(
      resolve.sync(fullPath[i], {
        basedir: DEFAULT_WEBPACK_MODULES_PATH
      })
    ),
    use: [
      {
        loader: require.resolve("expose-loader"),
        options: specifier
      }
    ]
  });

那么require.resolve的解析路径到底是怎么样的呢?我们这里来深入了解下,下面是官网的说明:

require(X) from module at path Y
1. If X is a core module,
   a. return the core module
   b. STOP
2. If X begins with '/'
   a. set Y to be the filesystem root
3. If X begins with './' or '/' or '../'
   a. LOAD_AS_FILE(Y + X)
   b. LOAD_AS_DIRECTORY(Y + X)
4. LOAD_NODE_MODULES(X, dirname(Y))
5. THROW "not found"

LOAD_AS_FILE(X)
1. If X is a file, load X as JavaScript text.  STOP
2. If X.js is a file, load X.js as JavaScript text.  STOP
3. If X.json is a file, parse X.json to a JavaScript Object.  STOP
4. If X.node is a file, load X.node as binary addon.  STOP

LOAD_INDEX(X)
1. If X/index.js is a file, load X/index.js as JavaScript text.  STOP
2. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
3. If X/index.node is a file, load X/index.node as binary addon.  STOP

LOAD_AS_DIRECTORY(X)
1. If X/package.json is a file,
   a. Parse X/package.json, and look for "main" field.
   b. let M = X + (json main field)
   c. LOAD_AS_FILE(M)
   d. LOAD_INDEX(M)
2. LOAD_INDEX(X)

LOAD_NODE_MODULES(X, START)
1. let DIRS=NODE_MODULES_PATHS(START)
2. for each DIR in DIRS:
   a. LOAD_AS_FILE(DIR/X)
   b. LOAD_AS_DIRECTORY(DIR/X)

NODE_MODULES_PATHS(START)
1. let PARTS = path split(START)
2. let I = count of PARTS - 1
3. let DIRS = []
4. while I >= 0,
   a. if PARTS[I] = "node_modules" CONTINUE
   b. DIR = path join(PARTS[0 .. I] + "node_modules")
   c. DIRS = DIRS + DIR
   d. let I = I - 1
5. return DIRS

其中有几个点需要注意的:

  • 参照点

    目录Y下require一个模块,此时参考点就是目录Y!

  • node_modules是递归的

    NODE_MODULES_PATHS函数其实将目录Y做了一个切割。请看下面的例子:

当前脚本文件/home/ry/projects/foo.js执行了require('bar')。Node内部运行过程如下:

首先,确定x的绝对路径可能是下面这些位置,依次搜索每一个目录

/home/ry/projects/node_modules/bar
/home/ry/node_modules/bar
/home/node_modules/bar
/node_modules/bar

搜索时,Node先将bar当成文件名,依次尝试加载下面这些文件,只要有一个成功就返回。

bar
bar.js
bar.json
bar.node

如果都不成功,说明bar可能是目录名,于是依次尝试加载下面这些文件。

bar/package.json(main字段)
bar/index.js
bar/index.json
bar/index.node

如果在所有目录中,都无法找到bar对应的文件或目录,就抛出一个错误。

18.如何侵入nodejs原生的require

比如在某一个模板加载之前或者之后插入自己的一段代码,我也是在看了jsdoc后才知道的:

  require = require("requizzle")({
    requirePaths: {
      before: [path.join(__dirname, "lib")],
      after: [path.join(__dirname, "node_modules")]
    },
    infect: true
  });

当然更深入的使用和原理可以查看requizzle

参考资料:

Node.js软肋之CPU密集型任务

Nodejs中 Callback 的执行是否造成阻塞?

Node.js 探秘:初识单线程的 Node.js

同步,异步,阻塞,非阻塞等关系轻松理解 #40

nodejs之yield 和 yield*

JavaScript既然是单线程的,那么异步要怎么理解?

【朴灵评注】JavaScript 运行机制详解:再谈Event Loop

linux中的轮询机制select/poll/epoll

linux IO模型与AIO

Linux下的I/O复用与epoll详解

Is NodeJs Single Threaded – Let’s Find Out

require() 源码解读