一看就懂的Promise/async/await

一看就懂的Promise/async/await

之前一直不太懂这几个的用法,现在一次总结了,并且自己设计了实际例子,请务必从头看到尾,跟着我的思路一步一步走,并复制我的代码到js文件中亲自测试,代码并不复杂,你一定能看懂。

传统回调

获取用户信息

首先我们来看获取用户信息的例子,可复制代码执行测试一下

// 根据用户id获取用户信息
function getUserInfo(uid) {
    // 假设这是从网络接口中返回的json数据
    let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "John", "age": 25, "gender": "男", "company_id": 1 }, "2": { "name": "Thomas", "age": 22, "gender": "男", "company_id": 2 }, "3": { "name": "Lily", "age": 18, "gender": "女", "company_id": 3 } } };

    // 获取成功
    if (jsonData.code == 10001) {
        return jsonData.data[uid];
    } else {
        // 获取失败
        return jsonData.msg;
    }
}

let uid = 1;
let userInfo = getUserInfo(1);

// 打印用户信息
console.log(userInfo);

遇到问题

上边的例子中,我为了演示,直接把用户数据写在代码里了,而现实中我们都知道,这种数据肯定是需要通过接口从后台获取的,而调用接口需要一定的时间,假设接口返回数据要0.8秒,为了方便,数据我还是直接写在代码里,但是我用setTimeout函数来模拟经过0.8秒钟后数据返回,但是你脑子里要知道,这个数据本应该用ajax请求接口获取的,我直接把数据写在代码只不过是模拟,不然还得自己搞一个接口之类的来测试,不方便,我这样做你直接把我的代码复制粘贴到js文件中或html文件中就能运行测试。

使用setTimeout模拟接口延迟返回的例子

// 根据用户id获取用户信息
function getUserInfo(uid) {
    setTimeout(function() {
        // 假设这是从网络接口中返回的json数据
        let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "John", "age": 25, "gender": "男", "company_id": 1 }, "2": { "name": "Thomas", "age": 22, "gender": "男", "company_id": 2 }, "3": { "name": "Lily", "age": 18, "gender": "女", "company_id": 3 } } };

        // 获取成功
        if (jsonData.code == 10001) {
            return jsonData.data[uid];
        } else {
            // 获取失败
            return jsonData.msg;
        }
    }, 800);
}

let uid = 1;
let userInfo = getUserInfo(uid);

// 出现问题,打印undefined,因为数据延迟返回,无法直接打印出来
console.log(userInfo);

以上代码无法打印出userInfo,这个大家应该都能理解,因为console.log()打印时,userInfo数据还没返回呢,所以打印为undefined。

回调函数

以上遇到的问题,大家应该都知道,可以用回调函数的方式来解决,如下所示

// 根据用户id获取用户信息
function getUserInfo(uid, callback1, callback2) {
    // 模拟请求网络接口,需要0.8秒才能返回结果,1秒等于1000毫秒,
    // 0.8秒就是800是毫秒,setTimeout第二个参数是毫秒数
    setTimeout(function() {
        // 假设这是从网络接口中返回的json数据
        let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "John", "age": 25, "gender": "男", "company_id": 1 }, "2": { "name": "Thomas", "age": 22, "gender": "男", "company_id": 2 }, "3": { "name": "Lily", "age": 18, "gender": "女", "company_id": 3 } } };

        // 获取成功
        if (jsonData.code == 10001) {
            return callback1(jsonData.data[uid]);
        } else {
            // 获取失败
            return callback2(jsonData.msg);
        }
    }, 800);
}

let uid = 1;

// 使用两个回调函数,一个是成功,一个是失败
getUserInfo(uid, function(userInfo) {
    console.log(userInfo);
}, function(msg) {
    console.log(msg);
});

// =====================================

// 上边这样可能有朋友不容易理解,那我换种写法,先定义好两个回调函数
function success(userInfo) {
    console.log(userInfo);
}

function failed(msg) {
    console.log(msg);
}

// 然后把函数名作为参数传进去,注意不能用引号,用引号就变成字符串了
getUserInfo(uid, success, failed);

以上代码中,getUserInfo()函数接收三个参数,第一个还是uid,而第二和第三个参数接收一个函数作为参数(没错,参数不一定只是变量或常量,把函数看成一个整体,整个函数也可以作为一个参数)。

当接口返回后,根据是返回成功还是返回失败,在getUserInfo()中调用对应的回调函数,并把从网络接口中获取到的用户信息传给该函数,然后我们只需要在这个函数中处理获取到的用户信息或者返回的错误信息就行,由于是等待网络数据返回之后才回过头来调用这个之前已经写好的函数,我们称这个函数为回调函数

需要注意callback1和callback2是形参,并不是说它非得用这个名字,你用aaa和bbb也可以,只要调用的时候用aaa()和bbb()就行,因为它们分别代表成功和失败,你也可以写成success和failed,这样调用的时候就是success()和failed(),这个命名只要能让写代码的人容易知道它是干什么用的就好,并不是说非得规定要叫什么名字。

获取信息增多

假设我们不仅仅需要用户信息,还需要用户的公司信息,如下所示,我们需要在获取用户信息成功的回调函数中再调用一次获取公司信息的函数,原理也是一样,它也有成功和失败两个回调函数,最终也是在它的回调函数中获得接口返回的数据

// 根据公司id获取公司信息
function getCompany(company_id, callback1, callback2) {
    setTimeout(function() {
        let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "腾讯", "city_id": 1 }, "2": { "name": "字节跳动", "city_id": 2 }, "3": { "name": "阿里巴巴", "city_id": 3 } } };
        // 获取成功
        if (jsonData.code == 10001) {
            return callback1(jsonData.data[company_id]);
        } else {
            // 获取失败
            return callback2(jsonData.msg);
        }
    }, 800);
}

// 根据用户id获取用户信息
function getUserInfo(uid, callback1, callback2) {
    // 模拟请求网络接口,需要0.8秒才能返回结果,1秒等于1000毫秒,
    // 0.8秒就是800是毫秒,setTimeout第二个参数是毫秒数
    setTimeout(function() {
        // 假设这是从网络接口中返回的json数据
        let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "John", "age": 25, "gender": "男", "company_id": 1 }, "2": { "name": "Thomas", "age": 22, "gender": "男", "company_id": 2 }, "3": { "name": "Lily", "age": 18, "gender": "女", "company_id": 3 } } };

        // 获取成功
        if (jsonData.code == 10001) {
            return callback1(jsonData.data[uid]);
        } else {
            // 获取失败
            return callback2(jsonData.msg);
        }
    }, 800);
}

let uid = 1;
// 根据用户id获取用户信息
getUserInfo(uid, function(info) {
    let company_id = info.company_id;
    // 根据前面获取到的公司id获取公司信息
    getCompany(company_id, function(company) {
        console.log(company);
    }, function(msg) {
        console.log(msg);
    });
}, function(msg) {
    console.log(msg);
});

获取信息再次增多

这次我继续增加了获取公司所在的城市,以及获取到城市后再获取城市所在的省份,也是一层一层的嵌套,后面一层都需要写在前面一层的成功的回调函数中

// 根据省份id获取省份名称
// 注意,由于北京是直辖市,直辖市属于省级,为了统一
// “省-市-县/区”格式,一般会写成“北京-北京-朝阳区”
function getProvice(provice_id, callback1, callback2) {
    setTimeout(function() {
        let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": "广东", "2": "北京", "3": "浙江" } };
        // 获取成功
        if (jsonData.code == 10001) {
            return callback1(jsonData.data[provice_id]);
        } else {
            // 获取失败
            return callback2(jsonData.msg);
        }
    }, 800);
}

// 根据城市id获取城市信息
function getCity(city_id, callback1, callback2) {
    setTimeout(function() {
        let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "深圳", "provice_id": 1 }, "2": { "name": "北京", "provice_id": 2 }, "3": { "name": "杭州", "provice_id": 3 } } };
        // 获取成功
        if (jsonData.code == 10001) {
            return callback1(jsonData.data[city_id]);
        } else {
            // 获取失败
            return callback2(jsonData.msg);
        }
    }, 800);
}

