先来回顾下异步操作:
// 需求:要封装一个方法,提供一个要读取文件的路径,要求这个方法能读取文件,并把内容返回给我
const fs = require('fs')
const path = require('path')
// 这是普通读取文件的方式 第一个参数文件的路径。第二个参数编码
fs.readFile(path.join(__dirname, './files/1.txt'), 'utf-8', (err, dataStr) => {
if (err) throw err;
return dataStr;
})
// 封装方法的初衷: 给定文件路径,返回读取到的内容。而上面我们不可以通过return dataStr将内容返回
当主程序跳到封装好的方法里去执行,但是进入后发现fs.readFile()
是个异步方法,主程序不会执行而是放在事件队列中就不管了(让子程序去执行)。然后主程序就跳出这个异步方法,所以就没有return dataStr
。所以就拿不到封装好的方法内部的异步函数的返回值。如果用变量接受此方法的返回值就是undefined
。return不行,我们就用回调函数callback。
// 我们可以规定一下, callback 中,有两个参数,第一个参数,是 失败的结果;第二个参数是 成功的结果;
// 如果路径不合法或者什么导致错误。不thorw掉err而是交给用户处理。所以我们把失败的结果也通过callback交给调用
// 同时,我们规定了:如果成功后,返回的结果,应该位于 callback 参数的第二个位置,此时,第一个位置由于没有出错,所以,放一个 null;如果失败了,则 第一个位置放 Error对象,第二个位置放置一个 undefined
function getFileByPath(fpath, callback) {
fs.readFile(fpath, 'utf-8', (err, dataStr) => {
// 如果报错了,进入if分支后,if后面的代码就没有必要执行了。所以return
if (err) return callback(err);
// console.log(dataStr);
// return dataStr;
callback(null, dataStr);
})
}
//调用
getFileByPath(path.join(__dirname, './files/11.txt'), (err, dataStr) => {
// console.log(dataStr + '-----')
if (err) return console.log(err.message)
//如果没有失败
console.log(dataStr)
})
共用一个callback如果不理解。可以看看下面这种改造的。这种上面callback传一个参数,下面传两个参数的。
// 需求:你要封装一个方法,我给你一个要读取文件的路径,你这个方法能帮我读取文件,并把内容返回给我
const fs = require('fs')
const path = require('path')
//第一个放成功的回调,第二个放失败的回调
function getFileByPath(fpath, succCb, errCb) {
fs.readFile(fpath, 'utf-8', (err, dataStr) => {
if (err) return errCb(err)
succCb(dataStr)
})
}
//调用
getFileByPath(path.join(__dirname, './files/11.txt'), function (data) {
console.log(data + '成功了!')
}, function (err) {
console.log('失败的结果,我们使用失败的回调处理了一下:' + err.message)
})
callback也会有个问题,当回调函数嵌套回调函数时,嵌套多了,会出现回调地狱,将来就会很难维护。
//换一种调用方式
// 需求: 先读取文件1,再读取文件2,最后再读取文件3 这是异步读取,我们来一个异步嵌套就可以了
// 如果层级很深的话 ==> 回调地狱
getFileByPath(path.join(__dirname, './files/1.txt'), function (data) {
console.log(data)
getFileByPath(path.join(__dirname, './files/2.txt'), function (data) {
console.log(data)
getFileByPath(path.join(__dirname, './files/3.txt'), function (data) {
console.log(data)
})
})
})
使用 ES6 中的 Promise,来解决 回调地狱的问题。
Promise 的本质:就是单纯的为了解决回调地狱问题;并不能帮我们减少代码量;
Promise 是一个 构造函数,既然是构造函数, 那么,我们就可以 new Promise() 得到一个 Promise 的实例;
在 Promise 上,有两个函数,分别叫做 resolve(成功之后的回调函数) 和 reject(失败之后的回调函数);
在 Promise 构造函数的 Prototype 属性上,有一个 .then() 方法,也就说,只要是 Promise 构造函数创建的实例,都可以访问到 .then() 方法
Promise 表示一个 异步操作;每当我们 new 一个 Promise 的实例,这个实例,就表示一个具体的异步操作;
既然 Promise 创建的实例,是一个异步操作,那么,这个 异步操作的结果,只能有两种状态:
5.1 状态1: 异步执行成功了,需要在内部调用 成功的回调函数 resolve 把结果返回给调用者;
5.2 状态2: 异步执行失败了,需要在内部调用 失败的回调函数 reject 把结果返回给调用者;
5.3 由于 Promise 的实例,是一个异步操作,所以,内部拿到操作的结果后,无法使用 return 把操作的结果返回给调用者; 这时候,只能使用回调函数的形式,来把 成功 或 失败的结果,返回给调用者;
我们可以在 new 出来的 Promise 实例上,调用 .then() 方法,【预先】 为 这个 Promise 异步操作,指定 成功(resolve) 和 失败(reject) 回调函数;
const fs = require('fs');
var promise = new Promise(function(){
// 这个 function 内部写的就是具体的异步操作。比如下面这个,就是读文件的异步操作
fs.readFile();
})
比如封装读取文件的异步操作的方法
// 初衷: 给路径,返回读取到的内容
function getFileByPath(fpath) {
//如果不return这个promise。函数内部的变量,外部无法访问(也就是调用时)
return new Promise(function (resolve, reject) {
fs.readFile(fpath, 'utf-8', (err, dataStr) => {
//注意下面的reject和resolve是两个函数。得传递,不能直接调用
if (err) return reject(err);
resolve(dataStr);
})
})
}
//那么调用时就可以通过.then指定resolve和reject
getFileByPath('./files/2.txt').then(function (data) {
console.log(data);
}, function (err) {
console.log(err.message);
})
Promise执行步骤:
定义函数
调用方法
进入方法new一个Promise实例,并立即执行后面括号中的function.
function中的异步读文件操作还没执行完。就将promise return出去了
拿到promise实例
通过.then执行两个回调
function中的异步读文件操作执行完。访问reject()或者resolve()函数
那么对于上面存在回调地狱的读取文件的回调函数嵌套,写法就是下面这样了。可以看到,比起呈阶梯状的回调地狱好很多。
// 读取文件1
getFileByPath('./files/1.txt')
.then(function (data) {
console.log(data)
// 读取文件2 在第一个.then()里面return了一个新的promise对象
return getFileByPath('./files/2.txt')
}, function (err) {
console.log('这是失败的结果:' + err.message)
// return 一个 新的 Promise
return getFileByPath('./files/2.txt')
})//第一个.then()后面可以继续.then()。这个.then()操作的就是上一个.then()中返回的promise
.then(function (data) {
console.log(data)
return getFileByPath('./files/3.txt')
}, function (err) {
console.log('这是失败的结果:' + err.message)
return getFileByPath('./files/3.txt')
})
如果有多个 promise 需要依次处理, 支持链式编程.then()
,前提条件:前一个promise
必须返回(return)
一个promise对象
promise解决了回调地狱的问题,但是可读性并没有那么高。
正常使用promise来进行异步操作
function myFun() {
return new Promise((resolve, reject) => {
let data = '123'; //假设一个耗时的异步操作获取到数据data
resolve(data);
}
}
myFun().then((vo)=>{
//通过.then()使得我们在异步操作完成获取到数据后才进行下面的代码
console.log(vo);
})
单一的Promise,它的then链看起来还没什么,如果需要处理由多个 Promise 组成的 then 链的时候,async/await优势就能体现出来了。async和await的使用可以使得异步的代码有了同步的风格。
从字面意思来理解。async 是”异步“的简写,而 await 可以认为是 async wait 的简写。所以应该很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。
async 会将其后的函数(函数表达式或 Lambda)的返回值封装成一个 Promise 对象,而 await 会等待这个 Promise 完成,并将其 resolve 的结果返回出来。
function myFun() {
return new Promise((resolve, reject) => {
let data = '123'; //假设一个耗时的异步操作获取到数据data
resolve(data);
})
}
//async 会将其后的函数的返回值封装成一个 Promise 对象,而 await 会等待这个 Promise 完成,并将其 resolve 的结果返回出来
//await后的函数必须为promise,而如果myFun本身就是promise函数,那么加不加async都行
async function getData() {
let data = await myFun();
console.log(data);
}
getData();
Promise通过.then()去解决了回调地狱的问题,而async/await则是进一步优化了then链,使得异步代码有了同步的风格。
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
promise.all()可以将多个promise实例包装成一个新的promise实例。可以让多个异步promise并发进行。
它通常在启动多个异步任务并发运行并为其结果创建承诺之后使用,以便人们可以等待所有任务完成。此方法在集合多个 promise
的返回结果时很有用。比如说页面需要两个或以上的ajax请求返回数据后才正常显示
function fun1(){
return new Promise((resolve, reject) => {
//promise 1
})
}
function fun1(){
return new Promise((resolve, reject) => {
//promise 2
})
}
function getData() {
Promise.all([fun1, fun2]).then((result) => {
console.log(result)
}).catch((error) => {
console.log(error)
})
}
//如果使用async/await
async function getData() {
await promise.all([fun1, fun2]);
}
成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject
失败状态的值。Promise.all()
获得的成功结果的数组里面的数据顺序和Promise.all()
接收到的数组顺序是一致的,即fun1
的结果在前,即便fun1
的结果获取的比fun2
要晚。这带来了一个绝大的好处:在前端开发请求数据的过程中,偶尔会遇到发送多个请求并根据请求顺序获取和使用数据的场景,使用Promise.all()
毫无疑问可以解决这个问题。