深入理解javascript系列(十九):从Promise开始到async/await

1,371 阅读7分钟

什么是同步与异步的定义,在这里我就不做记录,直接用代码来表示它们之间的区别。

首先使用Promise模拟一个发起请求的函数,该函数执行后,会在1s之后返回数值30。

function fn() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve(30);
        }, 1000);
    })
}

在该函数的基础上,我们也可以使用async/await语法来模拟同步效果。

var foo = async function() {
    var t = await fn();
    console.log(t);
    console.log('next');
}

foo();

输出结果为:

Promise {<pending>} //1s 之后依次输出
test:11 30
test:12 next

而异步效果则会有不同的输出结果:

var foo = function() {
    fn().then(function(res) {
        console.log(res);
    });
    console.log('next');
}

输出结果:

next
// 1s后
30

好了,接下来我们正式开始记录Promise

Promise

1.  Ajax

Ajax是网页与服务端进行数据交互的一种技术。我们可以通过服务端提供的接口,用Ajax向服务端请求我们需要的数据。过程如下:

//简单的Ajax原生实现

//服务端接口
var url = 'api/xxxx';
var result;

var XHR = new XMLHttpRequest();
XHR.open('GET', url, true);
XHR.send();

XHR.onreadystatechange = function() {
    if(XHR.readyState == 4 && XHR.status == 200) {
        result = XHR.response;
    }
}

这样看上去并没有什么问题。但是如果这个时候,还需要做另一个Ajax请求,那么这个新的Ajax请求中的一个参数,则必须从上一个Ajax请求中获取,这个时候我们就不得不就得在result得到后在进行一次请求。

当第三个Ajax(甚至更多)仍然依赖上一个请求的时候,此时的代码就变成了一场灾难。我们需要不停地嵌套回调函数,以确保下一个接口所需要的参数的正确性,这样的灾难,我们称为回调地狱。

所以随着发展,就出现了Promise,他能解决这个问题。

我们想要确保某代码在某某之后执行时,可以利用函数调用栈,将想要执行的代码放入回调函数中(这是利用同步阻塞)。

function a(callback) {
    console.log('先结婚')
    callback();
}

function b() {
    console.log('再生孩子')
}
a(b);

插个题外话:“浏览器最早内置的setTimeout与setInterval就是基于回调的思想实现的”。

但是这里也有一个问题,我们想要在a中执行的代码必须现在callback之前才能输出我们想输出的。那该怎么办?

其实问题很好解决,除了利用函数调用栈的执行顺序外,还可以利用队列机制来确保我们想要的代码压后执行。

function a(callback) {
    //将想要执行的代码放入队列中后,根据事件循环机制,
    //就不用把它放到最后面了。
    callback && setTimeout(callback, 0);
    console.log('先结婚')

}

function b() {
    console.log('再生孩子')
}
a(b);

与setTimeout类似,Promise也可以认为是一种任务分发器,它将任务分配到Promise队列中,通常的流程是首先发起一个请求,然后等待(等待时间没法确定)并处理请求结果。

var tag = true;
var p = new Promise(function(resolve, reject) {
    if(tag) {
        resolve('tag is true')
    } else {
        reject('tag is false')
    }
})

p.then(function(result) {
    console.log(result);
})
.catch(function(err) {
    console.log(err);
})

下面简单介绍一下Promise的相关基础知识:

  • new Promise表示创建一个Promise实例对象。
  • Promise函数中的第一参数为一个回调函数,也可以称之为executor。通常情况下,在这个函数中,会执行发起请求操作,并修改结果的状态值。
  • 请求结果有三种状态,分别是pending(等待中,表示还没有得到结果)、resolved(得到了我们想要的结果,可以继续执行),以及rejected(得到了错误的,或者不是我们期望的结果,拒绝继续执行)。请求结果的默认状态为pending。在executor函数中,可以分别使用resolve与rejected将状态修改为对应的resolved与rejected。resolve、reject是executor函数的两个参数,它们能够将请求结果的具体数据传递出去。
  • Promise实例拥有的then方法,可以用来处理当请求结果的状态变成resolved时的逻辑。then的第一个参数为一个回调函数,该函数的参数是resolve传递出来的数据。在上面的例子中,result = tag is true。
  • Promise实例拥有的catch方法,可用来处理当前请求结果的状态变成rejectd时的逻辑。catch的第一个参数为一个回调函数,该函数的参数是一个reject传递出来的数据。在上面的例子中,err = tag is false。