// 根据公司id获取公司信息
function getCompany(company_id, callback1, callback2) {
    setTimeout(function() {
        let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "腾讯", "city_id": 1 }, "2": { "name": "字节跳动", "city_id": 2 }, "3": { "name": "阿里巴巴", "city_id": 3 } } };
        // 获取成功
        if (jsonData.code == 10001) {
            return callback1(jsonData.data[company_id]);
        } else {
            // 获取失败
            return callback2(jsonData.msg);
        }
    }, 800);
}

// 根据用户id获取用户信息
function getUserInfo(uid, callback1, callback2) {
    // 模拟请求网络接口,需要0.8秒才能返回结果,1秒等于1000毫秒,
    // 0.8秒就是800是毫秒,setTimeout第二个参数是毫秒数
    setTimeout(function() {
        // 假设这是从网络接口中返回的json数据
        let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "John", "age": 25, "gender": "男", "company_id": 1 }, "2": { "name": "Thomas", "age": 22, "gender": "男", "company_id": 2 }, "3": { "name": "Lily", "age": 18, "gender": "女", "company_id": 3 } } };

        // 获取成功
        if (jsonData.code == 10001) {
            return callback1(jsonData.data[uid]);
        } else {
            // 获取失败
            return callback2(jsonData.msg);
        }
    }, 800);
}

let uid = 1;
// 根据用户id获取用户信息
getUserInfo(uid, function(info) {
    let company_id = info.company_id;
    // 根据前面获取到的公司id获取公司信息
    getCompany(company_id, function(company) {
        let city_id = company.city_id;
        // 根据前面获取到的城市id获取城市信息
        getCity(city_id, function(city) {
            let provice_id = city.provice_id;
            // 根据前面获取到的省份信息获取省份名称
            getProvice(provice_id, function(provice) {
                console.log(provice);
            }, function(msg) {
                console.log(msg);
            });
        }, function(msg) {
            console.log(msg);
        });
    }, function(msg) {
        console.log(msg);
    });
}, function(msg) {
    console.log(msg);
});

该例子其实很简单,想通过用户id知道用户的公司在哪个省份,获取步骤为:

  • 1、通过用户id获取用户信息,从用户信息中得到用户工作的公司id;
  • 2、通过上一步得到的公司id,获取公司信息,从公司信息中得到公司所在的城市id;
  • 3、通过上一步得到的城市id,获取城市信息,从城市信息中知道城市所在的省份id;
  • 4、通过上一步得到的省份id,最终获得省份名称。

由于每一层函数调用都要依赖于上一层的返回结果,所以每层要写在上一层的成功的回调函数中,都要缩进一下,每层都有“成功”和“失败”两个回调函数,层层缩进,层层堆叠,像上边这样堆起来,就问你怕不怕。


那有什么办法可以解决这个问题吗?

Promise就是用来解决这个问题的!
Promise就是用来解决这个问题的!
Promise就是用来解决这个问题的!

重要的事情说三遍!

Promise

Promise回调例子

还是以前面回调函数中的例子为例,这样从简单的开始,这样容易理解,为了方便,我直接把那段代码粘过来,这样可以对比着看有什么不同。

经典回调式写法

// 根据用户id获取用户信息
function getUserInfo(uid, callback1, callback2) {
    // 模拟请求网络接口,需要0.8秒才能返回结果,1秒等于1000毫秒,
    // 0.8秒就是800是毫秒,setTimeout第二个参数是毫秒数
    setTimeout(function() {
        // 假设这是从网络接口中返回的json数据
        let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "John", "age": 25, "gender": "男", "company_id": 1 }, "2": { "name": "Thomas", "age": 22, "gender": "男", "company_id": 2 }, "3": { "name": "Lily", "age": 18, "gender": "女", "company_id": 3 } } };

        // 获取成功
        if (jsonData.code == 10001) {
            return callback1(jsonData.data[uid]);
        } else {
            // 获取失败
            return callback2(jsonData.msg);
        }
    }, 800);
}

let uid = 1;

// 使用两个回调函数,一个是成功,一个是失败
getUserInfo(uid, function(info) {
    console.log(info);
}, function(msg) {
    console.log(msg);
});

Promise式写法:看不懂不要紧,可以行复制代码执行一下看看效果,然后往下看,会一段一段分析的

function getUserInfo(uid) {
    // new一个Promise对象
    let p = new Promise(function(callback1, callback2) {
        // 把之前回调函数中的代码整个放进来
        setTimeout(function() {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "John", "age": 25, "gender": "男", "company_id": 1 }, "2": { "name": "Thomas", "age": 22, "gender": "男", "company_id": 2 }, "3": { "name": "Lily", "age": 18, "gender": "女", "company_id": 3 } } };
            // 获取成功
            if (jsonData.code == 10001) {
                return callback1(jsonData.data[uid]);
            } else {
                // 获取失败
                return callback2(jsonData.msg);
            }
        }, 800);
    });

    // 返回Promise对象
    return p;
}

let uid = 1;
// getUserInfo返回的是Promise对象,我们定义一个p变量来接收
let p = getUserInfo(uid);

// p对象调用then()方法,以获取getUserInfo中从网络接口获取的数据
p.then(function(val) {
    // 成功
    console.log(val);
}, function(msg){
    // 失败
    console.log(msg);
});

由以上两个例子的对比可以看出,Promise方式是在getUserInfo()函数中先new一个Promise对象

// new一个Promise对象
let p = new Promise(function(callback1, callback2) {

});

然后把前面传统回调方式中getUserInfo()里的整段代码都放到Promise的回调函数中,当网络接口正常返回数据时,调用callback1,否则调用callback2,最后返回这个Promise对象

function getUserInfo(uid) {
    // new一个Promise对象
    let p = new Promise(function(callback1, callback2) {
        // 把之前回调函数中的代码整个放进来
        setTimeout(function() {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "John", "age": 25, "gender": "男", "company_id": 1 }, "2": { "name": "Thomas", "age": 22, "gender": "男", "company_id": 2 }, "3": { "name": "Lily", "age": 18, "gender": "女", "company_id": 3 } } };
            // 获取成功
            if (jsonData.code == 10001) {
                return callback1(jsonData.data[uid]);
            } else {
                // 获取失败
                return callback2(jsonData.msg);
            }
        }, 800);
    });

    // 返回Promise对象
    return p;
}

调用的时候,先获取到getUserInfo()返回的Promise对象,再用Promise对象(即p)来调用对象中的.then()方法,以获取从getUserInfo()函数中从网络接口获取内容

let uid = 1;
// getUserInfo返回的是Promise对象,我们定义一个p变量来接收
let p = getUserInfo(uid);

// p对象调用then()方法,以获取getUserInfo中从网络接口获取的数据
p.then(function(val) {
    // 成功
    console.log(val);
}, function(msg){
    // 失败
    console.log(msg);
});

你发现没,用这种方式,好像不需要自己定义一个回调函数,Promise自带了。但是这段代码只是简单的告诉你Promise大概怎么用,并没有体现Promise能解决前面多层嵌套的问题,不过后面会说到的,请继续往下看。

Promise例子分析

现在我们来分析一下Promise回调中Promise例子的原理。

首先我们看这个then()方法,它的两个参数都是回调函数,第一个是成功的回调函数,第二个是失败的回调函数

p.then(function(val) {
    // 成功
    console.log(val);
}, function(msg){
    // 失败
    console.log(msg);
});

这两个函数是有顺序的,第一个一定是成功的回调函数,第二个一定是失败的回调函数,不能反过来,因为它们对应的是Promise()回调函数中的两个参数,分别是callback1, callback2,如下所示

// new一个Promise对象
let p = new Promise(function(callback1, callback2) {

});

当Promise里面的ajax请求接收到网络返回的数据之后,调用callback1,而callback1对应的,其实就是then()中的第一个回调函数(表示成功的函数),而调用callback2,对应的就是then()中的第二个回调函数(表示失败的函数),如下图所示(点击图片可放大)

事实上,then()方法其实就是new Promise()时,Promise括号里的那个函数,所以Promise对象调用then()传入的两个函数其实就是两个实参(只不过把整个函数当作一个参数罢了),这两个实参会传到new Promise()括号里的函数的两个形参里,顺序是一一对应的,所以在Promise内部就可以调用这两个传进去的函数(就是回调函数)

