现在有一个这样的场景:我们需要通过node.js来读取本地文件并输出到控制台上显示,下面是文件内容:
- 1.txt 内容:111
- 2.txt 内容:222
- 3.txt 内容:333
创建一个 callback.js 文件,其中的代码如下
const fs = require('fs');// 导入node.js文件系统模块 fs
const path = require('path'); // 导入node.js处理路径的模块 path
// 参数 fpath 代表文件路径
function getFileByPath(fpath) {
fs.readFile(fpath, 'utf-8', (err, data) => {
if (err) {
throw err
}else{
return data
}
})
}
const result = getFileByPath(path.join(__dirname, './1.txt'));
console.log(result); // undeined
使用命令:node callback.js 运行,你会发现控制台打印出的是 undefined !为什么呢?我们来分析一下:
- 我们调用了
getFileByPath函数,然后传入了文件路径fpath,如果读入失败,就会抛出错误到控制台,但是控制台并没有抛出错误呀,这可以说明文件读取是成功的 - 既然读取成功了,那么就应该返回读到的数据data,可是,result的值却是 undefined,所以问题就出在
data上面——data没有被正确的返回
原因就在于readFile 是一个异步执行的函数,异步任务并不是按照代码中书写的顺序来执行的
readFile函数并不会被放到主线程上立即执行,而是被放入“任务队列”,这就造成getFileByPath函数瞬间执行完毕而不是等待着readFile函数执行完毕返回data,所以getFileByPath函数返回的就是 undefined。也就是说:当 readFile 函数(异步任务)执行完毕时,getFileByPath函数(同步函数)早就执行完了,所以它的返回值就是 undefined
回调函数
在异步任务中使用回调
为了获取到readFile函数读取文件的返回值,可以对getFileByPath函数传入一个回调函数,当readFile执行完毕就调用回调函数并把读取到的data数据传入,这样我们就一定能获取到异步操作的结果。对函数做如下改进:
// (【fpath——文件路径】,【successCallback——读文件成功执行的回调函数】,【errCallback——读文件失败执行的回调函数】)
function getFileByPath(fpath, successCallback, errCallback) {
fs.readFile(fpath, 'utf-8', function(err, data){
if (err) {
errCallback(err);
} else {
successCallback(data);
}
})
}
getFileByPath(path.join(__dirname, './1.txt'), function(data){
console.log(data)
}, function(err){
console.log(err.message)
});
可以看到,我们传入了两个函数,分别用于处理发生错误的情况和读取成功的情况。一旦成功就会调用 successCallback() 并且把读到的数据 data 传入回调函数;如果失败就会调用 errCallback() ,并且也会把错误信息传入回调函数。这样我们就能成功的读到 data 的数据了
回调地狱
现在,我们需要按顺序依次读出 1.txt、2.txt、3.txt;由于 readFile 是异步函数,所以无法保证执行顺序;不过,嵌套回调函数就行啦
getFileByPath(path.join(__dirname, './1.txt'), function(data){
// 如果 1.txt 读取成功,就去读取 2.txt
getFileByPath(path.join(__dirname, './2.txt'), function(data){
// 如果 2.txt 读取成功,就去读取 3.txt
getFileByPath(path.join(__dirname, './3.txt'), function(data){
console.log(data)
}, function(err){
console.log('1.txt 读取失败',err.message)
})
}, function(err){
console.log('2.txt 读取失败',err.message)
})
}, function(err){
console.log('3.txt 读取失败',err.message)
})
通过嵌套回调函数,我们成功的实现了按顺序执行的异步任务
现在我们只是读取3个文件,就嵌套了3层回调函数,即使是这样,看起来也非常臃肿;假如我们要读取1000个文件,那么我们就要写上1000个嵌套函数!这就是回调地狱,写起来就像这个样子
上图描述的就是回调函数的缺点,即:
- 无法解决嵌套过多的问题
- 不便于书写和维护
- 重复代码过多,可读性很低
Promise
回调函数是Javascript中处理异步任务一种手段或者方式,我们不能因为它容易变臃肿就不可能不用这种手段,只能慢慢的改进它,所以,社区里的编程大神们想出了 Promise 这个好东西。
Promise 能大大简化回调函数的书写,它本质上还是使用的回调函数,它只是换了一种形式,让我们能更方便的使用回调函数
我们创建一个 promise.js 文件,在其中写入代码,使用Promise读取文件就像下面这样:
const promise = new Promise(function () {
fs.readFile('./1.txt', 'utf-8', (err, data) => {
if(err){
throw err
}else{
console.log(data); // 111
}
})
})
Promise立即执行
直接运行上面的代码,你会发现,当你 new 一个 Promise之后,控制台立马就会打印出结果,这与普通的对象有很大不同;Promise的定义就是:只要实例化,就会立即执行
上面的Promise实例会立即执行,但是我们此时不需要它立即执行,希望它能被我们手动执行,所以,可以将 promise实例放进一个函数里,只有当我们去调用函数,promise才会被执行
function getFileByPath(fpath) {
const promise = new Promise(function () {
fs.readFile(fpath, 'utf-8', (err, data) => {
if (err) throw err;
console.log(data); // 111
})
})
}
getFileByPath('./2.txt'); // 222
Promise的状态
只是实例化一个Promise对象,并不能解决我们的问题,因为要处理失败和成功两种异步操作的结果,所以要给Promise实例传入处理失败和成功的回调函数,从而为失败和成功这两种状态指定下一步的操作
Promise有三种状态:Pending(进行中),Fulfilled(已成功),Rejected(已失败),Javascript提供了下面两个回调函数来分别处理失败的操作和成功的操作,它们是内置的,不用自己部署
- fulfilled:将Promise的状态从Pending变为Fulfilled,如果异步操作成功,就会调用这个函数
- rejected:将Promise的状态从Pending变为Rejected,如果异步操作失败,则会调用这个函数
无论是 Promise 的状态从Pending变为Fulfilled还是从Pending变为Rejected,只要发生了变化,之后就再也不会改变,就像整个 Promise 实例凝固了,也称为 resolved(已定型)
为Promise指定回调函数
function getFileByPath(fpath) {
const promise = new Promise(function (resolve, reject) {
fs.readFile(fpath, 'utf-8', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
})
})
// 为了在外部获取到 Promise 实例,所以要将其return
return promise;
}
现在,已经为Promise构造函数指定了回调函数,那就是形参resolve和reject;可问题是,我们并没有去定义resolve和reject这两个函数的内部细节,那要怎样去定义这两个函数呢?
Promise.then()
通常,Promise的回调函数,由调用者去指定,谁调用谁就负责指定;Promise规定,使用 Promise.then() 方法来指定回调
function getFileByPath(fpath) {
const promise = new Promise(function (resolve, reject) {
fs.readFile(fpath, 'utf-8', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
})
})
// 为了在外部获取到 Promise 实例,所以要将其return
return promise;
}
// 定义一个常量 p 用来保存返回的promise实例
const p = getFileByPath('./1.txt');
//使用 then 方法定义回调函数
//then 的形参顺序与 promise构造函数的形参顺序保持一致
//第一个函数代表 resolve(已成功)回调,第二个代表reject(已失败)回调
p.then(function(data){
// promise 内部调用了 resolve 传入获取到的数据 data
console.log(data);
},function(err){
// promise 内部调用了 reject 传入发生的错误信息 err
console.log(err);
})
我们非常有必要搞清楚这段代码的执行顺序:
- 调用 getFileByPath ,继而函数内部创建了一个 Promise 实例,并且立即执行
- 变量 p 获得 getFileByPath 返回的 Promise ,立即执行 then 方法的定义
- 此时,主线程执行完了所有同步代码,开始执行异步代码(readFile函数)
- readFile函数执行完毕,调用then方法指定的回调函数
- 异步任务的操作结果被输出到 控制台
通过分析我们可以发现,异步任务总是等着同步任务都执行完了才会被Javascript主线程执行,不过不用担心,我们已经通过 Promise.then() 为异步任务指定了未来——它执行完毕以后的操作
仔细观察上面的代码,就会发现,我们创建了许多中间变量,比如 promise和p,其实它们都是不必要的,为了写出更加精炼的代码,我们可以这样:
function getFileByPath(fpath) {
// 使用 return 直接返回 Promise 实例
return new Promise(function (resolve, reject) {
fs.readFile(fpath, 'utf-8', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
})
})
}
// getFileByPath函数的返回值 就是Promise实例,所以在后面直接使用then方法
getFileByPath('./1.txt').then(function(data){
console.log(data);
},function(err){
console.log(err.message);
})
Promise.catch()
Promise的第二个参数
现在,我们使用 Promise 来按顺序读取文件,给每个then方法都定义了作为第二个参数的回调函数,第一个参数处理成功的读取,第二个参数处理错误的读取。这种使用多个 then()来指定回调的方式,也叫做链式调用
getFileByPath('./1.txt')
.then(function (data) {
console.log(data);
return getFileByPath('./2.txt')
}, function(err){
console.log(err.message);
return getFileByPath('./2.txt')
})
.then(function (data) {
console.log(data);
return getFileByPath('./3.txt')
}, function(err){
console.log(err.message);
return getFileByPath('./3.txt')
})
.then(function (data) {
console.log(data);
}, function(err){
console.log(err.message);
})
运行上面的代码,你会发现,如果其中一个文件读取出错,代码不会立即停止执行,而是抛出错误,继续执行后面的操作;即,前面的错误不会阻塞会后面的执行;其实这样并不好,绝大多数情况下,在一系列操作中,如果前面某个步骤发生错误,应该抛出错误停止执行,因为再去执行后面的是没有意义的,我们想要得到的是一个严格正确的结果。那么怎样才能让程序只返回完全正确的结果呢?
使用 catch()
Promise实例抛出的错误,具有冒泡的性质,也就是说错误会沿着作用域链一直向后传递,前面的then方法抛出的错误,总是会被传递到写在最后的catch方法,所以我们要使用 catch方法来捕获错误,不必使用then 方法中的第二个参数,我们可以这样写:
getFileByPath('./0.txt').then(function (data) {
console.log(data)
// 如果当前的文件读取成功,那么就返回一个新的 Promise 实例,用于继续读取后面的文件
return getFileByPath('./2.txt');
}).then(function (data) {
console.log(data)
return getFileByPath('./3.txt');
}).then(function (data) {
console.log(data)
})
.catch(function (err) {
console.log(err.message)
})
运行上面的代码,如果某个文件读取出错,那么Promise就会抛出错误到 catch ,并且立即停止执行
**注意:**Promise内部发生的错误不会传递到外部,也就是说如果不用 catch 来捕获错误,我们根本看不到Promise报出的错误,整个程序还是会继续执行
高下立判
在读取文件这个异步操作中:
- 使用回调函数
getFileByPath(path.join(__dirname, './1.txt'), function(data){
console.log(data);
getFileByPath(path.join(__dirname, './2.txt'), function(data){
console.log(data);
getFileByPath(path.join(__dirname, './3.txt'), function(data){
console.log(data);
}, function(err){
console.log(err.message);
});
}, function(err){
console.log(err.message);
});
}, function(err){
console.log(err.message);
});
- 前面的Promise中使用了很多匿名函数,所以可以用箭头函数将其替代,使用 Promise
getFileByPath('./1.txt').then(data => {
console.log(data);
return getFileByPath('./2.txt')
}).then(data => {
console.log(data);
return getFileByPath('./3.txt')
}).then(data => {
console.log(data);
}).catch(err => console.log(err.message));
现在你应该懂了,为什么要使用 Promise 来代替回调函数了吧,Promise 能让我们的异步任务流程更加的清晰,它的链式调用方式非常易用,代码体验比回调函数的嵌套可是好了太多了!
高级用法
Promise.all()
all() 用来一次性处理多个 Promise 实例。它可以将多个 Promise 实例包装成一个新的 Promise 实例,接受一个数组(或者不是数组,但是一定要可迭代),它的用法是:const P = Promise.all([p1, p2, p3])。根据下面两种情况返回各自的结果:
- 只有当
p1、p2、p3的状态都是Fulfilled时,P的状态才转变为 Fulfilled ;此时p1、p2、p3的返回值组成一个数组,传递给P的回调函数 - 只要
p1、p2、p3其中有一个的状态变成Rejected,p的状态就变成 Rejected ,此时第一个状态为Rejected 的实例的返回值,会传递给P的回调函数
还是通过上面读取TXT文件的例子,我们实例化3个 Promise ,分别来读取TXT文件:
const p1 = new Promise((resolve, reject) => {
fs.readFile('./1.txt', 'utf-8', (err, data) => {
if (err) {
reject(err)
} else {
resolve(data)
}
})
})
const p2 = new Promise((resolve, reject) => {
fs.readFile('./2.txt', 'utf-8', (err, data) => {
if (err) {
reject(err)
} else {
resolve(data)
}
})
})
const p3 = new Promise((resolve, reject) => {
fs.readFile('./3.txt', 'utf-8', (err, data) => {
if (err) {
reject(err)
} else {
resolve(data)
}
})
})
然后,我们来使用 Promise.all() 将这三个实例合并起来管理
const P = Promise.all([p1, p2, p3])
P.then(values => {
console.log('This is printed by Promise.all', values)
// 'This is printed by Promise.all' [111, 222 ,333]
}).catch(err => {
console.log(err.message)
})
查看控制台输出发现,Promise.all() 的状态变为了 Fulfilled,因为三个实例都成功的读到了TXT文件(状态变为 Fulfilled),并且三个实例都将自己的读取结果传入了 P.then() 指定的回调函数中。
上面代码演示了第一种情况,下面我们接着来看第二种情况,我们让其中一个 Promise 实例发生错误:
const p2 = new Promise((resolve, reject) => {
// 给出一个不存在的文件路径
fs.readFile('./hello.txt', 'utf-8', (err, data) => {
if (err) {
reject(err)
} else {
resolve(data)
}
})
})
const p3 = new Promise((resolve, reject) => {
// 给出一个不存在的文件路径
fs.readFile('./good.txt', 'utf-8', (err, data) => {
if (err) {
reject(err)
} else {
resolve(data)
}
})
})
然后再来查看控制台输出:
P.then(values => {
console.log('This is printed by Promise.all', values)
}).catch(err => {
console.log(err.message) // ENOENT: no such file or directory hello.txt
})
此时,因为发生了错误,导致p2、p3 的状态都是 Rejected,所以 P 的状态变成了 Rejected,catch() 也捕获到了错误信息,注意,p3 也发生了错误,但是只有第一个 Rejected 的实例(本例中的 p2)的返回值会被传递给 P 的 catch 方法。**注意:**如果其中一个 Promise 实例,自己定义了 catch() 来捕获错误,那么错误不会冒泡到 P.catch() 中,而只会触发自己的 catch()
Promise.allSettled()
Promise.allSettled() 也用来管理多个 Promise 实例,它会等到所有 Promise 实例的状态都凝固了(无论是 Fulfilled 还是 Rejected),才返回实例执行的结果,因为如此,Promise.allSettled()最终的状态总是fulfilled。我们还是用读取TXT的例子来演示Promise.allSettled() 的用法
const p1 = new Promise((resolve, reject) => {
fs.readFile('./1.txt', 'utf-8', (err, data) => {
if (err) {
reject(err)
} else {
resolve(data)
}
})
})
const p2 = new Promise((resolve, reject) => {
// 让 p2 发生错误
fs.readFile('./hello.txt', 'utf-8', (err, data) => {
if (err) {
reject(err)
} else {
resolve(data)
}
})
})
const p3 = new Promise((resolve, reject) => {
fs.readFile('./3.txt', 'utf-8', (err, data) => {
if (err) {
reject(err)
} else {
resolve(data)
}
})
})
const P = Promise.allSettled([p1, p2, p3])
P.then(results => {
console.log('This is printed by Promise.allSettled', results)
}).catch(err => {
console.log(err.message)
})
查看控制台的打印结果,你能清楚的看到每一个 Promise 的状态、哪一个 Promise 实例发生了错误以及错误的详细信息:
[
{ status: 'fulfilled', value: '111' },
{
status: 'rejected',
reason: ['Error: ENOENT: no such file or directory, open hello.txt'] {
errno: -4058,
code: 'ENOENT',
syscall: 'open',
path: 'hello.txt'
}
},
{ status: 'fulfilled', value: '111' }
]
Promise.allSettled() 最大的用途就是能让你清楚的了解到每个 Promise 实例的确切执行情况。再者,有时候,我们不关心异步操作的结果,只关心这些操作有没有结束。这时,Promise.allSettled()方法就很有用
Promise.race()
Promise.race()方法同样是用来管理多个 Promise 实例,只要有一个实例率先改变状态,p的状态就跟着改变。那个最先改变状态的 Promise 实例的返回值,就传递给Promise.race()的回调函数
其实可以这样来理解, race 意为“竞赛、比赛”,多个 Promise 实例之间就好像在“竞赛”,谁最先完成任务,那么谁就决定了Promise.race()最终的状态
const P = Promise.race([p1, p2, p3])
P.then(results => {
console.log('This is printed by Promise.race', results)
// This is printed by Promise.race 111
}).catch(err => {
console.log(err.message)
})
可以看到,1.txt 最先读取完成,所以 p1 的状态最先变为 Fulfilled ,p1 执行的结果也就能传递给 P 了
Promise.resolve() & Promise.reject()
Promise.resolve() 返回一个状态为 resolve 的 Promise 实例,Promise.reject() 返回一个状态为 reject 的 Promise 实例。它们接受任意参数,反正最后返回的都是状态已经凝固了的 Promise 实例。
有时需要将现有对象转为 Promise 对象,Promise.resolve()方法就可以完成这个功能;Promise.reject() 的功能在于:通过使用Error的实例获取错误原因reason,对调试和选择性错误捕捉很有帮助
Promise.resolve() 的用法,有几条规则需要了解:
- 如果参数是 Promise 实例,那么
Promise.resolve将不做任何修改,直接返回这个实例 - 可以不传入参数调用,直接返回一个
resolved状态的 Promise 对象 - 参数不是对象,会返回一个 Promise 对象,状态为
resolved
Promise.finally()
finally()方法用于指定,不管 Promise 对象最后状态如何,都会执行的操作,类似于try catch语句的finally的功能。
promise.then(result => {···})
.catch(error => {···})
.finally(() => {···})
上面的代码中,不管 promise 的状态如何,finally的回调函数都会被执行,其实 finally 就是 then 的一个特例。
总结
本文通过一个读取TXT文件的例子,引出了回调函数,然后逐步的又引入了 Promise 对象。
回调函数跟 Promise 都是Javascript中处理异步任务的方式,但是回调函数存在许多缺点,所以未来将会被 Promise 所取代。而且,在ES6标准中,Promise 的优先级要高于 CallBack 回调函数的优先级,具体内容可以阅读这篇文章 《Understanding Asynchronous JavaScript》
Promise 是一个正在不断进化的异步编程方案,文中并没列出 Promise 的全部方法和属性,只考虑了目前可以稳定使用的属性和方法。
参考链接: