大家好,我来 CNode 灌水啦,希望大家捧场。
最近在整理自己在工作中使用到的 Nodejs 的知识点,都是抽自己早起和晚上的休息时间写,每次写一点。以前没写过博客,现在发现写博客好累,自己在写的过程中也发现了自己的不足,很多东西只会用,并未深究。在写的时候也是一个查漏补缺、深入了解的过程,也算有所成长。希望和大家分享一下。写的不好不要骂哦~
Node.js 是异步 IO,在遇到 IO 操作时,Node.js 会在底层再开一个线程去处理 IO,等 IO 执行完后再执行回调函数,导致 Node.js 里回调函数泛滥。比如
const fs = require('fs');
fs.readFile('/etc/passwd', 'UTF-8', (err, data) => {
if (err) {
console.error(err);
} else {
console.log(data);
}
});
读写文件、数据库、网络请求处处都是回调,当业务逻辑越来越复杂时,回调彼此嵌套,难以区分,代码就非常难看和难以维护。
以一个实例说明。
一个用 callback 方式实现的用户注册实例
假设现在要实现一个注册方法,要先来判断用户名是否存在,如果不存在就创建用户,创建成功后给用户发一封邮件。并有以下 API
- User.findByUsername(username, callback) 根据用户名查找用户并返回
- User.create(attributes, callback) 创建用户并返回用户
- sendEmail(email, subject, content, callback) 发送邮件,不返回任何值
// 用 callback 的方式实现注册
function register(attributes, callback) {
User.findByUsername(attributes.username, (user) => {
// callback 1
if (!user) {
User.create(attributes, (newUser) => {
// callback 2
if (newUser) {
sendEmail(newUser.email, '注册成功', '恭喜您注册成功', () => {
// callback 3
console.log('注册成功,邮件已发送');
callback(true);
});
} else {
callback('注册失败');
}
});
} else {
callback('用户名已被使用');
}
});
}
上面一共用了 3 个回调,每个回调都会增加一层缩进,并多一层嵌套,这让代码的层级结构很复杂,很难阅读和维护。
callback 的两个问题,太长的缩进和看不清开始和结束的嵌套
为了解决这个问题,后来有了Promise。
Promise/A+
使用 Promise 可以让你的异步代码进行链式调用,关于 Promise 有好几个规范,这里讲 Promise/A+ 规范。抽取最重要的几个约定整理如下
- Promise 即一个拥有 then 方法的对象(这个 then 方法必须符合下面的约定)
- then 方法有两个参数
then(onFulfilled, onRejected)
- 这两个参数都是可选的
- 当方法执行按预期完成时,将方法输出的值传入 onFulfilled 并调用(这里的值可以是你想到的任何值,包括 undefined、promise)
- 当方法抛出异常时,将调用 onRejected 方法并传入该异常
- onFulfilled 和 onRejected 都只能调用一次
- then 方法必须返回一个 promise(链式调用的基础)
尝试根据 fs.readFile 写一个 Promise 风格的 readFile 函数
const fs = require('fs');
function readFile(path, options) {
return new Promise((resolve, reject) => {
fs.readFile(path, options, (err, data) => {
if (err) {
return reject(err); // err 会被传入 onRejected 函数
}
resolve(data); // data 将会作为 onFulfilled 的第一个参数
});
});
}
事实上已经有一些库提供了将 callback 转为 Promise 的方法,比如 bluebird
const fs = require('fs');
const Promise = require('bluebird');
const readFile = Promise.promisify(fs.readFile);
Promise 的使用方法
readFile('/etc/passwd', 'UTF-8')
.then((file) => {
console.log(file);
})
.catch((err) => {
console.error(err);
});
catch 会捕获前面 promise 链式调用中未捕获的异常
方法不用再传入回调函数,而是在 then 里传入函数处理结果。这样做的好处是让代码始终保持在平级(callback 是逐级加深缩进)、不嵌套,易读性好了很多。并且可以统一在后面处理异常。
a()
.then(function () {
}).then(functino() {
}).then(function() {
}).catch(function () {
});
从 a 方法到后面的任何一个 then 里如果抛出异常都会进入 catch。
Promise 的还有一个好处是可以让多个方法并行统一处理结果。对于 a、b、c 都返回 promise 的情况,若要让 a、b、c 并行执行并且等待全部执行完毕
Promise.all([a(), b(), c()]).then((results) => {
// results 是三个 promise 的结果
});
用 Promise 改进后的注册代码
假设已用将注册所用到的三个方法改为了 Promise 风格,然后再重写注册方法
function register(attributes) {
let user;
User
.findByUsername(attributes.username)
.then((_user) => {
if (!_user) {
return User.create(attributes);
} else {
throw new Error('用户名已存在');
}
})
.then((_user) => {
user = _user;
return sendEmail(user.email, '注册成功', '恭喜您注册成功');
})
.then(() => {
doSomethingElse();
})
.then(() => {
resolve(user);
})
}
可以看到用 Promise 的代码结构比 callback 清晰得多,已经避免了代码嵌套,缩进也得到了解决。
Promise 比 callback 有了很大改进,但是仍然不够完美。需要写大量的括号和方法申明,代码格式上相比同步代码调用仍然不够精简。而且在逻辑流程控制上不自然,比如
a().then((ret) => {
// 1
if(ret) {
return b(ret);
}
}).then((ret) => {
// 2
if(ret) {
doSomething();
}
}).then((ret) => {
// 3
doXXX();
});
在 1 里当没有 ret 的时候希望直接跳到 3 是不行的,只能什么都不返回然后在 2 里再判断一次。
你可能觉得可以在 1 的 if 里调用 2,但是这样就让所有的 then 不在同一级上,代码退回了 callback 一样的风格,难以阅读和维护。
而且真实的逻辑往往比这更加复杂。
另外 Promise 的这种做法在变量赋值和传递上也不自然,比如上面的 user 变量,为了后面的步骤可以访问,不得不提前在代码外层申明。
后来 ES6 引入了 Generator 函数,解决了以上所有这些问题。
Generator 函数
想了解 Generator 函数的这有一篇博客写得很详细,这里只简单说一下。申明 generator 和申明一个方法类似,只是在 function 后面加了一个 *
function* nums() {
yield 1;
yield 2;
yield 3;
}
// generator 在被调用时不会执行方法体内的代码,只是返回一个对象
const numsGenerator = nums();
// 调用 next() 方法,方法体内的代码开始执行,并在第一个 yield 处停止
let ret = numsGenerator.next();
console.log(ret); // { value: 1, done: false }
// 再次调用,接着从刚才停止的地方执行,到第二个 yield 结束
ret = numsGenerator.next();
console.log(ret); // { value: 2, done: false }
// 第三个 yield
ret = numsGenerator.next();
console.log(ret); // { value: 3, done: false }
// 结束
ret = numsGenerator.next();
console.log(ret); // { value: undefined, done: true }
这里要注意的是,当有 n 个 yield 要执行时,实际上要调用 n + 1 次才会真正跑完整个函数。
next() 方法是可以传参的,其传入的值会作为上一次 yield 语句的结果,比如
function* nums() {
const n = yield 1;
const m = n * 2;
}
const numsGenerator = nums();
let ret = numsGenerator.next();
console.log(ret.value); // 1
// 将上一次 yield 的值代入,即 n = 1
// 如果这里传入的不是 ret.value,而是 4,那么代入后值将为 4 * 2 = 8
ret = numsGenerator.next(ret.value);
console.log(ret.value); // 1 * 2 = 2
如果调用 next 时没有把上一次的值传入,则视上一个 yield 表达式的值为 undefined,即 n = undefined;也可以传入其他值,无论传入什么值,都会替换掉上一个 yield 的结果。
自动执行的 generator
基于 generator 的机制,可以写一个方法自动去执行 generator
function co(generator) {
const gen = generator();
let ret = gen.next();
while (ret && !ret.done) {
ret = gen.next(ret.value);
}
}
function* nums() {
const n = yield 1;
console.log(n);
const m = n * 2;
console.log(m);
}
co(nums); // 输出 1 2
原理是通过一个 while 循环,检测 ret.done 是否为 true,否则就自动调用 next 方法,并将上一次调用的结果作为参数传入。
yield 后可接方法
function sum(a, b) {
return a + b;
}
function* nums() {
const n = yield 1;
console.log(n);
const m = yield sum(n, 5);
console.log(m);
}
co(nums); // 输出 1 6
gen.next() 会返回方法调用后的结果
当 yield 的方法返回 Promise
比如
function* nums() {
const n = yield readFile('/etc/passwd');
console.log(n);
const m = yield readFile('/etc/bashrc');
console.log(m);
}
对于 yield readFile('/etc/passwd')
,next() 得到的值就是 Promise,然后调用 promise.then,在 onFulfilled 里再次调用 next 即可。
function co(generator) {
const gen = generator();
function goon(ret) {
if (!ret || ret.done) {
return;
}
if (ret.value instanceof Promise) {
const promise = ret.value;
promise.then((value) => {
ret = gen.next(value);
goon(ret);
}).catch(console.error);
} else {
ret = gen.next(ret.value);
goon(ret);
}
}
const ret = gen.next();
goon(ret);
}
co(nums); // 输出文件的内容(先确认你是否有这两个文件)
因为设计到异步代码,不能直接用 while 来实现迭代,所以这里改为用一个递归调用的函数来实现迭代。代码很简陋,但是说明原理已经足够。
用 generator 改写注册用户
function* register(attributes) {
let user = yield User.findByUsername(attributes.username);
if (user) {
throw new Error('用户名已存在');
}
user = yield User.create(attributes);
yield sendEmail(user.email, '注册成功', '恭喜您注册成功');
yield doSomethingElse();
return user;
}
Amazon! 这才是该有的写法。Promise 版的注册代码有 22 行,而 generator 版包括一个空行也才 11 行,少了一半代码。而且代码看起来更自然,写起来跟写同步代码几乎没有区别,只是在异步方法前需要加一个 yield。这感觉棒极了!
ES7 async/await
ES7 提供了新的规范专门处理异步问题,规则很简单
- 在 function 关键字前加一个
async
来申明此方法为异步方法 - 在异步方法内用
await
等待一个 Promise 返回结果
相比 generator
,只是把 function*
换成了 async function
, yield
换成了 await
。
再来改写注册代码
async function register(attributes) {
let user = await User.findByUsername(attributes.username);
if (user) {
throw new Error('用户名已存在');
}
user = await User.create(attributes);
await sendEmail(user.email, '注册成功', '恭喜您注册成功');
await doSomethingElse();
return user;
}
说实话并没有太大变化,但是从语意上要更清晰。generator 的设计意图是为了提供迭代功能,但是无意间解决了异步问题,还是要借助一些第三方库来执行,而使用 ES7 的语法原生就能得到支持。
async/await
目前在 NodeJS v7.1.0+ 已经得到支持。
头像为啥改了 gravatar 和 github 都不行呢?
@sharejishu 建议把正文发出来,然后留下博客地址效果更好。如果大家还要点一下才能看,估计很多就不会做的,你说呢?
@i5ting 恩,下次一定把正文贴出来
@sharejishu 棒棒哒~坚持总结写博客,以后真的会受益无穷,加油
马克加索尔