失败回调函数可以不要

如下所示,我把then()中的callback2函数去掉了,因为它不是必填参数,它是可选的,然后在then()的后面加了个catch()用于接收错误,也就是说,之前错误本来是调用callback2的,现在因为我没有传callback2进去,它会调用catch(),这个非常重要,在后面的内容会体现

function getUserInfo(uid) {
    // new一个Promise对象
    let p = new Promise(function(callback1, callback2) {
        // 把之前回调函数中的代码整个放进来
        setTimeout(function() {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "John", "age": 25, "gender": "男", "company_id": 1 }, "2": { "name": "Thomas", "age": 22, "gender": "男", "company_id": 2 }, "3": { "name": "Lily", "age": 18, "gender": "女", "company_id": 3 } } };

            // 用新的值覆盖jsonData,模拟接口返回失败
            jsonData = { "code": "10002","msg": "获取失败" }
                // 获取成功
            if (jsonData.code == 10001) {
                return callback1(jsonData.data[uid]);
            } else {
                // 获取失败
                return callback2(jsonData.msg);
            }
        }, 800);
    });

    // 返回Promise对象
    return p;
}

let uid = 1;
// getUserInfo返回的是Promise对象,我们定义一个p变量来接收
let p = getUserInfo(uid);

// p对象调用then()方法,以获取getUserInfo中从网络接口获取的数据
p.then(function(val) {
    // 成功
    console.log(val);
}).catch(function(err) {
    console.log("Catch被调用了!");
    console.log(err);
});

注意1:catch并不一定要放在最后,catch也不会阻止then的执行,也就是说,catch后面还可以继续写then,即使catch捕获到了catch前面的then的报错,也会继续往下走catch后面的then,并不会因为捕获了前面的报错就停止执行了,但一般来说,我们都是把catch写在最后(但是在finally前,后面会说到finally)。

注意2:我只是说then()方法可以不传第二个回调失败的函数,但是Promise那边你还是照常写,照常调用callback2,Promise那边不能不写,如果你不写,它就不会走catch了。

另外,一般来说我都会用一个catch来捕获整个Promise链的错误,而不会在前面的某个then里又写上reject回调函数,如果有,那么错误将会进入reject回调函数,而不会被catch捕获到。

增加获取公司信息

如下所示,Promise方式获取用户信息后再获取公司信息,演示两次链式调用

// 根据公司id获取公司信息
function getCompany(company_id) {
    // new一个Promise对象
    let p = new Promise(function(resolve, reject) {
        setTimeout(function() {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "腾讯", "city_id": 1 }, "2": { "name": "字节跳动", "city_id": 2 }, "3": { "name": "阿里巴巴", "city_id": 3 } } };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[company_id]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });
    return p;
}

// 根据用户id获取用户信息
function getUserInfo(uid) {
    // new一个Promise对象
    let p = new Promise(function(resolve, reject) {
        // 把之前回调函数中的代码整个放进来
        setTimeout(function() {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "John", "age": 25, "gender": "男", "company_id": 1 }, "2": { "name": "Thomas", "age": 22, "gender": "男", "company_id": 2 }, "3": { "name": "Lily", "age": 18, "gender": "女", "company_id": 3 } } };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[uid]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });

    // 返回Promise对象
    return p;
}

let uid = 1;
// 先获取用户信息
let p1 = getUserInfo(uid);

// 调用p1对象的.then()方法,获取网络接口返回的用户信息,获取到返回信息后,又调用
// getCompany(),而我们知道,getCompany返回的也是一个Promise对象,我们把它存到p2
let p2 = p1.then(function(userInfo) {
    // 成功获取到用户信息
    console.log(userInfo);

    // 根据获取到的用户信息中的公司id去获取公司信息,并且把结果return回去,
    // 我们知道,无论是getUserInfo还是getCompany,它们返回的都是Promise对象
    // 所以这里return加去的也是一个Promise对象
    return getCompany(userInfo.company_id);
}, function(msg) {
    // 获取用户信息失败
    console.log(msg);
});

// 前面p1.then返回的Promise对象赋值给了p2,
// 所以p2也是一个Promise对象,也可以调用.then()方法
p2.then(function(companyInfo) {
    // 获取公司信息成功
    console.log(companyInfo);
}, function(msg) {
    console.log(msg);
});

// ==============================================

// 更常用的方式,链式调用,其实就是把前面的p1,p2变量去掉,直接在函数/方法后面用.then()的方式去调用

// 链式调用,因为getUserInfo()返回的是一个promise对象,所以可以调用.then()方法,而由于第一个then()
// 里return getCompany返回的也是一个promise对象,所以又可以在后面继续调用.then()方法,最后一个catch()
// 是用来接收错误的,由于前面的then都没有定义返回错误的回调函数,所以无论前面有几个then(),
// 只要其中一个有错误,它都会一级一级的往下传,最终传到catch()方法中
getUserInfo(uid).then(function(userInfo) {
    // 获取用户信息成功
    console.log(userInfo);
    // 获取公司信息
    return getCompany(userInfo.company_id);
}).then(function(companyInfo) {
    // 获取公司信息成功
    console.log(companyInfo);
}).catch(function(msg) {
    // 捕获到错误信息,无论是前面哪个then()函数的错误,都会传到这里
    console.log(msg);
});

增加获取城市和省份信息

继续增加获取城市和省份的两级接口调用,并介绍一个finally()方法