下面通过例子来感受一下Promise的用法。

//demo01.js
function fn(num) {
    //创建一个Promise实例
    return new Promise(function(resolve, reject) {
        if(typeof num == 'number') {
           //修改结果状态值为resolved
           resolve();
        } else {
            // 修改结果状态值为rejected
            reject();
        }
    }).then(function() {
        console.log('参数是一个number值');
    }).catch(function() {
        console.log('参数不是一个number值');
    })
}

//修改参数的类型,观察输出的结果
fn('12');

//注意观察该语句的执行顺序
console.log('next code');

then方法可以接收两个参数,第一个参数用来处理resolved状态的逻辑,第二个参数用来处理rejected状态的逻辑。

then方法因为返回的仍是一个Promise实例对象,因此then方法可以嵌套使用。在这个过程中,通过在内部函数末尾return的方式,能够将数据持续往后传递。

下面我们来对Ajax进行一个简单的封装。

var url = 'api/xxxx';

//封装一个get请求的方法
function getJSON(url) {
    return new Promise(function(resolve, reject) {
        //利用Ajax发送一个请求
        var XHR = new XMLHttpRequest();
        XHR.open('GET', url, true);
        XHR.send();

        //等待结果
        XHR.onreadystatechange = function() {
            if(XHR.readyState == 4) {
                if(XHR.status == 200) {
                    try {
                        var res = JSON.parse(XHR.responseText);
                        // 得到正确的结果修改状态并将数据传递出去
                        resolve(response);
                    } catch(e) {
                        reject(e)
                    }
                } else {
                    // 得到错误的结果并抛出异常
                    reject(new Error(XHR.statusText));
                }
            }
        }
    })
}


//封装好以后,使用就很简单了
getJSON(url).then(function(res){
    console.log(res)
})

2.  Promise.all

当有一个Ajax请求,它的参数需要另外两个甚至更多个请求都有返回结果之后才能确定时,就需要用到Promise.all来帮助我们应对这个场景。

Promise.all接收一个Promise对象组成的数组作为参数,当这个数组中所有的Promise对象状态都变成resolved或者rejected时,它才会去调用then方法。

var url1 = 'xxx';
var url2 = 'xxxxx';

function renderAll() {
    return Promise.all([getJSON(url1), getJSON(url2)]);
}

renderAll().then(function(value) {
    console.log(value);
})

3.  Promise.race

与Promise.all相似的是,Promise.race也是一个Promise对象组成的数组作为参数,不同的是,只要当数组中的其中一个Promise状态变成了resolved或者rejected时,就可以调用then方法。

async/await

异步问题不仅可以用Promise,还可以用async/await,都说这是终极解决方案。

async/await是ES7中新增的语法,虽然现在有些浏览器已经支持了该语法,但在实际使用中,仍然需要在构建工具中配置对该语法的支持才能放心使用。

在函数声明的前面,加上关键字async,这就是async的具体使用。

async function fn() {
    return 30;
}

//或者
const fn = async ()=> {
    return 30;
}

console.log(fn());

//打印结果
Promise {<resolved>: 30}__proto__:Promise[[PromiseStatus]]:"resolved"[[PromiseValue]]:30

可以发现打印结果是一个Promise对象,因此可以猜到async其实是Promise的一个语法糖,目的是为了让写法更加简单,因此也可以使用Promise的相关语法来处理后续的逻辑。

fn().then(res=>{
    console.log(res);
})

await的含义是等待,意思就代码需要等待await后面的函数运行完并且有了返回结果之后,才继续执行下面的代码。这正是同步的效果。

但是需要注意的是,await关键字只能在async函数中使用,并且await后面的函数运行后必须返回一个Promise对象才能实现同步的效果。

当使用一个变量去接收await的返回值时,该返回值为Promise中resolve传递出来的值,也就是PromiseValue。

为了切实感受下async/await的用法。我们结合实际开发中最常遇到的异步请求接口的场景。

//先定义接口请求的方法,由于jQuery封装的几个请求方法都是返回Promise实例。
//因此可以直接使用async/await函数实现同步

const getUserInfo = () => $.get('api/asdsd');

const clickHandler = async ()=>{
    try{
        const res = await getUserInfo();
        console.log(res);
        
        // do something
    } catch(e){
        //处理错误逻辑
    }
}

为了保证逻辑的完整性,在实践中try/catch必不可少。