Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 678|回复: 0

前端的异步解决方案之Promise和Await/Async

[复制链接]
  • TA的每日心情
    奋斗
    2024-4-6 11:05
  • 签到天数: 748 天

    [LV.9]以坛为家II

    2034

    主题

    2092

    帖子

    70万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    705612
    发表于 2021-9-7 17:42:42 | 显示全部楼层 |阅读模式

    Promise

    Promise 对象是一个返回值的代理,这个返回值在promise对象创建时未必已知。它允许你为异步操作的成功返回值或失败信息指定处理方法。 这使得异步方法可以像同步方法那样返回值:异步方法会返回一个包含了原返回值的 promise 对象来替代原返回值。 

    我们来看一下官方定义,Promise实际上就是一个特殊的Javascript对象,反映了”异步操作的最终值”。”Promise”直译过来有预期的意思,因此,它也代表了某种承诺,即无论你异步操作成功与否,这个对象最终都会返回一个值给你。
    先写一个简单的demo来直观感受一下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const promise = new Promise((resolve, reject) => {
    $.ajax('https://github.com/users', (value) => {
    resolve(value);
    }).fail((err) => {
    reject(err);
    });
    });

    promise.then((value) => {
    console.log(value);
    },(err) => {
    console.log(err);
    });
    //也可以采取下面这种写法
    promise.then(value => console.log(value)).catch(err => console.log(err));

     

    上面的例子,会在Ajax请求成功后调用resolve回调函数来处理结果,如果请求失败则调用reject回调函数来处理错误。Promise对象内部包含三种状态,分别为pending,fulfilled和rejected。这三种状态可以类比于我们平常在ajax数据请求过程的pending,success,error。一开始请求发出后,状态是Pending,表示正在等待处理完毕,这个状态是中间状态而且是单向不可逆的。成功获得值后状态就变为fulfilled,然后将成功获取到的值存储起来,后续可以通过调用then方法传入的回调函数来进一步处理。而如果失败了的话,状态变为rejected,错误可以选择抛出(throw)或者调用reject方法来处理。

    请求的几个状态:

    1. pending( 中间状态)—> fulfilled , rejected
    2. fulfilled(最终态)—> 返回value 不可变
    3. rejected(最终态) —> 返回reason 不可变

    如图所示:

    promisespromises

    promise

    一个promise内部可以返回另一个promise,这样就可以进行层级调用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    const getAllUsers = new Promise((resolve, reject) => {
    $.ajax('https://github.com/users', (value) => {
    resolve(value);
    }).fail((err) => {
    reject(err);
    });
    });

    const getUserProfile = function(username) {
    return new Promise((resolve, reject) => {
    $.ajax('https://github.com/users' + username, (value) => {
    resolve(value);
    }).fail((err) => {
    reject(err);
    });
    };

    getAllUsers.then((users) => {
    //获取第一个用户的信息
    return getUserProfile(users[0]);
    }).then((profile) => {
    console.log(profile)
    }).catch(err => console.log(err));

    Promise实现原理

    目前,有多种Promise的实现方式,我选择了https://github.com/then/promise的源码进行阅读。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    function Promise(fn) {
    var state = null; //用以保存处理状态,true为fulfilled状态,false为rejected状态
    var value = null; //用以保存处理结果值
    var deferreds = [];
    var self = this;
    this.then = function(onFulfilled, onRejected) {
    return new self.constructor(
    function(resolve, reject) {...}
    );
    }; //返回一个延迟处理函数,调用这个方法,就能触发用户传入的处理函数,分别对应处理promise的fulfilled状态和rejected状态

    function handle(deferred) {...} //延迟队列处理

    function resolve(newValue) {...} //更新value值,并把state更新为true,代表结果正常

    function reject(newValue) {...} //更新vlaue值,并把state更新为false,代表结果错误,这个value值就是错误原因方便后面调用处理

    function finale() {...} //清空异步队列

    doResolve(fn, resolve, reject); //调用resolve和reject两个回调函数处理结果
    }

     

    通过阅读promise的源码,我们可以很清楚地看到,在构建一个promise对象的时候,是利用函数式编程的特性,如惰性求值和部分求值等来进行将异步处理的。而处理多线程并发的机制就是利用setTimeout(fn,0)这个技巧。

    构造Promise

    Promise构造函数的初始函数需要有两个参数,resolve和reject,分别对应fulfilled和rejected两个状态的处理。

    1
    2
    3
    4
    5
    6
    7
    8
    var promise = new Promise((resolve, reject) => {
    try {
    var value = doSomething();
    resolve(value);
    } catch(err) {
    reject(err);
    }
    });

    Promise的常用方法

    1.Promise.all(iterator):

    ​ 返回一个新的promise对象,其中所有promise的对象成功触发的时候,该对象才会触发成功,若有任何一个发成错误,就会触发改对象的失败方法。成功触发的返回值是所有promise对象返回值组成的数组。直接看例子吧:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //设置三个任务
    const tasks = {
    task1() {
    return new Promise(...); //return 1
    },

    task2() {
    return new Promise(...); // return 2
    },

    task3() {
    return new Promise(...); // return 3
    }
    };

    //列表中的所有任务会并发执行,当所有任务执行状态都为fulfilled后,执行then方法
    Promise.all([tasks.task1(), tasks.task2(), tasks.task3()]).then(result => console.log(result));
    //最终结果为:[1,2,3]

    2.Promise.race(iterable): 返回一个新的promise对象,其回调函数迭代遍历每个值,分别处理。同样都是传入一组promise对象进行处理,同Promise.all不同的是,只要其中有一个promise的状态变为fulfilledrejected,就会调用后续的操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //设置三个任务
    const tasks = {
    task1() {
    return new Promise(...); //return 1
    },

    task2() {
    return new Promise(...); // return 2
    },

    task3() {
    return new Promise(...); // return 3
    }
    };

    //列表中的所有任务会并发执行,只要有一个promise对象出现结果,就会执行then方法
    Promise.race([tasks.task1(), tasks.task2(), tasks.task3()]).then(result => console.log(result));
    //假设任务1最开始返回结果,则控制台打印结果为`1`

    3.Promise.reject(reason): 返回一个新的promise对象,用reason值直接将状态变为rejected

    1
    2
    3
    4
    5
    const promise2 = new Promise((resolve, reject) => {
    reject('Failed');
    });

    const promise2 = Promise.reject('Failed');

    上面两种写法是等价的。

    4.Promise.resolve(value): 返回一个新的promise对象,这个promise对象是被resolved的。

    与reject类似,下面这两种写法也是等价的。

    1
    2
    3
    4
    5
    const promise2 = new Promise((resolve, reject) => {
    resolve('Success');
    });

    const promise2 = Promise.resolve('Success');

    5.then 利用这个方法访问值或者错误原因。其回调函数就是用来处理异步处理返回值的。

    6.catch 利用这个方法捕获错误,并处理。

    Generator & Iterator 迭代器和生成器

    虽然Promise解决了回调地狱(callback hell)的问题,但是仍然需要在使用的时候考虑到非同步的情况,而有没有什么办法能让异步处理的代码写起来更简单呢?在介绍解决方案之前,我们先来介绍一下ES6中有的迭代器和生成器。
    迭代器(Iterator),顾名思义,它的作用就是用来迭代遍历集合对象。
    在ES6语法中迭代器是一个有next方法的对象,可以利用Symbol.iterator的标志返回一个迭代器。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const getNum = {
    [Symbol.iterator]() {
    let arr = [1,2,3];
    let i = 0;
    return {
    next() {
    return i < arr.length ? {value: arr[i++]} : {done: true};
    }
    }
    }
    }

    //利用for...of语法遍历迭代器
    for(const num of getNum) {
    console.log(num);
    }

    而生成器(Generator)可以看做一个特殊的迭代器,你可以不用纠结迭代器的定义形式,使用更加友好地方式实现代码逻辑。
    先来看一段简单的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function* getNum() {
    yield 1;
    yield 2;
    yield 3;
    }
    //调用生成器,生成一个可迭代的对象
    const gen = getNum();

    gen.next(); // {value: 1, done: false}
    gen.next(); // {value: 2, done: false}
    gen.next(); // {value: 3, done: true}

     

    生成器函数的定义需要使用function*的形式,这也是它和普通函数定义的区别。yield是一个类似return的关键字,当代码执行到这里的时候,会暂停当前函数的执行,并保存当前的堆栈信息,返回yield后面跟着表达式的值,这个值就是上面代码所看到的value所对应的值。而done这个属性表示是否还有更多的元素。当donetrue的时候,就表明这个迭代过程结束了。需要注意的是这个next方法其实传入参数,这个参数表示上一个yield语句的返回值,如果你给next方法传入了参数,就会将上一次yield语句的值设置为对应值。

    利用generator的异步处理

    先来看一下下面这段代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    function getFirstName() {
    setTimeout(() => {
    gen.next('hello');
    },2000);
    }

    function getLastName() {
    setTimeout(() => {
    gen.next('world');
    },1000);
    }

    function* say() {
    let firstName = yield getFirstName();
    let lastName = yield getLastName();
    console.log(firstName + lastName);
    }

    var gen = say();

    gen.next(); // {value: undefined, done: false}
    //helloworld

     

    我们可以发现,当第一次调用gen.next()后,程序执行到第一个yield语句就中断了,而在getFirstName里显式地将上一个yield语句的返回值改为hello,触发了第二yield语句的执行。以此类推,最终就打印出我们想要的结果了。

    spawn函数

    我们可以考虑把上面的代码改写一下,在这里将Promise和Generator结合起来,将异步操作用Promise对象封装好,然后,resolve出去,而创建一个spawn函数,这个函数的作用是自动触发generatornext方法。来看一下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    function getFirstName() {
    return new Promise((resolve, reject) => {
    setTimeout(() => {
    resolve('hello');
    }, 2000);
    });
    }

    function getLastName() {
    return new Promise((resolve, reject) => {
    setTimeout(() => {
    resolve('world');
    }, 1000);
    });
    }

    function* say() {
    let firstName = yield getFirstName();
    let lastName = yield getLastName();
    console.log(firstName + lastName);
    }

    function spawn(generator) {
    return new Promise((resolve, reject) => {
    var onResult = (lastPromiseResult) => {
    var {value, done} = generator.next(lastPromiseResult);
    if(!done) {
    value.then(onResult, reject);
    }else {
    resolve(value);
    }
    }
    onResult();
    });
    }

    spawn(say()).then((value) => {console.log(value)});

     

    到这里,这个解决方案就很接近接下来要介绍的async/await的实现方式了。

    Async/Await

    这两个关键字其实是一起使用的,async函数其实就相当于funciton *的作用,而await就相当与yield的作用。而在async/await机制中,自动包含了我们上述封装出来的spawn自动执行函数。
    利用这两个新的关键字,可以让代码更加简洁和明了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    function getFirstName() {
    return new Promise((resolve, reject) => {
    setTimeout(() => {
    console.log('hello');
    resolve('hello');
    }, 2000);
    });
    }

    function getLastName() {
    return new Promise((resolve, reject) => {
    setTimeout(() => {
    console.log('world');
    resolve('world');
    }, 1000);
    });
    }

    async function say() {
    let firstName = await getFirstName();
    let secondName = await getLastName();
    return firstName + lastName;
    }

    console.log(say());

     

    执行结果为,先等待2秒打印hello,再等待1秒打印world,最后打印’helloworld’,与预期的执行顺序是一致的。

    上面的代码你需要注意的是,你必须显式声明await,否则你会得到一个promise对象而不是你想要获得的值。

    比起Generator函数,async/await的语义更好,代码写起来更加自然。将异步处理的逻辑放在语法层面去处理,写的代码也更加符合人的自然思考方式。

    错误处理

    对于async/await这种方法来说,错误处理也比较符合我们平常编写同步代码时候处理的逻辑,直接使用try..catch就可以了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function getUsers() {
    return $.ajax('https://github.com/users');
    }

    async function getFirstUser() {
    try {
    let users = await getUsers();
    return users[0].name;
    } catch (err) {
    return {
    name: 'default user'
    }
    }
    }
    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-4-20 20:08 , Processed in 0.083715 second(s), 29 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表