// 根据省份id获取省份名称
// 注意,由于北京是直辖市,直辖市属于省级,为了统一
// “省-市-县/区”格式,一般会写成“北京-北京-朝阳区”
function getProvice(provice_id) {
    // new一个Promise对象
    let p = new Promise(function(resolve, reject) {
        setTimeout(function() {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": "广东", "2": "北京", "3": "浙江" } };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[provice_id]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });
    return p;
}

// 根据城市id获取城市信息
function getCity(city_id) {
    // new一个Promise对象
    let p = new Promise(function(resolve, reject) {
        setTimeout(function() {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "深圳", "provice_id": 1 }, "2": { "name": "北京", "provice_id": 2 }, "3": { "name": "杭州", "provice_id": 3 } } };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[city_id]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });
    return p;
}

// 根据公司id获取公司信息
function getCompany(company_id) {
    // new一个Promise对象
    let p = new Promise(function(resolve, reject) {
        setTimeout(function() {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "腾讯", "city_id": 1 }, "2": { "name": "字节跳动", "city_id": 2 }, "3": { "name": "阿里巴巴", "city_id": 3 } } };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[company_id]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });
    return p;
}

// 根据用户id获取用户信息
function getUserInfo(uid) {
    // new一个Promise对象
    let p = new Promise(function(resolve, reject) {
        // 把之前回调函数中的代码整个放进来
        setTimeout(function() {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "John", "age": 25, "gender": "男", "company_id": 1 }, "2": { "name": "Thomas", "age": 22, "gender": "男", "company_id": 2 }, "3": { "name": "Lily", "age": 18, "gender": "女", "company_id": 3 } } };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[uid]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });

    // 返回Promise对象
    return p;
}

let uid = 1;

// 链式调用,因为getUserInfo()返回的是一个promise对象,所以可以调用.then()方法,而由于第一个then()
// 里return getCompany返回的也是一个promise对象,所以又可以在后面继续调用.then()方法,以此类推,一直到
// 最后一个catch(),catch()是用来接收错误的,无论前面有几个then(),只要有错误最终都会一级一级的往下传,最后传到catch()方法中
getUserInfo(uid).then(function(userInfo) {
    // 获取用户信息成功
    console.log(userInfo);
    // 根据用户信息中的公司id,获取公司信息
    return getCompany(userInfo.company_id);
}).then(function(companyInfo) {
    // 获取公司信息成功
    console.log(companyInfo);
    // 根据公司信息中的城市id,获取城市信息
    return getCity(companyInfo.city_id);
}).then(function(cityInfo) {
    // 获取城市信息成功
    console.log(cityInfo);
    // 根据城市信息中的省份id,获取省份信息
    return getProvice(cityInfo.provice_id);
}).then(function(provinceInfo) {
    // 获取省份信息成功
    console.log(provinceInfo);
}).catch(function(msg) {
    // 捕获到错误信息,无论是前面哪个then()函数的错误,都会传到这里
    console.log(msg);
}).finally(function() {
    // finally是最终的意思,无论前面是正常执行成功还是有错误走catch了,只要前面全部执行完,
    // 最终都会走finally这里,主要是有时候我们可以需要在链式调用完之后,再继续做其它工作,
    // 比如关闭连接之类的,就可以在这里做,如果没有finally,那你就需要自己写一个函数,然后
    // 在最后一个then和catch里分别都调用一遍你的自定义函数,因为你不确定它最终是正常执行完成
    // 还是会报错,但是有finally就方便多了,因为无论最终是成功还是失败,都会走finally。
    console.log("finally");
});

可以看到,用Promise的方式,最终也成功取到了省份信息,但是变成了平级链式调用,而不会越嵌越深,也不需要每一级都设置一个失败回调函数,因为Promise失败后,由于每一级的then都没有定义失败的回调函数,所以错误会一级一级往下传,直到最终被catch()捕获。

另外整个链式调用,最终无论是成功还是失败,都会走finally()方法,如果你需要在整个链式调用完最终做点什么事儿,就可以在finally()里做,如果没有,也可以不写finally()方法。

js定义函数的三种方法

先来看定义函数的三种方法

// ============ 第一种,常规方法 ==========
// 可以在定义前调用函数,因为函数的解析永远是提前的,
// 调用语句就算写在前面,但它也是会在函数定义后才执行
// 因为程序需要先存入代码区,才能开始从上到下执行
// 而存入代码区后,函数就已经存在了,所以即使函数调用
// 在函数定义的前面,也不会影响调用
test1()

function test1() {
    console.log('Hello world1!');
}

// ========= 第二种,匿名函数 =========
// 把整个函数赋值给变量,用变量去调用函数
let test2 = function() {
    console.log('Hello world2!');
}

// 这种方式定义的函数,调用必须在定义之后,因为变量肯定要先定义了才能调用
test2()

// ========= 第三种,自启动函数 ============
// 就是不需要调用,自己就能直接执行,它有两种写法
// 这种写法比较正规,后面的括号用于向里面的函数传参
// 这里str是实参,words是形参
let str = 'Hello world3-1!';
(function(words) {
    console.log(words);
})(str)

// 这种写法在node18下执行会报错,但却能正常执行里面的打印语句
// 不过在浏览器中能正常执行,个人感觉这种写法方法可能不是正规写法
(function() {
    console.log('Hello world3-2!');
}())

箭头函数

箭头函数是ES6中一种把匿名函数简写的方法,即js定义函数的三种方法中方法二的简化写法。

简写方法1:把function关键字和函数名去年,在圆括号和花括号之间加个=>箭头符号,但由于这样是匿名函数,所以需要定义一个常量来保存这个匿名函数,否则你就无法调用它

// 原始函数写法
function say() {
    console.log("Hello World");
}

// 省掉function关键字,只需要在括号和函数体语句之间加一个箭头=>
// 然后把整个函数赋值给一个常量,所以用const定义,当然你用var或let也是可以的,
// 只不过因为函数定义之后就是这个名称,我们不希望像变量一样再去改变它的值,而且
// 用var还会导致同名覆盖问题,比如代码太长你不知道前面定义过一个函数名,此时你
// 在后面var定义同名的变量,就会覆盖前面的值就会被后面的覆盖,当然let不会有这
// 个问题,因为let定义过的,如果你再次定义,它就会报错,但通常我们都用const比较多
const say2 = () => {
    console.log("Hello World");
}

say();
say2();

简写方法2:如果函数体只有一行,并且这一行是对函数或方法的调用,那花括号都可以省略掉

function english() {
    console.log("I can speak English.");
}

// 函数中调用另一个函数:原始函数写法
function say() {
    english();
}

// 函数中调用另一个函数:箭头函数写法
// 省掉function关键字,由于函数体只有一句,且这句是对函数的调用,
// 所以可以省掉函数体的花括号,只需要在括号和函数体语句之间加一个箭头=>
const say2 = () => english();

say();
say2();

简写方法3:如果函数体只有一句return语句,可省略return关键字,只写return后面的部分(返回部分可以是一个对函数的调用,也可以是变量,常量等等),这样就不用写花括号,如果不省略return,则必须带花括号,否则报错

// 原始函数写法
function say() {
    return "I can speak English.";
}

// 省掉function关键字,由于函数体只有一句,且这句是return,在箭头函数的写法中,
// 可以省略return关键字以及花括号,只需要在括号和函数体语句之间加一个箭头=>就行
const say2 = () => "I can speak English.";

console.log(say());
console.log(say2());

简写方法4:当只有一个参数的时候,可省略参数圆括号,但两个参数及以上,或没有参数时,就不能省略圆括号了

function english(words) {
    console.log(words);
}

// 原始函数写法
function say(words) {
    english(words);
}

// 只有一个参数,可省略圆括号,函数体只有一句
// 且这句是调用一个函数,可省略函数体的花括号
const say2 = words => english(words);

// 只有一个参数,可省略圆括号,函数体只有一句
// 且这句是调用一个方法,花括号可省略
const say3 = words => console.log(words);;

say("I can speak English.");
say2("I can speak English.");
say3("I can speak English.");

链式调用使用箭头函数

这里我是把前面增加获取城市和省份信息中的整个例子粘贴过来了,只不过里面所有的函数/方法改成了箭头函数,这样更简洁

// 根据省份id获取省份名称
// 注意,由于北京是直辖市,直辖市属于省级,为了统一
// “省-市-县/区”格式,一般会写成“北京-北京-朝阳区”
const getProvice = provice_id => {
    // new一个Promise对象
    let p = new Promise((resolve, reject) => {
        setTimeout(() => {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": "广东", "2": "北京", "3": "浙江" } };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[provice_id]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });
    return p;
}

// 根据城市id获取城市信息
const getCity = city_id => {
    // new一个Promise对象
    let p = new Promise((resolve, reject) => {
        setTimeout(() => {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "深圳", "provice_id": 1 }, "2": { "name": "北京", "provice_id": 2 }, "3": { "name": "杭州", "provice_id": 3 } } };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[city_id]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });
    return p;
}

// 根据公司id获取公司信息
const getCompany = company_id => {
    // new一个Promise对象
    let p = new Promise((resolve, reject) => {
        setTimeout(() => {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "腾讯", "city_id": 1 }, "2": { "name": "字节跳动", "city_id": 2 }, "3": { "name": "阿里巴巴", "city_id": 3 } } };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[company_id]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });
    return p;
}

// 根据用户id获取用户信息
const getUserInfo = uid => {
    // new一个Promise对象
    let p = new Promise((resolve, reject) => {
        // 把之前回调函数中的代码整个放进来
        setTimeout(() => {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "John", "age": 25, "gender": "男", "company_id": 1 }, "2": { "name": "Thomas", "age": 22, "gender": "男", "company_id": 2 }, "3": { "name": "Lily", "age": 18, "gender": "女", "company_id": 3 } } };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[uid]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });

    // 返回Promise对象
    return p;
}

let uid = 1;

// 链式调用,因为getUserInfo()返回的是一个promise对象,所以可以调用.then()方法,而由于第一个then()
// 里return getCompany返回的也是一个promise对象,所以又可以在后面继续调用.then()方法,以此类推,一直到
// 最后一个catch(),catch()是用来接收错误的,无论前面有几个then(),只要有错误最终都会一级一级的往下传,最后传到catch()方法中
getUserInfo(uid)
    .then(userInfo => {
        console.log(userInfo);
        return getCompany(userInfo.company_id)
    })
    .then(companyInfo => {
        console.log(companyInfo);
        return getCity(companyInfo.city_id)
    })
    .then(cityInfo => {
        console.log(cityInfo);
        return getProvice(cityInfo.provice_id)
    })
    // 由于函数体只有一句,且这句是调用方法,所以可以省略花括号
    .then(provinceInfo => console.log(provinceInfo))
    .catch(msg => {
        // 捕获到错误信息,无论是前面哪个then()函数的错误,都会传到这里
        console.log(msg);
    }).finally(() => {
        // finally是最终的意思,无论前面是正常执行成功还是有错误走catch了,只要前面全部执行完,
        // 最终都会走finally这里,主要是有时候我们可以需要在链式调用完之后,再继续做其它工作,
        // 比如关闭连接之类的,就可以在这里做,如果没有finally,那你就需要自己写一个函数,然后
        // 在最后一个then和catch里分别都调用一遍你的自定义函数,因为你不确定它最终是正常执行完成
        // 还是会报错,但是有finally就方便多了,因为无论最终是成功还是失败,都会走finally。
        console.log("这里可以执行任何你想要在整个链式调用完之后执行的代码");
    });

Promise的三种状态

Promise有三种状态:

  • 1、pending,等待状态,new了Promise对象之后的默认状态;
  • 2、fulfilled,已成功,调用resolve()方法后状态会变成fullfilled;
  • 3、rejected,已失败,调用rejected()方法后状态会变成rejected。

Promise的两个回调函数,也叫Promise对象的方法吧,如下,放在第一个位置的,叫resolve方法,放在第二个位置的叫reject方法,我这里是故意写成callback1和callback2的,因为它是形参,形参的名字并不是它实际的名字,只需要调用的时候也用同样的名字调用就好,你完全可以把callback1写成abc,把callback2写成def,然后调用的时候就用abc()或def()这样调用就行

new Promise(function(callback1, callback2) {

});

当然事实上一般会写成形参所在的位置具有的意义,比如第一个参数本身就代表resolve,所以一般会把第一个参数写成resolve,同理,第二个参数是reject,所以会把它写成reject,而且都会用箭头函数来写

new Promise((resolve, reject) => {

});

在任意一个网页下打开网页控制台,执行上边的new Promise代码(其实就是new了一个Promise对象),点开折叠箭头,就可以看到在在promise对象的原型(Prototype)→constructor下边,就有该对象能调用的方法,其中里面就有reject()和resolve()两个方法

注意:对于类(class)来说,new一个类创建一个对象,那么对象能调用的方法就是类中的方法,而对于js这种基于原型对象的语言来说,创建一个对象是通过new一个原型对象来创建的,所以该对象能调用的方法,自然就是在它的原型对象中,而且是在它的原型对象中的构造函数中,因为对象(包括原型对象)本身并不是作为一个对象出现的,而是作为一个函数出现的,你可以认为一个对象就是它自己的构造函数(有点怪,但确实就是这样)。

比如Promise对象,你说它是对象,没错,它确实是对象,你new它就可以创建一个新对象,可是同时它又是一个构造函数,你用typeof Promise检测,它就是一个function类型,你直接执行Promise()也可以创建一个对象,都不需要new,这个是js的一个非常奇特的地方,我也不是非常的懂,但从这一点可以理解为什么原型对象的方法都在它的构造函数中,因为构造函数就是对象自己(这个表述可能有点不准确,你可以自己理解一下)。

而且并不只是Promise对象是这样,js中所有对象都是这样的,比如js中所有对象的最终原型对象就是Object对象,你用typeof Object检测,它也是function,就是说它是对象,但同时它也是该对象的构造函数,你用Object()就能创建一个新对象,跟new Object()是一样的。

Promise的基本用法

这里再写一次Promise的基本用法,解释了resolve和reject其实就是两个形参,而then中的两个函数就是实参

let promise = new Promise((resolve, reject) => {
    // 调用接口返回成功
    if (1) {
        resolve(value);
    } else {
        // 调用接口返回失败
        reject(error);
    }
});

// then中有两个方法,第一个是成功回调,第二个是失败回调
// 只要Promise中执行resolve()方法,就会调用then中的第一个方法
// 只要Promise中执行rejct()方法,就会调用then中的第二个方法

// 换句话说,其实then方法中的两个函数就是实参,而Promise中的resolve和reject就是形参
promise.then(value => {
    // 成功回调
}, error => {
    // 失败回调
})

// 当然我说了,Promise中的两个回调函数它只是一个形参,形参的名称并不是非得叫
// resolve和reject,只是为了方便我们知道它有什么作用,才叫resolve和reject。
// 比如我完全可以写成aaa和bbb,只要你在调用的时候使用aaa()和bbb()调用就行
let promise2 = new Promise((aaa, bbb) => {
    // 假设调用接口返回成功
    if (1) {
        // 这里我直接定义了value的值,事实上这个value的值是从接口返回的数据,并且大多数时候
        // 是一个json,当然也不是非得把整个json都传给aaa(),而是看你自己需要用到什么就传什么
        let value = 1
        aaa(value);
    } else {
        // 这里我直接定义了err的值,事实上这个err的值是从接口返回的数据,并且大多数时候
        // 是一个json,当然也不是非得把整个json都传给bbb(),而是看你自己需要用到什么就传什么
        let err = new Error('error');
        // 调用接口返回失败
        bbb(error);
    }
});

Promise.all的用法

假设我有两个不相关的Promise,分别执行,如下所示

const setDelayMillisec = (millisecond) => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(`我是setDelay,我延迟了${millisecond}毫秒后输出的`)
        }, millisecond)
    })
}

