当前位置: 主页 > 网页 >

浅析js中的Promise和async/await

时间:2021-09-30  作者:haden   点击:
【摘要】浅析js中的Promise和async/await 最早的callback js是单线程的,为了避免io类操作阻塞主线程,所以必须采用回调函数callback的形式把耗时的io操作委托给其他io线程处理(所以js并不是纯的单线

最早的callback

js是单线程的,为了避免io类操作阻塞主线程,所以必须采用回调函数callback的形式把耗时的io操作委托给其他io线程处理(所以js并不是纯的单线程,只是有一个主线程在做mainloop而已)比如这样:

fs.readFile(“a.txt”,{encoding:“utf-8”}, function (err, fr) {
    //readFile回调函数
    if (err) {
        console.log(err);
    }else {
        let str = fr;
        fs.readFile(str,{encoding:“utf-8”}, function (err, fr2){
        if (err) {
            console.log(err);
        }else{
            console.log(fr2);
        }
        })
    }
});

callback的代码是很影响阅读体验的,上面的代码我要这么理解:先read一个file,然后获得的内容到哪里去了呢?它被赋值给回调函数的fr参数里去了,我要再去看回调函数是怎么处理fr的,所以我的注意力又转到回调函数里去了,然后回调函数里又嵌套了一层callback,我的思路又被打断了,又要进入第二个callback看看fr2是怎么处理的。

人不是机器,都习惯顺序查看代码,假如能这样写那代码就好理解的多了:

let str = fs.readFile(“./a.txt”,{encoding:“utf-8”})
if(str){
    let fr2 = fs.readFile(str,{encoding:“utf-8”})
    if(fr2){
        console.log(fr2)
    }
}

所以后来各种架构做了各种努力让js的代码更加human readable。

Promise

我们先说说Promise,因为它是后面要讲的async/await的基础。

首先要说清楚Promise到底是什么

Promise是一个代理对象,它代理的是一个值,这个值我们可以称为Promise对象的状态(status)。这个status对Promise非常重要,它有三种状态:pending,resolved,rejected。我们来看看怎么去new一个Promise:

new Promise( function(resolve, reject) {…} / executor / );

Promise的构造函数里直接传入了一个function,我们可以称呼它为executor,它有两个函数参数resolve,reject。当上面这行语句执行的时候,executor会立即异步执行。在executor方法的方法体内,你可以写你自己的代码逻辑,一般逻辑代码都包括正常执行逻辑和出错异常处理,你可以在代码的正常执行逻辑里调用resolve(retValue)来把Promise的status改为resolved,在出错异常处理的代码里调用reject(err)来把Promise的status改为rejected。也就是说,executor函数的执行成功还是失败,是可以从Promise的状态里判断出来的。这里要注意两点:

  1. Promise从pending状态改为resolved或rejected状态只会有一次,一旦变成resolve或rejected之后,这个Promise的状态就再也不会改变了。
  2. 通过resolve(retValue)传入的retValue可以是任何值,null也可以,它会传递给后面的then方法里的function去使用。通过rejected(err)传入的err理论上也是没有限制类型的,但我们一般都会传入一个Error,比如reject(new Error(“Error”))

then和catch

Promise的状态变化了有什么用呢,它的状态可以影响后续的then的行为

promise.then(function onFulfilled(value){
        console.log(value); })
    .catch(function onRejected(error){
        console.error(error);})

当promise的状态是resolved时候,会调用then方法里面的onFulfilled函数,value的值就是我们通过上面提到的resolve(retValue)传入的;如果是rejected状态,会调用catch方法里面的onRejected函数,error就是我们通过rejected(error)传入的。上面的then catch模式和下面的模式是等价的:

promise.then(function onFulfilled(value){ console.log(value); },
function onRejected(error){ console.error(error);})

then里面的onFulfilled和onRejected函数的执行,有个前提是promise必须要是resolved或者rejected状态,如果是pending状态,then是不会执行的。所以聪明的你一定想到了,promise就可以替代callback了,我们把读取文件的代码放到promise的executor里面,当读取完毕了再resolve这个promise,就可以接着执行then里面的onFulfilled了,所以本文开头的例子可以这样重写了:

//对readFile做一下封装,从这个封装的代码可以看出来promise完美地对应了callback的代码书写方式
const readFile = function (path) {
    return new Promise((resolve, reject) => {
        fs.readFile(path, (err, data) => {
            if (err) reject(err);
            resolve(data);//注释1
        });
    });
};
//readFile会把读取文件包装成一个promise,我们接下来开始使用它
readFile(‘a.text’).then(function onFulfilled(str) {
    console.log(str);
    return readFile(str);
}).then(function onFulfilled(str2) {
    console.log(str2);
}).catch(function onRejected(error){ console.error(error);});

有几点需要说明下:

  1. then可以多次调用形成一个调用链。每个then返回的都必须是一个promise,比如上面代码中的readFile函数返回的就是一个Promise。假如then方法里面的onFulfilled函数返回的不是promise,比如是Number或string,那么架构会用Promise.resolve(return的返回值)包装成一个resolved状态的promise返回。传入下一个then的不是promise,而是resolve方法的参数,比如上面代码中注释1那一行的data。
  2. then的调用链可以有多个then,但最后一般都需要有个catch来捕捉前面某个then的promise通过reject(error)传过来的error,then的调用链中只要有一个then边长rejected状态,那么后面的then都不会执行,直接跳到catch。catch也可以捕捉promise和then里面的Error,类似加上了try catch的效果,但是如果function里面如果有异步代码,是没有办法catch到的:
var promise = new Promise(function (resolve, reject) {
    setTimeout(function () { 
        throw new Error(‘error’) //setTimeout里的代码是异步执行的,throw的Error不会被捕捉
    }, 0)
    //throw new Error(‘error’)   //把上面的setTimeout注释掉,用这一行的throw new Error是会被捕捉的
  1. then里面的function onFulfilled必须return了之后下一个then才会执行,而且会把返回值传递给下个then的onFulfilled函数,所以then的调用链可以构成一个顺序执行的方法链,每个方法都依赖于前一个方法执行之后的返回值(如果没有return语句,会传递undefined给下一个方法)。我们尽量不要在then的调用链里调用异步函数,比如:
Promise.resolve(43).then(function (value) {//then方法1
    console.log(value);
    setTimeout(()=>{
        return value+1
    },1000)
}).then(value=>console.log(value))//then方法2

上面的代码会输出:

43
undefined

因为setTimeout是异步执行的,所以then方法1不会等它1000ms后返回value+1传给then方法2,而是直接传了undefined。异步的需求完全可以通过新增一个then来解决。

async/await

上面的promise调用链看起来比callback方式清晰了不少,但是还是有它的不足:

  • 还是不够简洁。我们仍然需要创建then的调用链,需要创建匿名函数,把返回值一层层传递给下一个then调用。
  • 异常不会向上抛出。比如某个then里的函数抛出异常,即使没有写catch,异常也不会向上抛出,所以你在then的调用链外面写try catch是没有效果的。
  • 代码调试的小问题。你在某个then的方法中设置断点,然后一步步往下走,你是不能步进到下一个then的方法的。你只能每个then里面都设置断点,然后resume run到下一个断点。所以async/await就应运而生了。async是一个函数的修饰符,加上async关键词的函数会隐式地返回一个Promise,函数的返回值将作为Promise resolve的值。await后面跟的一定是一个Promise,await只能出现在async函数内,await的语义是:必须等到await后面跟的Promise有了返回值,才能继续执行await的下一行代码,听起来是不是跟同步执行代码很类似?

开篇的代码我们终于可以写成下面的形式了(readFile是我们上文中封装的返回Promise的方法):

async function(){
    let str = await readFile(“./a.txt”)
    if(str){    
        let fr2 = await readFile(str)
        if(fr2){
            console.log(fr2)
        }
    }
}

看起来是不是很完美?跟同步代码比只多了await。相比于Promise写法,它的好处是:

  • 代码简洁明了,易于阅读和理解
  • 抛出的异常可以被try catch捕捉到
  • 对程序员也友好,await是可以步进到下一行代码的
顶一下
(0)
0%
踩一下
(0)
0%
发表评论
请自觉遵守互联网相关的政策法规,严禁发布色情、暴力、反动的言论。
评价:
验证码: 点击我更换图片

推荐内容