最近一段时间参与开发了一个Node.js后台项目,做为一个PHP开发者,上手项目自己并不难,可是开发的过程却并不顺利,不顺利的主要缘由在于思路上没有转变,没有从同步
的思惟转换到异步
的思惟。html
所谓同步
,就是程序(进程/线程)在一个任务的处理过程当中,不会插入处理其余任务,即便遇到IO等不占CPU的操做,也会一直等待其结束才会继续往下处理。node
所谓异步
,就是程序(进程/线程)在一个任务的处理过程当中,会插入处理其余任务,如遇到IO操做,当前任务会将程序(进程/线程)的控制权释放给其余任务,等IO操做结果返回后再继续往下处理。程序员
简单地讲,同步不会释放控制权,异步会释放控制权。编程
众所周知,Node.js采用的是单线程的异步模型,在具体代码的写法上天然和PHP等同步模型不同。在具体项目开发的过程当中,各类异步操做相关的关键字层出不穷,如:.then()
、function* ... yield
、async...await
等等。为了写一个相似同步的操做,好比:“在执行完A步骤拿到结果以后再执行B步骤”这么一个简单的需求,却要通过大量的反复调试验证才能解决。究其缘由,就是对于这些异步操做的场景和关键字的含义理解不到位,异步操做所提供的选择太多了。api
下面就结合代码实例,理一理这些异步操做方案究竟怎么使用。promise
任务说明:项目根目录下有三个文件Jay.txt
、Angela.txt
、Henry.txt
,依次读取这三个文件的内容并打印。异步
下面使用各类异步处理的方法来完成此任务。async
const fs = require('fs'); fs.readFile('Jay.txt', 'utf8', function (err, data) { if (err) throw err; console.log(data); fs.readFile('Angela.txt', 'utf8', function (err, data) { if (err) throw err; console.log(data); fs.readFile('Henry.txt', 'utf8', function (err, data) { if (err) throw err; console.log(data); }); }); }); console.log("finish");
一、函数 fs.readFile() 用于异步读取文件的全部内容,该函数自己没有返回值。读取的文件内容异步返回后经过回调函数处理。
二、函数fs.readFile()
的第二个参数是可选参数,若是指定了编码方式,则返回对应编码方式的字符串;若是没有指定,则返回文件的二进制内容,对应类型为Buffer,能够经过 buf.toString() 方法转换成对应的字符串。
三、回调函数的第一个参数必须是 错误对象,若是没有错误则错误对象的值为null
。
执行程序:函数
$ node 0A_callback_01.js finish Hello, I'm Jay. Hello, I'm Angela. Hello, I'm Henry.
程序最早返回finish
,是由于函数fs.readFile()
是异步处理的,在调用后会直接继续往下处理,在文件内容返回后经过注册的回调函数处理。学习
串行和并行
一般,人们老是分不清同步
、异步
、串行
、并行
以前的区别,朴素地认为:同步就是串行,异步就是并行
。这么讲彷佛对又彷佛不对。
同步
和异步
是从程序(进程/线程)执行方式的角度来看的,文章开头已经简单讲过同步
和异步
的概念和区别。若是在程序的执行的过程当中不发生任务切换,即:作当前任务的一件事情,等待这件事情完成后,再作当前任务的下一件事情,直到当前任务完成,这种方式就是同步
。若是在程序的执行的过程当中发生任务切换,即:作当前任务的一件事情,不等待这件事情作完,直接转去作其余任务,再作当前任务的下一件事情,如此往复,直到当前任务完成,这种方式就是异步
。
串行
和并行
是从任务(事情)的角度来看的,若是多个任务(事情)不能同时作,而是作完一个才能作下一个,则就将这几个任务(事情)称做是串行
的。若是多个任务(事情)能够同时作,则就将这几个任务(事情)称做是并行
的。
同步就是串行
这句话在必定程度上是正确
的,由于同步程序作完一件事情,才会作下一件事情,从两件事情上看,是不会同时作的,因此同步程序只能串行地作事情。
异步就是并行
这句话就不是那么回事了,异步程序能够选择串行地作事情,也能够选择并行地作事情,是串行作仍是并行作取决于具体的业务场景。对于一个任务下的两件事情A和B,若是B依赖于A的结果,则须要串行;若是B不依赖于A的结果,则能够并行。仍是以本文的读取三个文件为例,上面的代码示例就是串行
执行的,依次读取"Jay.txt"
、"Angela.txt"
、"Henry.txt"
的内容并打印出来。若是要改为并行执行该怎么作呢?简单作个改造就行,以下。
const fs = require('fs'); fs.readFile('Jay.txt', 'utf8', function (err, data) { if (err) throw err; console.log(data); }); fs.readFile('Angela.txt', 'utf8', function (err, data) { if (err) throw err; console.log(data); }); fs.readFile('Henry.txt', 'utf8', function (err, data) { if (err) throw err; console.log(data); }); console.log("finish");
执行程序:
finish Hello, I'm Angela. Hello, I'm Jay. Hello, I'm Henry.
从结果也能够看出,因为三个文件是并行读取的,因此哪一个先读完是随机
的,和代码写的顺序无关。按顺序写的代码就会按顺序执行
,这是典型的同步编程思惟
,要尽快转变过来,不然迟早有一天会“翻车”的。
Promise对象可以表示一个异步操做的状态和结果,使用其提供的.then()
方法能够将多个多个异步操做“串联”起来,.then()
方法自己也返回一个Promise对象。
一样是按顺序读取三个文件的任务,示例以下:
var readFilePromise = require('fs-readfile-promise'); readFilePromise('Jay.txt', 'utf8') .then(function(data) { console.log(data); }) .then(function() { return readFilePromise('Angela.txt', 'utf8'); }) .then(function(data) { console.log(data); }) .then(function() { return readFilePromise('Henry.txt', 'utf8'); }) .then(function(data) { console.log(data); }) .catch(function(err) { console.log(err); }); console.log("finish");
执行程序:
$ node 0B_promise_01.js finish Hello, I'm Jay. Hello, I'm Angela. Hello, I'm Henry.
Promise对象有pending(初始)
、fulfilled(成功)
、rejected(失败)
三种状态。当异步操做成功时,Promise对象从pending状态变为fulfilled状态,并将成功结果传递给.then()
方法的第一个参数(也叫onfulfilled
函数);当异步操做失败时,Promise对象从pending状态变为rejected状态,并将失败信息传递给.then()
方法的第二个参数(也叫onrejected
函数),若是没有指定第二个参数,则将失败信息传递给.catch()
方法的参数(一样也叫onrejected
函数)。
上面的程序,能够将上个文件的处理和下个文件的读取合并到一个.then()
当中,示例以下:
var readFilePromise = require('fs-readfile-promise'); readFilePromise('Jay.txt', 'utf8') .then(function(data) { console.log(data); return readFilePromise('Angela.txt', 'utf8'); }) .then(function(data) { console.log(data); return readFilePromise('Henry.txt', 'utf8'); }) .then(function(data) { console.log(data); }) .catch(function(err) { console.log(err); }); console.log("finish");
.then()
当中,能够返回一个Promise对象,能够返回一个基础类型的值(数字、字符串、布尔值),也能够什么都不返回(直接return;
),甚至连return
语句均可以省略。这几种场景下的处理方式参考:.then()
方法的 返回值说明。
Generator函数
(生成器函数)使用 function* 关键字定义,函数中使用yield
关键字进行流程控制,yield
后面能够跟任何表达式(普通同步表达式、Promise对象、Generator函数)。须要特别注意的是,yield
关键字必须放在Generator函数当中,不然运行时会报错!
Generator函数
的返回值叫作 Generator对象(生成器对象),Generator对象有一个 .next() 方法,每执行一次.next()
方法,就会迭代执行至Generator函数
的下一个yield
语句位置,并返回一个对象。该对象包含两个属性:value
和done
,value
存储了yield
后面表达式的值;done
是一个布尔值,表示Generator函数是否执行完毕。
一样是按顺序读取三个文件的任务,示例以下:
var readFilePromise = require('fs-readfile-promise'); function* generator() { yield readFilePromise('Jay.txt', 'utf8'); yield readFilePromise('Angela.txt', 'utf8'); yield readFilePromise('Henry.txt', 'utf8'); } let gen = generator(); gen.next().value.then(function(data) { console.log(data); gen.next().value.then(function(data) { console.log(data); gen.next().value.then(function(data) { console.log(data); gen.next(); // 返回:{ value: undefined, done: true },表示生成器函数执行结束 }); }); }); console.log("finish");
执行程序:
$ node 0C_generator_01.js finish Hello, I'm Jay. Hello, I'm Angela. Hello, I'm Henry.
本例中,
generator()
是一个生成器函数,其返回值gen
是一个生成器对象。gen.next()
返回的对象结构以下。{ value: Promise { <pending> }, done: false }其中,
gen.next().value
是一个Promise,表示yield
后面的readFilePromise()
函数所返回的是一个Promise对象。
须要注意的是,生成器函数自己包含的各个异步操做并不能按照顺序串行执行,想要实现串行执行的话,仍是须要配合Promise对象及其.then()
函数来实现,如本例所示。
co
函数库是干什么的?co
函数库是Generator函数的一种执行器
。简单来说,co
函数库用来将上一节中手动执行Generator函数的过程自动化
,这样一来,就使得采用同步思惟写异步代码
的想法成为现实。做为曾经是彻底同步思惟
的程序员终于看到了曙光。
一样是按顺序读取三个文件的任务,示例以下:
var co = require('co'); var readFilePromise = require('fs-readfile-promise'); // generator()是一个生成器函数 function* generator() { let data = yield readFilePromise('Jay.txt', 'utf8'); console.log(data); data = yield readFilePromise('Angela.txt', 'utf8'); console.log(data); data = yield readFilePromise('Henry.txt', 'utf8'); console.log(data); } let gen = generator(); // gen是一个生成器对象 co(generator()).then(function() { console.log('Generator function is finished!'); }); console.log("finish");
执行程序:
$ node 0D_co_01.js finish Hello, I'm Jay. Hello, I'm Angela. Hello, I'm Henry. Generator function is finished!
固然,用好co
的前提是有一些注意事项须要知道的:
一、co
函数配套使用的Generator函数
中,yield
后面的异步操做须要返回一个Promise对象,不然就没法实现指望的同步
效果;
二、co
函数自己会返回一个Promise对象,因此如本例所示,是能够在其后使用.then()
方法增长回调函数的。
co
库函数已经将Generator
函数的执行简化了不少,还能更简单一点吗?答案是:有,那就是async
函数。
async
函数与Generator
函数相比,能够简单地理解为:将Generator函数中的*改成async,将yield改成await,就成了async函数
。
async
函数与Generator
函数相比:
async
函数自己内置了执行器,无需再像Generator
函数同样须要引入额外的执行器(如:co
执行器);async...await
与function*...yield
相比,语义更加清晰明了:async
表示函数中有异步操做,await
表示须要等待异步操做返回结果;await
后面除了能够跟Promise对象以外,也能够跟基础类型的值,如:数字、字符串、布尔值,而yield
后面必需要跟Promise对象;async
函数的返回值也是一样是按顺序读取三个文件的任务,示例以下:
var readFilePromise = require('fs-readfile-promise'); async function asyncReadFile() { let data = await readFilePromise('Jay.txt', 'utf8'); console.log(data); data = await readFilePromise('Angela.txt', 'utf8'); console.log(data); data = await readFilePromise('Henry.txt', 'utf8'); console.log(data); return "Async function is finished!" } asyncReadFile().then(function(data) { console.log(data); }); console.log("finish");
执行程序:
$ node 0E_async_await_01.js finish Hello, I'm Jay. Hello, I'm Angela. Hello, I'm Henry. Async function is finished!
从本例能够看出,除了async
和await
这两个关键字以外,总体代码的格式与函数调用方式和同步代码彻底同样。因而可知,异步
代码写法的终极目标就是让异步代码写起来和同步代码同样简单方便
。然而,悲催的是,做为一个JS新人须要花很久才能把这些逐渐弄清楚,学习成本不可谓不高。