const setDelaySec = (seconds) => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(`我是setDelaySecond,我延迟了${seconds}秒后输出的`);
        }, seconds * 1000)
    })
}

// 执行第一个Promise
setDelayMillisec(1000)
    .then(res => console.log(res))
    .catch(err => console.log(err));

// 执行第二个Promise
setDelaySec(2)
    .then(res => console.log(res))
    .catch(err => console.log(err));

但是如果用Promise.all就可以并行执行它们,并且只需要一个catch

const setDelayMillisec = (millisecond) => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(`我是setDelay,我延迟了${millisecond}毫秒后输出的`)
        }, millisecond)
    })
}

const setDelaySec = (seconds) => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(`我是setDelaySecond,我延迟了${seconds}秒后输出的`);
        }, seconds * 1000)
    })
}


// 并行执行多个Promise,只需要把多个函数放在数组里,
// 然后把整个数组作为参数传到Promise.all()中
Promise.all([setDelayMillisec(1000), setDelaySec(1)])
    .then(res => console.log(res))
    .catch(err => console.log(err));

async与await

async与await的出现,是为了解决Promise链中then()存在的两个问题的:

  • 1、一连串的.then().then().then().then()的过于复杂的问题;
  • 2、中断的问题,就是执行到某个then(),根据返回的值,可能我并不需要执行后面的then()了;

链式调用过于复杂问题

如果你前面有认真看下来,应该知道最终写法是链式调用使用箭头函数吧,现在我们再把那段链式调用粘贴到这里

getUserInfo(uid)
    .then(userInfo => {
        console.log(userInfo);
        return getCompany(userInfo.company_id)
    })
    .then(companyInfo => {
        console.log(companyInfo);
        return getCity(companyInfo.city_id)
    })
    .then(cityInfo => {
        console.log(cityInfo);
        return getProvice(cityInfo.provice_id)
    })
    // 由于函数体只有一句,且这句是调用方法,所以可以省略花括号
    .then(provinceInfo => console.log(provinceInfo))
    .catch(msg => {
        // 捕获到错误信息,无论是前面哪个then()函数的错误,都会传到这里
        console.log(msg);
    }).finally(() => {
        console.log("这里可以执行任何你想要在整个链式调用完之后执行的代码");
    });

看完了之后,你是不是发现,虽然链式调用把“无限内嵌回调函数”变成了平级的链式调用,但一连串的.then(),看着还是挺多挺复杂的,那有没有更简单的解决方法呢?肯定是有的,为了解决这个问题,ES7引入了async/await两个关键字。

注意,既然async/await其中一个用途是用来解决一连串的.then()问题的,那就说明async/await并不是替代Promise的,它们只是用来替代Promise中无数的.then()而已。

链式调用不方便跳出或停止

还是链式调用使用箭头函数中的这段链式调用,在第三个then()中获取到城市之后,如果城市是北京,那我可能就不需要再去获取省份了,因为北京没有上级省份,它是直辖市,所以我想停止执行后面那个then(),怎么停止?

