JavaScript关于异步场景解决方案发展历程:回调函数 => Promise => Generator生成器函数 => async函数。
以文件读取示例:
阶段1,回调函数
const fs = require('fs')
const path = './package.json'
const readFile = (path, cb) => {
fs.readFile(path, (err, data) => {
if (err) cb(err)
else cb(null, data)
})
}
readFile(path, (err, data) => {
if (!err) {
console.log(JSON.parse(data))
} else {
console.log(err)
}
})
优点:
- 解决了同步的问题
缺点:
回调函数本身并没有问题,它的问题出现在多个回调函数嵌套。
- 嵌套函数过多,回调中嵌着回调,造成回调地狱(callback hell)。导致调试困难,错误处理不易
- 嵌套函数存在一定耦合性,一旦有所改动,就会牵一发而动全身。
- 回调函数没有返回值(不要试图用return),仅仅被用来在函数内部执行某些动作。
readFile(path1, (err, data) => {
if (!err) {
console.log(JSON.parse(data))
readFile(path2, (err, data) => {
if (!err) {
console.log(JSON.parse(data))
readFile(path3, (err, data) => {
if (!err) {
console.log(JSON.parse(data))
} else {
console.log(err)
}
})
} else {
console.log(err)
}
})
} else {
console.log(err)
}
})
阶段2,Promise
const fs = require('fs')
const path = './package.json'
const readFileAsync = (path) => {
return new Promise((resolve, reject) => {
fs.readFile(path, (err, data) => {
if (err) reject(err)
else resolve(data)
})
})
}
readFileAsync(path)
.then(data => {
console.log(JSON.parse(data))
})
.catch(err => {
console.log(err)
})
优点:
- 解决了回调地狱的问题,使得原本多层级的嵌套代码,变成了链式调用,让代码更清晰。
- 异常捕获和处理异常简单多啦,只要在最后catch一下。
readFileAsync(path1)
.then(data => {
console.log(JSON.parse(data))
return readFileAsync(path2)
})
.then(data => {
console.log(JSON.parse(data))
return readFileAsync(path3)
})
.then(data => {
console.log(JSON.parse(data))
})
.catch(err => {
console.log(err)
})
缺点:
- 原来的任务被Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得不清楚。
阶段3-基础,Generator生成器函数 + Promise
Generator生成器是ES6(ES2015)的新特性。当我们调用一个生成器函数的时候,它并不会立即执行, 而是需要我们手动的去执行迭代操作(next方法)。
也就是说,你调用生成器函数,它会返回给你一个迭代器。迭代器会遍历每个中断点。
通俗的讲,当你在执行一个函数的时候,你可以在某个点暂停函数的执行,并且做一些其他工作,然后再返回这个函数继续执行, 甚至是携带一些新的值,然后继续执行。
const util = require('util')
const fs = require('fs')
const path1 = './package1.json'
const path2 = './package2.json'
const path3 = './package3.json'
const readFileAsync = function *() {
yield util.promisify(fs.readFile)(path1)
yield util.promisify(fs.readFile)(path2)
yield util.promisify(fs.readFile)(path3)
}
const g = readFileAsync()
g.next().value
.then(data => {
console.log(JSON.parse(data))
return g.next().value
})
.then(data => {
console.log(JSON.parse(data))
return g.next().value
})
.then(data => {
console.log(JSON.parse(data))
return g.next().value
})
.catch(err => {
console.log(err)
})
优点:
- Generator 函数将异步操作表示得很简洁
缺点:
- 流程管理不方便(何时执行第一阶段、何时执行第二阶段)。没有实现自动化的流程管理,需要手动调用next()。
- 一般的yield关键字后面会跟上Promise化的异步函数,因此要在then方法里面调用下一个next方法。于是在代码中任然有一堆then存在。
阶段3-进阶,Co库 + Generator生成器函数 + Promise
Co库是一个为Node.js和浏览器打造的基于Generator生成器函数的流程控制工具,借助于Promise,可以使用更加优雅的方式编写非阻塞代码。TJ大神所写。
注意:Co库使用时,yield后面一定要返回一个Promise对象
const co = require('co')
const util = require('util')
const fs = require('fs')
const path1 = './package1.json'
const path2 = './package2.json'
const path3 = './package3.json'
co(function *() {
const data1 = yield util.promisify(fs.readFile)(path1)
console.log(JSON.parse(data1))
const data2 = yield util.promisify(fs.readFile)(path2)
console.log(JSON.parse(data2))
const data3 = yield util.promisify(fs.readFile)(path3)
console.log(JSON.parse(data3))
return 'test'
}).then(data => {
console.log(data)
// 输出 test
}).catch(err => {
console.log(err)
})
优点:
- 实现自动化流程管理
- 代码中没有一堆then啦
阶段4,Async/Await + Promise 统一世界
在ES7(还未正式标准化)中引入了Async函数的概念,使用async关键字,可以轻松地达成之前使用阶段3方法所做到的工作。8.x之后的node版本可以直接使用。
const util = require('util')
const fs = require('fs')
const path1 = './package1.json'
const path2 = './package2.json'
const path3 = './package3.json'
const readFileAsync = util.promisify(fs.readFile)
async function init() {
const data1 = await readFileAsync(path1)
console.log(JSON.parse(data1))
const data2 = await readFileAsync(path2)
console.log(JSON.parse(data2))
const data3 = await readFileAsync(path3)
console.log(JSON.parse(data3))
}
init()
优点:
- 代码清晰,不用像 Promise 写一大堆 then 链,解决了回调地狱的问题。
缺点:
- await 将异步代码改造成同步代码,如果多个异步操作没有前后依赖,这时使用 await 会降低性能。
扩展阅读,关于Promise
Promise不是简单的语法糖,而是一种关于异步编程的规范,目的是将异步处理对象和处理规则进行规范化,为异步编程提供统一接口。
以下几段代码都是等价的,都是将普通回调函数Promise化。
1. 直接用ES6原生Promise对象
const fs = require('fs')
const readFileAsync = (path) => {
return new Promise((resolve, reject) => {
fs.readFile(path, (err, data) => {
if (err) reject(err)
else resolve(data)
})
})
}
2. 借助一些Promise库,一个流行的选择是使用 bluebird。这些库可能会提供比原生方案更多的功能,并且不局限于Promise/A+标准所规定的特性。
const fs = require('fs')
const Promise = require('bluebird')
const readFileAsync = (path) => {
return new Promise((resolve, reject) => {
fs.readFile(path, (err, data) => {
if (err) reject(err)
else resolve(data)
})
})
}
3. Node.js到8.x版本之后,可以使用Node.js里面的util模块提供的promisify方法轻易的去包装一个回调试的api,让他(这个回调式的api)可以直接支持Promise。
const util = require('util')
const fs = require('fs')
const readFileAsync = util.promisify(fs.readFile)
与君共勉:再牛逼的梦想,也抵不住傻逼般的坚持!