【转】详解JS的四种异步解决方案:回调函数、Promise、Generator、async/await

转自:详解JS的四种异步解决方案:回调函数、Promise、Generator、async/await

同步&异步的概念

在讲这四种异步方案之前,我们先来明确一下同步和异步的概念:

所谓同步(synchronization),简单来说,就是顺序执行,指的是同一时间只能做一件事情,只有目前正在执行的事情做完之后,才能做下一件事情。比如咱们去火车站买票,假设窗口只有1个,那么同一时间只能处理1个人的购票业务,其余的需要进行排队。这种one by one的动作就是同步。同步操作的优点在于做任何事情都是依次执行,井然有序,不会存在大家同时抢一个资源的问题。同步操作的缺点在于会阻塞后续代码的执行。如果当前执行的任务需要花费很长的时间,那么后面的程序就只能一直等待。从而影响效率,对应到前端页面的展示来说,有可能会造成页面渲染的阻塞,大大影响用户体验。

所谓异步(Asynchronization),指的是当前代码的执行不影响后面代码的执行。当程序运行到异步的代码时,会将该异步的代码作为任务放进任务队列,而不是推入主线程的调用栈。等主线程执行完之后,再去任务队列里执行对应的任务即可。因此,异步操作的优点就是不会阻塞后续代码的执行

js中异步的应用场景

开篇讲了同步和异步的概念,那么在JS中异步的应用场景有哪些呢?

  • 定时任务:setTimeout、setInterval

  • 网络请求:ajax请求、动态创建img标签的加载

  • 事件监听器:addEventListener

实现异步的四种方法

对于setTimeout、setInterval、addEventListener这种异步场景,不需要我们手动实现异步,直接调用即可。但是对于ajax请求、node.js中操作数据库这种异步,就需要我们自己来实现了~

1、 回调函数

在微任务队列出现之前,JS实现异步的主要方式就是通过回调函数。以一个简易版的Ajax请求为例,代码结构如下所示:

function ajax(obj){let default = {url: '...',type:'GET',async:true,contentType: 'application/json',success:function(){}};for (let key in obj) {defaultParam[key] = obj[key];}let xhr;if (window.XMLHttpRequest) {xhr = new XMLHttpRequest();} else {xhr = new ActiveXObject('Microsoft.XMLHTTP');}xhr.open(defaultParam.type, defaultParam.url+'?'+dataStr, defaultParam.async);xhr.send();xhr.onreadystatechange = function (){if (xhr.readyState === 4){if(xhr.status === 200){let result = JSON.parse(xhr.responseText);// 在此处调用回调函数defaultParam.success(result);}}}
}

我们在业务代码里可以这样调用ajax请求:

ajax({url:'#',type:GET,success:function(e){// 回调函数里就是对请求结果的处理}
});

ajax的success方法就是一个回调函数,回调函数中执行的是我们请求成功之后要做的进一步操作。这样就初步实现了异步,但是回调函数有一个非常严重的缺点,那就是回调地狱的问题。大家可以试想一下,如果我们在回调函数里再发起一个ajax请求呢?那岂不是要在success函数里继续写一个ajax请求?那如果需要多级嵌套发起ajax请求呢?岂不是需要多级嵌套?如果嵌套的层级很深的话,我们的代码结构可能就会变成这样:

因此,为了解决回调地狱的问题,提出了Promise、async/await、generator的概念。

2、Promise

Promise作为典型的微任务之一,它的出现可以使JS达到异步执行的效果。一个Promise函数的结构如下列代码如下:

const promise = new Promise((resolve, reject) => {resolve('a');
});
promise.then((arg) => { console.log(`执行resolve,参数是${arg}`) }).catch((arg) => { console.log(`执行reject,参数是${arg}`) }).finally(() => { console.log('结束promise') });
复制代码

如果,我们需要嵌套执行异步代码,相比于回调函数来说,Promise的执行方式如下列代码所示:

const promise = new Promise((resolve, reject) => {resolve(1);
});
promise.then((value) => {console.log(value);return value * 2;}).then((value) => {console.log(value);return value * 2;}).then((value) => {console.log(value);}).catch((err) => {console.log(err);});

即,通过then来实现多级嵌套(链式调用),这看起来是不是就比回调函数舒服多了~

每个Promise都会经历的生命周期是:

  • 进行中(pending) - 此时代码执行尚未结束,所以也叫未处理的(unsettled)

  • 已处理(settled) - 异步代码已执行结束 已处理的代码会进入两种状态中的一种:

    • 已完成(fulfilled) - 表明异步代码执行成功,由resolve()触发

    • 已拒绝(rejected)- 遇到错误,异步代码执行失败 ,由reject()触发

因此,pending,fulfilled, rejected就是Promise中的三种状态啦~ 大家一定要牢记,在Promise中,要么包含resolve()来表示Promise的状态为fulfilled,要么包含reject()来表示Promise的状态为rejected。不然我们的Promise就会一直处于pending的状态,直至程序崩溃...

除此之外,Promise不仅很好的解决了链式调用的问题,它还有很多神奇的操作呢:

  • **Promise.all(promises)**:接收一个包含多个Promise对象的数组,等待所有都完成时,返回存放它们结果的数组。如果任一被拒绝,则立即抛出错误,其他已完成的结果会被忽略

  • Promise.allSettled(promises): 接收一个包含多个Promise对象的数组,等待所有都已完成或者已拒绝时,返回存放它们结果对象的数组。每个结果对象的结构为{status:'fulfilled' // 或 'rejected', value // 或reason}

  • Promise.race(promises): 接收一个包含多个Promise对象的数组,等待第一个有结果(完成/拒绝)的Promise,并把其result/error作为结果返回

function getPromises(){return [new Promise(((resolve, reject) => setTimeout(() => resolve(1), 1000))),new Promise(((resolve, reject) => setTimeout(() => reject(new Error('2')), 2000))),new Promise(((resolve, reject) => setTimeout(() => resolve(3), 3000))),];
}Promise.all(getPromises()).then(console.log);
Promise.allSettled(getPromises()).then(console.log);
Promise.race(getPromises()).then(console.log);

打印结果如下:

3、Generator

Generator是ES6提出的一种异步编程的方案。因为手动创建一个iterator十分麻烦,因此ES6推出了generator,用于更方便的创建iterator。也就是说,Generator就是一个返回值为iterator对象的函数。
在讲Generator之前,我们先来看看iterator是什么:
iterator是什么?
iterator中文名叫迭代器。它为js中各种不同的数据结构(Object、Array、Set、Map)提供统一的访问机制。任何数据结构只要部署了Iterator接口,就可以完成遍历操作。 因此iterator也是一种对象,不过相比于普通对象来说,它有着专为迭代而设计的接口。

iterator 的作用:

  • 为各种数据结构,提供一个统一的、简便的访问接口;

  • 使得数据结构的成员能够按某种次序排列;

  • ES6 创造了一种新的遍历命令for…of循环,Iterator 接口主要供for…of消费

iterator的结构: 它有next方法,该方法返回一个包含valuedone两个属性的对象(我们假设叫result)。value是迭代的值,后者是表明迭代是否完成的标志。true表示迭代完成,false表示没有。iterator内部有指向迭代位置的指针,每次调用next,自动移动指针并返回相应的result。

原生具备iterator接口的数据结构如下:

  • Array

  • Map

  • Set

  • String

  • TypedArray

  • 函数里的arguments对象

  • NodeList对象

这些数据结构都有一个Symbol.iterator属性,可以直接通过这个属性来直接创建一个迭代器。也就是说,Symbol.iterator属性只是一个用来创建迭代器的接口,而不是一个迭代器,因为它不含遍历的部分。
使用Symbol.iterator接口生成iterator迭代器来遍历数组的过程为:

let arr = ['a','b','c'];let iter = arr[Symbol.iterator]();iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }

for ... of的循环内部实现机制其实就是iterator,它首先调用被遍历集合对象的 Symbol.iterator 方法,该方法返回一个迭代器对象,迭代器对象是可以拥有.next()方法的任何对象,然后,在 for ... of 的每次循环中,都将调用该迭代器对象上的 .next 方法。然后使用for i of打印出来的i也就是调用.next方法后得到的对象上的value属性。

对于原生不具备iterator接口的数据结构,比如Object,我们可以采用自定义的方式来创建一个遍历器。

比如,我们可以自定义一个iterator来遍历对象:

let obj = {a: "hello", b: "world"};
// 自定义迭代器
function createIterator(items) {let keyArr = Object.keys(items);let i = 0;return {next: function () {let done = (i >= keyArr.length);let value = !done ? items[keyArr[i++]] : undefined;return {value: value,done: done,};}};
}let iterator = createIterator(obj);
console.log(iterator.next()); // "{ value: 'hello', done: false }"
console.log(iterator.next());  // "{ value: 'world', done: false }"
console.log(iterator.next());  // "{ value: undefined, done: true }"

接下来,我们来聊聊Generator:
我们通过一个例子来看看Gnerator的特征:

function* createIterator() {yield 1;yield 2;yield 3;
}
// generators可以像正常函数一样被调用,不同的是会返回一个 iterator
let iterator = createIterator();
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3

Generator 函数是 ES6 提供的一种异步编程解决方案。形式上,Generator 函数是一个普通函数,但是有两个特征:

  • function关键字与函数名之间有一个星号

  • 函数体内部使用yield语句,定义不同的内部状态

Generator函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象(Iterator Object

打印看看Generator函数返回值的内容:发现generator函数的返回值的原型链上确实有iterator对象该有的next,这充分说明了generator的返回值是一个iterator。除此之外还有函数该有的return方法和throw方法。

在普通函数中,我们想要一个函数最终的执行结果,一般都是return出来,或者以return作为结束函数的标准。运行函数时也不能被打断,期间也不能从外部再传入值到函数体内。但在generator中,就打破了这几点,所以generator和普通的函数完全不同。当以function*的方式声明了一个Generator生成器时,内部是可以有许多状态的,以yield进行断点间隔。期间我们执行调用这个生成的Generator,他会返回一个遍历器对象,用这个对象上的方法,实现获得一个yield后面输出的结果。

function* generator() {yield 1yield 2
};
let iterator = generator();
iterator.next()  // {value: 1, done: false}
iterator.next()  // {value: 2, done: false}
iterator.next()  // {value: undefined, done: true}

yield和return的区别:

  • 都能返回紧跟在语句后面的那个表达式的值

  • yield相比于return来说,更像是一个断点。遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。

  • 一个函数里面,只能执行一个return语句,但是可以执行多次yield表达式。

  • 正常函数只能返回一个值,因为只能执行一次return;Generator 函数可以返回一系列的值,因为可以有任意多个yield

语法注意点:

  • yield表达式只能用在 Generator 函数里面

  • yield表达式如果用在另一个表达式之中,必须放在圆括号里面

  • yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。

  • 如果 return 语句后面还有 yield 表达式,那么后面的 yield 完全不生效

使用Generator的其余注意事项:

  • 需要注意的是,yield 不能跨函数。并且yield需要和*配套使用,别处使用无效

function* createIterator(items) {items.forEach(function (item) {// 语法错误yield item + 1;});
}
  • 箭头函数不能用做 generator

讲了这么多,那么Generator到底有什么用呢?

  • 因为Generator可以在执行过程中多次返回,所以它看上去就像一个可以记住执行状态的函数,利用这一点,写一个generator就可以实现需要用面向对象才能实现的功能。

  • Generator还有另一个巨大的好处,就是把异步回调代码变成“同步”代码。这个在ajax请求中很有用,避免了回调地狱.

4、 async/await

最后我们来讲讲async/await,终于讲到这儿了!!!async/await是ES7提出的关于异步的终极解决方案。我看网上关于async/await是谁的语法糖这块有两个版本:

  • 第一个版本说async/await是Generator的语法糖

  • 第二个版本说async/await是Promise的语法糖

其实,这两种说法都没有错。关于async/await是Generator的语法糖: 所谓Generator语法糖,表明的就是aysnc/await实现的就是generator实现的功能。但是async/await比generator要好用。因为generator执行yield设下的断点采用的方式就是不断的调用iterator方法,这是个手动调用的过程。针对generator的这个缺点,后面提出了co这个库函数来自动执行next,相比于之前的方案,这种方式确实有了进步,但是仍然麻烦。而async配合await得到的就是断点执行后的结果。因此async/await比generator使用更普遍。

总结下来,async函数对 Generator函数的改进,主要体现在以下三点:

  • 内置执行器:Generator函数的执行必须靠执行器,因为不能一次性执行完成,所以之后才有了开源的 co函数库。但是,async函数和正常的函数一样执行,也不用 co函数库,也不用使用 next方法,而 async函数自带执行器,会自动执行。

  • 适用性更好:co函数库有条件约束,yield命令后面只能是 Thunk函数或 Promise对象,但是 async函数的 await关键词后面,可以不受约束。

  • 可读性更好:async和 await,比起使用 *号和 yield,语义更清晰明了。

关于async/await是Promise的语法糖: 如果不使用async/await的话,Promise就需要通过链式调用来依次执行then之后的代码:

function counter(n){return new Promise((resolve, reject) => { resolve(n + 1);});
}function adder(a, b){return new Promise((resolve, reject) => { resolve(a + b);});
}function delay(a){return new Promise((resolve, reject) => { setTimeout(() => resolve(a), 1000);});
}
// 链式调用写法
function callAll(){counter(1).then((val) => adder(val, 3)).then((val) => delay(val)).then(console.log);
}
callAll();//5

 

虽然相比于回调地狱来说,链式调用确实顺眼多了。但是其呈现仍然略繁琐了一些。而async/await的出现,就使得我们可以通过同步代码来达到异步的效果

async function callAll(){const count = await counter(1);const sum = await adder(count + 3);console.log(await delay(sum));
}
callAll();// 5

由此可见,Promise搭配async/await的使用才是正解!

总结

  • promise让异步执行看起来更清晰明了,通过then让异步执行结果分离出来。

  • async/await其实是基于Promise的。async函数其实是把promise包装了一下。使用async函数可以让代码简洁很多,不需要promise一样需要些then,不需要写匿名函数处理promise的resolve值,也不需要定义多余的data变量,还避免了嵌套代码。

  • async函数是Generator函数的语法糖。async函数的返回值是 promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。同时,我们还可以用await来替代then方法指定下一步的操作。

  • 感觉Promise+async的操作最为常见。因为Generator被async替代了呀~

作者:DoubleSweet

原文链接:https://juejin.cn/post/7082753409060716574

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/435549.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

wince6.0 s5pv210 中断

1. 在smdkv210\src\oal\oallib\ksarm.h头文件里可以看到wince的中断定义。 设备中断的起始编号即wince预定义的设备中断ID的基值 SYSINTR_DEVICES EQU 8 一共支持64个设备中断 SYSINTR_MAX_DEVICES EQU 64 可用的设备…

【转】vsftp配置实例-虚拟用户锁定目录

转自: vsftp配置实例-虚拟用户锁定目录_jasonyang69的博客-CSDN博客_vsftpd锁定用户目录 快速解决 vsftpd nologin 虚拟用户 拒绝访问 无法登录_小飞飞飞鱼的博客-CSDN博客_vsftpd 拒绝访问 一、实验步骤 0、启用 nologin shell #vim /etc/shells /bin/sh /bi…

【转】高端球管使用了哪些高科技?

转自:高端球管使用了哪些高科技? 本文来源:第三方维修平台 本文作者:RepairCT 随着人类发现X射线,这项技术不断应用到医学领域,比如X光机、DR、CT、乳腺钼靶等医疗设备,它们的主要核心均是利用…

关于代码组织的一些看法(上)

今天看了一个篇关于架构的文章,略有所感,记录一下。 软件的架构基本是从一个原始需求出发,逐步构建可维护、更灵活的开发框架的过程,在这个构建过程中可能会逐渐的增加代码的复杂度来满足灵活性的要求,从这个层面来讲&…

CE下基于Zylonite硬件平台的SD卡驱动开发

摘要:本文结合实际项目(一款以WINCE为操作系统内核的GSM/PHS双模智能手机)对嵌入式系统Windows CE5.0的底层驱动(SD卡)的架构进行了分析和研究,以MARVELL公司提供的基于INTEL Zylonite硬件平台的BSP为基础&…

pthread-win32在VC2005下的使用

pthread-win32是一个在Win32环境下的Unix POSIX线程库的移植. 有了它, 可以比较方便的移植Unix/Linux多线程程序到Windows下. 在VC2005下使用也很简单: 下载, 地址是 http://sourceware.org/pthreads-win32 里面include目录中是头文件, lib目录中是.lib和.dll文件. 在VC项目的属…

【转】apt命令

转自:apt命令详解 - 简书 apt命令可以说是Ubuntu系统下最为重要的命令,安装、更新、卸载软件,升级系统内核都离不开apt命令。 一、apt的简介 apt的全称是Advanced Packaging Tool是Linux系统下的一款安装包管理工具。 最初的时候&#xff…

maple 2018 窗口关闭提示乱码_如果解决SOLIDWORKS工程图转CAD字体出现乱码的问题_SolidWorks生信科技...

操作SOLIDWORKS工程图转CAD时,由于SOLIDWORKS使用的是Windows字体,而CAD使用的是线性字体,字体就容易出现乱码的苦恼。一般出现乱码是因为字体映射里面没有中文,所以会导致乱码。遇到这种情况,只需找到字体映射文件&am…

【转】apt 和 apt-get的区别

转自:https://www.sysgeek.cn/apt-vs-apt-get/ Ubuntu 16.04 发布时,一个引人注目的新特性便是 apt 命令的引入。其实早在 2014 年,apt 命令就已经发布了第一个稳定版,只是直到 2016 年的 Ubuntu 16.04 系统发布时才开始引人关注…

准确检测图像的轮廓 opencv_图像处理案例实战

1. 切边源图像: 需求:扫描仪扫描到的法律文件,需要切边,去掉边缘空白,这样看上去才真实,人工操作成本与时间花费高,希望程序自动实现,高效、准确。 实现思路:边缘检测 轮…

pp助手苹果版_再见!PP助手iOS端即将下线 曾是中国最大的苹果助手

苹果PP助手即将下线 在今日PP助手官方发布公告:尊敬的PP助手iOS版用户:衷心感谢您多年以来对我们的支持及厚爱。因业务调整,PP助手将于2020年2月28日正式下线iOS版产品,包括PP助手iOS版、PP助手iOS PC版等。 对您造成的不便还望理…

oppo手机工程模式清除数据需要密码_手机隐藏的快捷键都有哪些?

手机从开发出来到现在,已经换了很多代了,从大哥大,小灵通,到诺基亚到山寨机到苹果到华为,几十年过去了。手机从奢侈品一下飞去每个人手里,反正我手机有手机已经快10年了,所以我相信每个人对自己…

关于竖表转横表的问题

本文作者:dinya内容摘要:在开发过程,经常遇到一些将表的显示方式进行转换的需求,我们习惯性称之为竖表到横表的转换,本文通过一个例子来简要说明常见的两种竖表转横表的问题。本文适宜读者范围:oracle初级&…

全系光谱完整版

宇宙射线也叫电磁波,其中包含γ射线、X射线、紫外线、可见光、红外线、近红外、远红外,还有无线电和超声波。 无线电波是振荡电路中自由电子作周期性的运动产生的. 红外线、可见光、紫外线是原子外层电子受激发产生的. X射线是原…

cat 几行_迷风花说Cat!Cat因为林教练是好教练回去现实却给了一巴掌

滔博TES前主教练迷风花26日发布了微博,就自己离职、Cat林教练事件以及汤汤纵情转会事件进行了澄清,网友粉丝们在看到教练迷风花发布的微博后,更加了解了很多事情的真相,粉丝们纷纷对教练迷风花送上了祝福,教练迷风花到…

前端和后端的英文_计算机专业的本科生在前端、后端、测试和运维之间该如何选择...

首先,对于计算机专业的同学来说,在选择发展方向的时候往往会遇到一些困惑,当前随着计算机技术体系的不断壮大,计算机领域的岗位分类也越来越细化,而且在新技术的推动下,还在不断出现新的岗位,比…

医学影像后处理技术超详细收藏版(上)

转自:医学影像后处理技术超详细收藏版(上) 导语: 医学图像后处理是通过综合运用计算机图像处理技术,医学知识,将由各种数字化成像技术所获得的人体信息按照一定的需要在计算机上表现出来,使之…

【转】VTK和ITK中的坐标系统

当我们在处理医学图像和应用时,一个问题就是坐标系统之间的不同。在图像应用中通常有三个坐标系,分别是世界坐标系,解剖坐标系和图像坐标系。 每种坐标系目的不同,呈现数据的方式也不一样。 解剖坐标系由三个平面组成,…

(ZT)在日本市场推广 iPhone 应用的经验

原贴:http://www.cocoachina.com/appstore/market/2010/0728/1942.html 本文作者为 CocoaChina 会员 “lawrencewu”,是一位专攻日本市场的 iPhone 开发者。他的10款 App (9个收费,一个免费)在9个月的时间里已经取得了…

嵌入式系统 Boot Loader 技术内幕

1. 引言 在专用的嵌入式板子运行 GNU/Linux 系统已经变得越来越流行。一个嵌入式 Linux 系统从软件的角度看通常可以分为四个层次: 1. 引导加载程序。包括固化在固件(firmware)中的 boot 代码(可选),和 Boot Loader 两大部分。 2. Linux 内核。特定于嵌入…