getUserInfo(uid)
    .then(userInfo => {
        console.log(userInfo);
        return getCompany(userInfo.company_id)
    })
    .then(companyInfo => {
        console.log(companyInfo);
        return getCity(companyInfo.city_id)
    })
    .then(cityInfo => {
        console.log(cityInfo);
        // 这里我不想获取省份信息了,因为北京没有省,所以
        // 我想直接在这里停止不要再执行下边的那个then()了
        // 这里要怎么写才能在这里跳出,不要再执行后面的then()呢?
    })
    // 由于函数体只有一句,且这句是调用方法,所以可以省略花括号
    .then(provinceInfo => console.log(provinceInfo))
    .catch(msg => {
        // 捕获到错误信息,无论是前面哪个then()函数的错误,都会传到这里
        console.log(msg);
    }).finally(() => {
        console.log("这里可以执行任何你想要在整个链式调用完之后执行的代码");
    });

其实并没有一个类似break能在for循环中跳出循环的语句来让then()停止,要想让then()停止,有两种方法:

  • 1、返回一个reject,因为reject代表失败了,这个失败状态将会被catch捕获,换句话说,它将会跳过后面所有的then(),直达catch();
  • 2、返回一个pending状态的Promise对象,因为只有resolve才会往下执行后面的then(),reject会直接跳到catch,而pending状态两个都不会做,而是就在那不动。

这两种方法我都在链式调用在中间跳出的实例中写了实例。

链式调用在中间跳出的实例

链式调用在中间跳出或停止的实例,其实就是从链式调用使用箭头函数中把代码粘贴过来然后改了一下第三个then()和catch()

// 根据省份id获取省份名称
// 注意,由于北京是直辖市,直辖市属于省级,为了统一
// “省-市-县/区”格式,一般会写成“北京-北京-朝阳区”
const getProvice = provice_id => {
    // new一个Promise对象
    let p = new Promise((resolve, reject) => {
        setTimeout(() => {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": "广东", "2": "北京", "3": "浙江" } };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[provice_id]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });
    return p;
}

// 根据城市id获取城市信息
const getCity = city_id => {
    // new一个Promise对象
    let p = new Promise((resolve, reject) => {
        setTimeout(() => {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "深圳", "provice_id": 1 }, "2": { "name": "北京", "provice_id": 2 }, "3": { "name": "杭州", "provice_id": 3 } } };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[city_id]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });
    return p;
}

// 根据公司id获取公司信息
const getCompany = company_id => {
    // new一个Promise对象
    let p = new Promise((resolve, reject) => {
        setTimeout(() => {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "腾讯", "city_id": 1 }, "2": { "name": "字节跳动", "city_id": 2 }, "3": { "name": "阿里巴巴", "city_id": 3 } } };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[company_id]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });
    return p;
}

// 根据用户id获取用户信息
const getUserInfo = uid => {
    // new一个Promise对象
    let p = new Promise((resolve, reject) => {
        // 把之前回调函数中的代码整个放进来
        setTimeout(() => {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "John", "age": 25, "gender": "男", "company_id": 1 }, "2": { "name": "Thomas", "age": 22, "gender": "男", "company_id": 2 }, "3": { "name": "Lily", "age": 18, "gender": "女", "company_id": 3 } } };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[uid]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });

    // 返回Promise对象
    return p;
}

let uid = 1;

// 链式调用,因为getUserInfo()返回的是一个promise对象,所以可以调用.then()方法,而由于第一个then()
// 里return getCompany返回的也是一个promise对象,所以又可以在后面继续调用.then()方法,以此类推,一直到
// 最后一个catch(),catch()是用来接收错误的,无论前面有几个then(),只要有错误最终都会一级一级的往下传,最后传到catch()方法中
getUserInfo(uid)
    .then(userInfo => {
        console.log('用户信息:', userInfo);
        return getCompany(userInfo.company_id)
    })
    .then(companyInfo => {
        console.log('公司信息:', companyInfo);
        return getCity(companyInfo.city_id)
    })
    .then(cityInfo => {
        console.log('城市信息:', cityInfo);
        // 跳出方式1:返回Promise.reject(),用于主动跳出then()链
        // return Promise.reject({ isNotErrorExpection: true, msg: "我要跳出Promise链" });
        // 跳出方式2:返回一个Promise对象,因为Promise对象默认是pending状态,
        // 它是不会往下执行的,除非你调用了resolve或reject改变了它的状态
        return new Promise(() => '后续的不会再执行了,但其实也没有跳出Promise链,就只是停在这儿了')
    })
    // 由于函数体只有一句,且这句是调用方法,所以可以省略花括号
    .then(provinceInfo => console.log(provinceInfo))
    .catch(err => {
        // 捕获到错误信息,无论是前面哪个then()函数的错误,都会传到这里
        if (err.isNotErrorExpection != undefined && err.isNotErrorExpection == true) {
            console.log('我是主动跳出Promise链的:', err.msg);
        } else {
            console.log('我是真的报错了:', err);
        }
    }).finally(() => {
        // finally是最终的意思,无论前面是正常执行成功还是有错误走catch了,只要前面全部执行完,
        // 最终都会走finally这里,主要是有时候我们可以需要在链式调用完之后,再继续做其它工作,
        // 比如关闭连接之类的,就可以在这里做,如果没有finally,那你就需要自己写一个函数,然后
        // 在最后一个then和catch里分别都调用一遍你的自定义函数,因为你不确定它最终是正常执行完成
        // 还是会报错,但是有finally就方便多了,因为无论最终是成功还是失败,都会走finally。
        console.log("这里可以执行任何你想要在整个链式调用完之后执行的代码");
    });

复制我的代码去执行一下试试,你会发现真的是在第三个then就跳出或停止了,第四个then没有执行,跳出方式1和跳出方式2两个都是return,所以试的时候,你需要注释掉其中一个。

这样看起来是挺好的,但其实还是存在问题的:

  • 1、方式1为了区分是真的出错了,还是我主动跳出,我需要在主动跳出的时候添加一个状态(返回一个对象,对象里有一个属性表示是否真的报错),而且这是理想状态,因为有时候catch并非在最后,而是可能插在多个then()的中间,那样的话,这种方式会更麻烦;
  • 2、方式2为了让Promise链停止,new了一个空的Promise对象,但事实上这有可能会导致内存泄漏,因为Promise没有resove和reject来改变它的状态,它将一直保持在内存中,一直处于pending(等待改变状态)的状态,所以这并不是一个好的方案。

关于如何跳出或停止Promise链的问题,这篇文章讲的非常详细:从如何停掉Promise链说起,但是靠在then()链中处理这个问题,还是非常的不方便,而使用async/await就非常简单方便。

async/await替换then()举例

以下例子是我从链式调用使用箭头函数中复制下来的,只不过我把一连串的.then改成了async/await方式,你可以直接复制我的代码去运行测试

// 根据省份id获取省份名称
// 注意,由于北京是直辖市,直辖市属于省级,为了统一
// “省-市-县/区”格式,一般会写成“北京-北京-朝阳区”
const getProvice = (provice_id) => {
    // new一个Promise对象
    let p = new Promise((resolve, reject) => {
        setTimeout(() => {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": "广东", "2": "北京", "3": "浙江" } };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[provice_id]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });
    return p;
}

// 根据城市id获取城市信息
const getCity = (city_id) => {
    // new一个Promise对象
    let p = new Promise((resolve, reject) => {
        setTimeout(() => {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "深圳", "provice_id": 1 }, "2": { "name": "北京", "provice_id": 2 }, "3": { "name": "杭州", "provice_id": 3 } } };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[city_id]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });
    return p;
}

// 根据公司id获取公司信息
const getCompany = (company_id) => {
    // new一个Promise对象
    let p = new Promise((resolve, reject) => {
        setTimeout(() => {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "腾讯", "city_id": 1 }, "2": { "name": "字节跳动", "city_id": 2 }, "3": { "name": "阿里巴巴", "city_id": 3 } } };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[company_id]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });
    return p;
}

// 根据用户id获取用户信息
const getUserInfo = (uid) => {
    // new一个Promise对象
    let p = new Promise((resolve, reject) => {
        // 把之前回调函数中的代码整个放进来
        setTimeout(() => {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "John", "age": 25, "gender": "男", "company_id": 1 }, "2": { "name": "Thomas", "age": 22, "gender": "男", "company_id": 2 }, "3": { "name": "Lily", "age": 18, "gender": "女", "company_id": 3 } } };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[uid]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });

    // 返回Promise对象
    return p;
}

// 定义一个async函数,注意async并不是函数名,只不过箭头函数本来就没有函数名,如果不写async,
// 它是只有一个括号在那儿的,async只是把这个函数定义为“异步函数”(async是asynchronous的简写),
// 所以千万别认为async()是一个名为“async”的函数,这样就大错特错了
const container = async() => {
    let uid = 1;
    // 在调用的函数前面加一个await关键字,表示等待数据返回后再往下执行,这样console.log
    // 打印出来的就不是undefined,也不是Promise对象,而是getUserInfo()中Promise里的
    // resolve函数返回的数据。但必须要注意,await关键字只能在async修饰的函数内部添加,
    // 并且await后面那个函数,必须是返回Promise对象的函数,如果函数里没有返回Promise对象,
    // 虽然不会报错,但await不会起到任何作用,相当于没写await。后面的getCompany、getCity、
    // getProvice添加了await都与getUserInfo一样原理
    const userInfo = await getUserInfo(uid);
    console.log(userInfo);
    const companyInfo = await getCompany(userInfo.company_id);
    console.log(companyInfo);
    const cityInfo = await getCity(companyInfo.city_id);
    console.log(cityInfo);
    const proviceInfo = await getProvice(cityInfo.provice_id);
    console.log(proviceInfo);
}

// 然后单独调用.catch()来捕获错误(后面有解释,请按顺序往下看)
container().catch(err => console.log(err));

看了之后,是不是觉得,用async/await方式,比一连串的.then().then()简洁多了?


另外也可以用try catch捕获错误:
就是在函数体内,把整个函数体的语句都放到try中,然后就可以通过catch来统一接收报错。由于这样就不需要在外部调用.catch,所以我直接把这个函数写成自执行函数,就是(function(){})()这种形式,只不过是用箭头函数,并且添加了async声明为异步函数而已

// 定义一个async函数,注意async并不是函数名,它只是把这个函数定义为“异步函数”,async是asynchronous的简写
(async() => {
    let uid = 1;
    try {
        const userInfo = await getUserInfo(uid);
        console.log(userInfo);
        const companyInfo = await getCompany(userInfo.company_id);
        console.log(companyInfo);
        const cityInfo = await getCity(companyInfo.city_id);
        console.log(cityInfo);
        const proviceInfo = await getProvice(cityInfo.provice_id);
        console.log(proviceInfo);
    } catch (err) {
        // 统一在这里捕获错误
        console.log(err);
    }
})()

也可以单独捕获错误:
当然有可能你想单独捕获某个函数返回的err,那你可以这样写

// 定义一个async函数,注意async并不是函数名,它只是把这个函数定义为“异步函数”,async是asynchronous的简写
(async() => {
    let uid = 1;
    // 在调用的函数前面加一个await关键字,表示等待数据返回后再往下执行,这样console.log
    // 打印出来的就不是undefined,也不是Promise对象,而是getUserInfo()中Promise里的
    // resolve函数返回的数据。但必须要注意,await关键字只能在async修饰的函数内部添加,
    // 并且await后面那个函数,必须是返回Promise对象的函数,如果函数里没有返回Promise对象,
    // 虽然不会报错,但await不会起到任何作用,相当于没写await。后面的getCompany、getCity、
    // getProvice添加了await都与getUserInfo一样原理
    const userInfo = await getUserInfo(uid).catch(err => console.log(err));
    console.log(userInfo);
    const companyInfo = await getCompany(userInfo.company_id).catch(err => console.log(err));
    console.log(companyInfo);
    const cityInfo = await getCity(companyInfo.city_id).catch(err => console.log(err));
    console.log(cityInfo);
    const proviceInfo = await getProvice(cityInfo.provice_id).catch(err => console.log(err));
    console.log(proviceInfo);
})()

async详解

前面async/await替换then()举例中用例子说明了使用async/await比一连串的then()方便很多,但并没有详细说明async/await具体的作用和应该注意的地方,这里就来讲一下。

async用于写在一个函数前面来声明这个函数是“异步函数”,即这个函数执行了不能马上返回结果,而是需要一定的时间

// async写在普通函数前,声明该函数为“异步函数”
async function process() {

}

// async写在匿名箭头函数前,声明该函数为“异步函数”
const process = async() => {

}

// async写在自执行函数前,声明该函数为“异步函数”
(async()=>{

})()

// async一定要写在函数前面,即使是匿名函数,
// 也要直接写在匿名函数前,不能写在定义变量前
// 所以以下两种写法都是错误的,会报错
async const process = () => {

}
const async process = () => {

}

使用async声明的函数,本质上会返回一个Promise对象,如果你没有写return语句,那么它默认会返回一个Promise.resolve(undefined)对象。

以下例子需要在浏览器控制台执行才能看出来它本质是返回一个Promise对象,在html里或用node执行是看不出来的

// async写在匿名箭头函数前,声明该函数为“异步函数”
const process = async() => {
    return '随便返回试试';
}
process();

如下所示,我随便返回了一句话,打印出来就是Promise fullfilled状态(已完成)

如果我不返回,那就相当于返回了Promise.resolve(undefined)

手动返回Promise.resolve(undefined),发现跟上边返回的是一样的,这就证明了它默认是返回一个Promise.resolve(undefined)

await详解

await这个单词的意思是“等待,期待”,等待什么呢?等待一个Promise返回结果,相当于把异步变成同步了。前面async/await替换then()举例已经演示了一遍用法了,如果忘了,可以回去看一下,然后再来看这边的解释。

await要遵循的两个条件:

  • 1、await必须在async直接声明的函数里面使用,否则报错。“直接声明”是指你不能在async声明的函数中再套另一个函数,然后在套的那个函数里用await,这样是不行的,会报错;
  • 2、await只有放在Promise对象前才能体现它的作用(否则虽然不报错,但是不会起作用)。

看一下以下例子,await用于放在一个Promise对象前,用于表示等待该Promise对象中resolve回调函数返回的结果(注意不能是reject,后面会解释),也就是说,resolve()函数中的“我延迟了一秒”这句话,本来需要用then()来获取的,但是现在它会直接返回给result变量,这个效果就是因为new Promise前面加了个await

const demo = async() => {
        let result = await new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve('我延迟了一秒')
            }, 1000)
        });
        console.log(result);
        console.log('只有上面的Promise返回结果了,程序才会往下执行我');
    }
