为什么ES6新增了Promise对象来处理异步调用

2,748 阅读5分钟

异步调用是?:

首先我们得知道,什么是异步调用,而在前端中异步调用最常见的场景无非就是处理ajax的请求响应了。

const client = new XMLHttpRequest();
client.open("GET","/");

console.log(1);
client.onlaod = function () {
    console.log(2);
};
client.send();
console.log(3);

要知道js是单线程的,因此是按顺序执行该代码,理应打印的顺序是1,2,3才对,但是实际输出是1,3,2。可以看出ajax的响应处理被跳过了,放到了最后才被执行。

这就是异步调用了:需要等待才能得结果的程序不会堵塞当前线程的执行,当该程序有结果后才会触发当前线程去执行它的处理函数。

如何处理异步调用:

说到这我们可能又要引入一个新问题了,就是在程序中如何处理异步调用呢?

js中最常见的方法就是使用事件处理函数事件队列

在ajax响应之前,我们先注册事件处理函数(也叫回调函数):

client.onlaod = function () {
    console.log(2);
};

在当ajax响应完毕后,处理响应结果的事件处理函数就会放到事件队列中,等待当前线程去查找(轮询)并且执行它。

关于js的单线程和事件队列的具体说明,同学们可以看看阮大的博客

为什么使用Promise:

总算到正文了,处理异步调用看起来是不是很简单,只需注册相关的事件处理程序即可,那为啥又要搞出一个Promise类来呢?

const client = new XMLHttpRequest();
client.open("GET","/");
console.log(1);

client.onlaod = function () {
    console.log(2);
    
    const client2 = new XMLHttpRequest();
    client2.open("GET","/");
    client2.onlaod = function () {
        console.log(2.1);
    };
    client2.send();
};

client.send();
console.log(3);

有没有遇到过这种需求,一个请求完毕后才能发起另一个请求。这样的话另一个请求就要嵌套进前一个请求中了,一个两个还好,但想象下要是有四五个呢,那代码将会变得非常难看和难以维护(回调地狱)。而promise的出现正是要解决这个问题的。

Promise的原理和简单使用:

Promise本身是ES6的一个内置类(注意它的开头字母大写),而它的作用就是保存一个异步操作,并提供相关的api去获取其异步操作的结果和设置多个异步操作间的顺序(注意异步操作本身是无序的,只不过是Promise对象在内部实现了的嵌套和其他的相关方法使异步操作达到有序的现象)。

首先初始化一个Promise对象,在构造函数中传入我们涉及异步操作函数即可。(这里我们用setTImeOut来模拟异步调用,方便查看结果)。

const p1=new Promise(function (resolve) {
    console.log(1);
    setTimeout(function (n) {
        resolve(n);
    },3000,2);
    console.log(3);
});

可以看到,我们传入涉及异步操作的函数中还接收了一个函数参数,resolve

resolve函数会在异步操作成功时执行,并且将异步操作成功时的结果做为参数传入,并且返回。如:

setTimeout(function (n) {
    resolve(n);
},3000,2);

三秒后,setTimeOut的回调函数会执行resolve函数,传入resolve函数的参数就是2,并且,resolve函数会将2返回。

是不是很简单,就只需在原来的异步事件处理函数(回调函数)中调用resolve函数即可。并且传递其需要的结果做为变量。但是如何获取其异步的操作结果并进行进一步操作呢?

p1.then(function (res) {
    console.log(res);
});

是不是也很简单,只需调用Promise对象的then函数(Promise的API),然后传入一个处理函数,它的参数res就是resolve函数的返回值了(异步操作的结果)。

忍住,是不是觉得我欺骗了你们,写到这里,我都觉得奇怪了,为什么Promise这东西要写两次回调函数,更加麻烦了,第一次是在异步操作的事件处理函数中调用resolve函数,第二次是在then函数再次传入一个回调函数处理resolve的返回值。这简直了。

稳住,其实这也是有道理的,resolve的作用主要有两个:

第一个便是实现多个异步操作的有序进行,解决回调嵌套过度问题。

const p1=new Promise(function (resolve,reject) {
    setTimeout(function (n) {
        resolve(n);
    },3000,"p1");
});
const p2=new Promise(function (resolve,reject) {
    setTimeout(function () {
        console.log("p2");
        resolve(p1);
    },1000);
});

p2.then(function (resolve) {
    console.log(resolve);
});

看p2的resolve函数接收的是p1,当resolve函数接收一个Promise对象(p1)时,该resolve函数(p2的)会等待接收的Promise对象(p1)的resolve执行后再返回,并且返回值是该接收Promise对象(p1)的resolve的返回值。是不是有点绕,因此,p2等待1秒后就输出了值“p2”,但是他的then函数确要等到resolve函数响应后才进行输出,因此要等待3秒后才输出了“p1”。

这样是不是实现无嵌套情况下的异步操作有序调用,(p2要等待p1)。突然觉得嵌套丑的就丑点吧,这特么也太麻烦了。当然后来结合上ES6的await和async就好多了,这个同学们去阮大的ES6教程中看吧。

对了刚才说了resolve的作用有两个,而另一个的话不过是把异步操作的结果暴露出来了而已,这样的话,异步操作的回调函数就可以在任何地点去定义了,不用像事件处理程序一样一定要定义在异步操作的前面。这又涉及到事件队列多线程的的问题了,这个同学们实在不明白可以到时再问下我。

Promise的简单使用就说到这儿了,要声明下,这些都是简化版的使用,比如Promise构造函数接收的回调函数中其实还有个reject函数参数的,具体的同学们要去阮大的ES6教程中去学习。

后话:

第二篇在掘金的文章。