demo();

// 最后输出结果:
// 我延迟了一秒
// 只有上面的Promise返回结果了,程序才会往下执行我

并且由于外边对demo()的调用仅仅是调用它,并不需要从demo中获得返回值,所以我们也可以写成匿名自执行函数,这样就不需要在外边单独调用一下demo()了

(async() => {
    let result = await new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('我延迟了一秒')
        }, 1000)
    });
    console.log(result);
    console.log('我由于上面的程序还没执行完,先不执行“等待一会”');
})()

当然,一般我们会把Promise放在一个函数里,拿前面一直举例的例子来说,如下所示,你可以复制我的代码去执行,你会发现,getUserInfo函数本来是延迟返回的,加了await之后它会“等待”数据返回后,才会继续往下执行,就像这个函数不是异步函数一样

const getUserInfo = uid => {
    // new一个Promise对象
    let p = new Promise(function(resolve, reject) {
        // 把之前回调函数中的代码整个放进来
        setTimeout(() => {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "John", "age": 25, "gender": "男", "company_id": 1 }, "2": { "name": "Thomas", "age": 22, "gender": "男", "company_id": 2 }, "3": { "name": "Lily", "age": 18, "gender": "女", "company_id": 3 } } };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[uid]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });

    // 返回Promise对象
    return p;
}

(async() => {
    // 获取用户信息
    let userInfo = await getUserInfo(1);
    console.log(userInfo);
    console.log('看看是我先输出还是用户信息先输出');
})()

// 输出结果如下:
// { name: 'John', age: 25, gender: '男', company_id: 1 }
// 看看是我先输出还是用户信息先输出

考虑失败的情况
以上例子只考虑了成功(resolve)的情况,如果失败了(即调用了reject)会发生什么情况?答案是…会报错!因为我们根本没处理失败的情况,await只会返回resolve返回的结果,所以失败的情况我们还要处理一下,有两种方法。

捕获错误方法一:在async修饰的函数内用try catch包住所有语句

const getUserInfo = uid => {
    // new一个Promise对象
    let p = new Promise(function(resolve, reject) {
        // 把之前回调函数中的代码整个放进来
        setTimeout(() => {
            // 改成失败
            let jsonData = { "code": "10002", "msg": "获取失败" };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[uid]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });

    // 返回Promise对象
    return p;
}

(async() => {
    try {
        // 获取用户信息
        let userInfo = await getUserInfo(1);
        console.log(userInfo);
        // 由于失败后会直接跳到catch中,所以这句是不会输出的
        console.log('看看是我先输出还是用户信息先输出');
    } catch (err) {
        console.log("这里是错误信息");
        console.log(err);
    }
})()

// 输出结果如下:
// 这里是错误信息
// 获取失败

捕获错误方法二:在async修饰的函数外部用catch捕获

const getUserInfo = uid => {
    // new一个Promise对象
    let p = new Promise(function(resolve, reject) {
        // 把之前回调函数中的代码整个放进来
        setTimeout(() => {
            // 改成失败
            let jsonData = { "code": "10002", "msg": "获取失败" };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[uid]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });

    // 返回Promise对象
    return p;
}

const container = async() => {
    // 获取用户信息
    let userInfo = await getUserInfo(1);
    console.log(userInfo);
    // 由于失败后会直接跳到catch中,所以这句是不会输出的
    console.log('看看是我先输出还是用户信息先输出');
}

// 还记得前面说过async修饰的函数会返回一个Promise对象吗?
// 正是因为如此,container()才能调用.catch()方法来捕获错误
container().catch(err => {
    console.log(err)
});

// 输出结果如下:
// 这里是错误信息
// 获取失败

async修饰的函数也是可以用then()来接收它的返回值的,因为async修饰的函数本质上是返回一个Promise对象(见async详解),而Promise就可以调用.then()方法来获取结果

const getUserInfo = uid => {
    // new一个Promise对象
    let p = new Promise(function(resolve, reject) {
        // 把之前回调函数中的代码整个放进来
        setTimeout(() => {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "John", "age": 25, "gender": "男", "company_id": 1 }, "2": { "name": "Thomas", "age": 22, "gender": "男", "company_id": 2 }, "3": { "name": "Lily", "age": 18, "gender": "女", "company_id": 3 } } };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[uid]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });

    // 返回Promise对象
    return p;
}

const container = async() => {
    // 获取用户信息
    let userInfo = await getUserInfo(1);
    return userInfo;
}

// 由于async返回的是一个Promise对象,只要我们在async修改的函数中返回
// 一个结果,那么我们还是可以用then()来接收这个返回的,这又回到了之前
// 那种then()了,当然具体要怎么用,还得看具体情况,选择合适的方法用就好
container().then(res => {
    console.log(res);
}).catch(err => {
    console.log(err)
});

// 输出结果如下:
// { name: 'John', age: 25, gender: '男', company_id: 1 }

await可以放在非Promise对象前(函数/变量前都可以),虽然不会报错,但不会起任何作用

(async() => {
    await setTimeout(() => {
        console.log('我会延迟一秒输出')
    }, 1000)
    console.log('我由于上面的程序还没执行完,先不执行“等待一会”');
})()

并且编辑器还会提示:await对这个类型的表达式没有效果

async/await跳出Promise链

在async/await中跳出Promise链,只需要return就可以了,复制以下代码执行测试一下

// 根据省份id获取省份名称
// 注意,由于北京是直辖市,直辖市属于省级,为了统一
// “省-市-县/区”格式,一般会写成“北京-北京-朝阳区”
const getProvice = (provice_id) => {
    // new一个Promise对象
    let p = new Promise((resolve, reject) => {
        setTimeout(() => {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": "广东", "2": "北京", "3": "浙江" } };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[provice_id]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });
    return p;
}

// 根据城市id获取城市信息
const getCity = (city_id) => {
    // new一个Promise对象
    let p = new Promise((resolve, reject) => {
        setTimeout(() => {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "深圳", "provice_id": 1 }, "2": { "name": "北京", "provice_id": 2 }, "3": { "name": "杭州", "provice_id": 3 } } };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[city_id]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });
    return p;
}

// 根据公司id获取公司信息
const getCompany = (company_id) => {
    // new一个Promise对象
    let p = new Promise((resolve, reject) => {
        setTimeout(() => {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "腾讯", "city_id": 1 }, "2": { "name": "字节跳动", "city_id": 2 }, "3": { "name": "阿里巴巴", "city_id": 3 } } };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[company_id]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });
    return p;
}

// 根据用户id获取用户信息
const getUserInfo = (uid) => {
    // new一个Promise对象
    let p = new Promise((resolve, reject) => {
        // 把之前回调函数中的代码整个放进来
        setTimeout(() => {
            let jsonData = { "code": "10001", "msg": "获取成功", "data": { "1": { "name": "John", "age": 25, "gender": "男", "company_id": 1 }, "2": { "name": "Thomas", "age": 22, "gender": "男", "company_id": 2 }, "3": { "name": "Lily", "age": 18, "gender": "女", "company_id": 3 } } };
            // 获取成功
            if (jsonData.code == 10001) {
                return resolve(jsonData.data[uid]);
            } else {
                // 获取失败
                return reject(jsonData.msg);
            }
        }, 800);
    });

    // 返回Promise对象
    return p;
}

// 定义一个async函数,注意async并不是函数名,它只是把这个函数定义为“异步函数”,async是asynchronous的简写
const container = async() => {
    let uid = 2;
    // 在调用的函数前面加一个await关键字,表示等待数据返回后再往下执行,这样console.log
    // 打印出来的就不是undefined,也不是Promise对象,而是getUserInfo()中Promise里的
    // resolve函数返回的数据。但必须要注意,await关键字只能在async定义的函数内部添加
    // 并且await后面那个函数,必须是返回Promise对象的函数,如果函数里没有Promise对象
    // 后面的getCompany、getCity、getProvice添加了await都与getUserInfo一样原理
    const userInfo = await getUserInfo(uid);
    console.log(userInfo);
    const companyInfo = await getCompany(userInfo.company_id);
    console.log(companyInfo);
    const cityInfo = await getCity(companyInfo.city_id);
    console.log(cityInfo);
    if (cityInfo.name == "北京") {
        // 在这里return,可以停止Promise链,让它不再停下执行
        // 直接return,就相当于return Promise.resolve();

        // 比如:以下两句是一样的,只不过第一句是简写,
        // 你可以分别注释其中一个return来测试另一个
        // return '我要在这里停止,不再往下执行了';
        // return Promise.resolve('我要在这里停止,不再往下执行了');

        // 当然你也可以return reject,但是这样就不能简写,因为简写默认为resolve
        // 但是reject也只是会停止Promise链,它的输出并不会被后面的catch捕获,除非
        // 你在reject()函数中返回的是一个Error,比如:new Error('这是错误')
        // return Promise.reject('我要在这里停止,不再往下执行了');
        return Promise.reject(new Error('我要在这里停止,由于我是Error,所以会被catch捕获'));
    }
    const proviceInfo = await getProvice(cityInfo.provice_id);
    console.log(proviceInfo);
}

// 然后单独调用.catch()来捕获错误
container().catch(err => console.log(err));

其实我就是在if (cityInfo.name == "北京")中添加了四个return,你可以分别注释其中三个来测试停下的那个。


总结:使用async/await+Promise来做异步调用,既避免了回调函数的多层嵌套,又避免了Promise调用多个then()过于复杂的问题以及then()调用链不方便中途中断的问题,非常方便。

参考
看了就懂的Promise和回调函数
ECMAScript 6入门(第三版)
30分钟,让你彻底明白Promise原理
promise学习笔记
Promise() 构造器
异步Promise及Async/Await可能最完整入门攻略

打赏

订阅评论
提醒
guest

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

2 评论
内联反馈
查看所有评论
wjx
wjx
2 年 前

好,很清晰

2
0
希望看到您的想法,请您发表评论x

扫码在手机查看
iPhone请用自带相机扫
安卓用UC/QQ浏览器扫

一看就懂的Promise/async/await