异步编程Promise
1.异步编程2,回调函数3,回调地狱4,Promise4.1,prmise的语法格式:4.2,Promise链式4.3,Promise.all()1.异步编程
有必要了解一下,什么是异步编程,为什么要异步编程。 先说一个概念异步与同步。
介绍异步之前,回顾一下,所谓同步编程,就 是计算机一行一行按顺序依次执行代码,当前代码任务耗时执行会阻塞后 续代码的执行。 同步编程,即是一种典型的请求-响应模型,当请求调用一个函数或方法 后,需等待其响应返回,然后执行后续代码。 一般情况下,同步编程,代码按序依次执行,能很好的保证程序的执行, 但是在某些场景下,比如读取文件内容,或请求服务器接口数据,需要根 据返回的数据内容执行后续操作,读取文件和请求接口直到数据返回这一 过程是需要时间的,网络越差,耗费时间越长,如果按照同步编程方式实 现,在等待数据返回这段时间,JavaScript 是不能处理其他任务的,此时 页面的交互,滚动等任何操作也都会被阻塞,这显然是及其不友好,不可 接受的,而这正是需要异步编程大显身手的场景。我们想通过 Ajax 请求数 据来渲染页面,这是一个在我们前端当中很常见渲染页面的方式。基本每 个页面都会都这样的过程。在这里用同步的方式请求页面会怎么样?浏览 器锁死,不能进行其他操作。而且每当发送新的请求,浏览器都会锁死, 用户体验极差。
在浏览器中同步执行将会是上面的这个样子, 任务 1 做完才能做任务 2, 任务 2 做完才会做任务 3。这里面体现出同步编程的有序的特点。只能 1, 2,3 不能 1,3,2。但是我们的代码逻辑中可以存在多任务同时执行的过 程。在我们生活中,煮饭和烧水可以同时去做,同样在我们编程中也需要 这样的逻辑。
在计算机中有多线程的概念,什么意思呢,每一个线程做一件事,像下面 任务 1 任务 2 任务 3 这样。
在不同的线程中可以执行不同的任务。 但是我们的 JavaScript 是单线程的,这里的单线程,强调的执行线程是单 线程。后面也是有线程池的,线程以及线程池的概念在这里就不多说了。 想了解的同学可以看看操作系统相关书籍。
JavaScript 语言执行环境是单线程的,单线程在程序执行时,所走的程序 路径按照连续顺序排下来,前面的必须处理好,后面的才会执行。 但是我们也需要类似多线程机制的这种执行方式。但是 JavaScript 还是单 线程的,我们需要异步执行,异步执行会使得多个任务并发执行。 并行与并发。前文提到多线程的任务可以并行执行,而 JavaScript 单线程 异步编程可以实现多任务并发执行,这里有必要说明一下并行与并发的区 别。 并行,指同一时刻内多任务同时进行。边煮饭,边烧水,可以同时进行 并发,指在同一时间段内,多任务同时进行着,但是某一时刻,只有某一 任务执行。边煮饭边烧水,同一时间点只能喝水和吃饭。 接下来说一说异步机制。
并发模型
目前,我们已经知道,JavaScript 执行异步任务时,不需要等待响应返回, 可以继续执行其他任务,而在响应返回时,会得到通知,执行回调或事件 处理程序。那么这一切具体是如何完成的,又以什么规则或顺序运作呢? 接下来我们需要解答这个问题。回调和事件处理程序本质上并无区别,只 是在不同情况下,不同的叫法。 前文已经提到,JavaScript 异步编程使得多个任务可以并发执行,而实现 这一功能的基础是 JavaScript 拥有一个基于事件循环的并发模型。
堆栈与队列
介绍 JavaScript 并发模型之前,先简单介绍堆栈和队列的区别:堆(heap):内存中某一未被阻止的区域,通常存储对象(引用类型);栈(stack):后进先出的顺序存储数据结构,通常存储函数参数和基本类 型值变量(按值访问);队列(queue):先进先出顺序存储数据结构。
事件循环(Event Loop)
JavaScript 引擎负责解析,执行 JavaScript 代码,但它并不能单独运行,通 常都得有一个宿主环境,一般如浏览器或 Node 服务器,前文说到的单线 程是指在这些宿主环境创建单一线程,提供一种机制,调用 JavaScript 引 擎完成多个 JavaScript 代码块的调度,执行(是的,JavaScript 代码都是按 块执行的),这种机制就称为事件循环(Event Loop)。 JavaScript 执行环境中存在的两个结构需要了解:
消息队列(message queue),也叫任务队列(task queue):存储待处理消 息及对应的回调函数或事件处理程序; 执行栈(execution context stack),也可以叫执行上下文栈:JavaScript 执行 栈,顾名思义,是由执行上下文组成,当函数调用时,创建并插入一个执 行上下文,通常称为执行栈帧(frame),存储着函数参数和局部变量, 当该函数执行结束时,弹出该执行栈帧; 注:关于全局代码,由于所有的代码都是在全局上下文执行,所以执行栈 顶总是全局上下文就很容易理解,直到所有代码执行完毕,全局上下文退 出执行栈,栈清空了;也即是全局上下文是第一个入栈,最后一个出栈。
任务:分析事件循环流程前,先阐述两个概念,有助于理解事件循环:同步任务 和异步任务。 任务很好理解,JavaScript 代码执行就是在完成任务,所谓任务就是一个 函数或一个代码块,通常以功能或目的划分,比如完成一次加法计算,完 成一次 ajax 请求;很自然的就分为同步任务和异步任务。同步任务是连续 的,阻塞的;而异步任务则是不连续,非阻塞的,包含异步事件及其回调, 当我们谈及执行异步任务时,通常指执行其回调函数。
事件循环流程 关于事件循环流程分解如下:
宿主环境为 JavaScript 创建线程时,会创建堆(heap)和栈(stack),堆内存储 JavaScript 对象,栈内存储执行上下文;栈内执行上下文的同步任务按序执行,执行完即退栈,而当异步任务执行 时,该异步任务进入等待状态(不入栈),同时通知线程:当触发该事件 时(或该异步操作响应返回时),需向消息队列插入一个事件消息;当事件触发或响应返回时,线程向消息队列插入该事件消息(包含事件及 回调);当栈内同步任务执行完毕后,线程从消息队列取出一个事件消息,其对应 异步任务(函数)入栈,执行回调函数,如果未绑定回调,这个消息会被 丢弃,执行完任务后退栈;当线程空闲(即执行栈清空)时继续拉取消息队列下一轮消息(next tick, 事件循环流转一次称为一次 tick)。
var eventLoop = [];var event;var i = eventLoop.length - 1; // 后进先出while(eventLoop[i]) {event = eventLoop[i--];if (event) {// 事件回调存在event();}// 否则事件消息被丢弃}
这里注意的一点是等待下一个事件消息的过程是同步的。
var ele = document.querySelector('body');function clickCb(event) {console.log('clicked');}function bindEvent(callback) {ele.addEventListener('click', callback);} bindEvent(clickCb);
如上图,当执行栈同步代码块依次执行完直到遇见异步任务时,异步任务 进入等待状态,通知线程,异步事件触发时,往消息队列插入一条事件消 息;而当执行栈后续同步代码执行完后,读取消息队列,得到一条消息, 然后将该消息对应的异步任务入栈,执行回调函数;一次事件循环就完成 了,也即处理了一个异步任务。
扩展:异步编程与多线程的区别
共同点:异步和多线程两者都可以达到避免调用线程阻塞的目的,从而提 高软件的可响应性
不同点:
(1)线程不是一个计算机硬件的功能,而是操作系统提供的一种逻辑功能,线程本质上是进程中一段并发运行的代码,所以线程需要操作 系统投入 CPU 资源来运行和调度。 多线程的优点很明显,线程中的处理程序依然是顺序执行, 符合普通人的思维习惯,所以编程简单。但是多线程的缺点也同样明显, 线 程的使用(滥用)会给系统带来上下文切换的额外负担。并 且线程间的共享变量可能造成死锁的出现
(2)异步操作无须额外的线程负担,并且使用回调的方式进行处理, 在设计良好的情况下,处理函数可以不必使用共享变量(即使无法 完 全不用,最起码可以减少 共享变量的数量),减少了死 锁的可能。当然异步操作也并非完美无暇。编写异步操作的复杂程度较高, 程序 主要使用回调方式进行处理,与普通人的思维方式有些 初入,而且难以调试。
这里有一个疑问。异步操作没有创建新的线程,我们一定会想,比 如有一个文件操作,大量数据从硬盘上读取,若使用单线程的同步操作自 然要等待会很长时间,但是若使用异步操作的话,我们让数据读取异步进 行,二线程在数据读取期间去干其他的事情,我们会想,这怎么行呢,异 步没有创建其他的线程,一个线程去干其他的事情去了,那数据的读取异 步执行是去由谁完成的呢?实际上,本质是这样的。
熟悉电脑硬件的朋友肯定对 DMA 这个词不陌生,硬盘、光驱的技 术规格中都有明确 DMA 的模式指标,其实网卡、声卡、显卡也是有 DMA 功能的。DMA 就是直 接内存访问的意思,也就是说,拥有 DMA 功能的 硬件在和内存进行数据交换的时候可以不消耗 CPU 资源。只要 CPU 在发 起数据传输时发送一个指令,硬件就开 始自己和内存交换数据,在传输 完成之后硬件会触发一个中断来通知操作完成。这些无须消耗 CPU 时间的 I/O 操作正是异步操作的硬件基础。所以即使在 DOS 这样的单进程(而且 无线程概念)系统中也同样可以发起异步的 DMA 操作。
即 CPU 在数据的长时间读取过程中 ,只需要做两件事,第一发布指 令,开始数据交换;第二,交换结束,得到指令,CPU 再进行后续操作。 而中间读取数据漫长的等待过程,CPU 本身就不需要参与,顺序执行就是 我不参与但是我要干等着,效率低下;异步执行就是,我不需要参与那我 就去干其他事情去了,你做完了再通知我就可以了(回调)。
但是你想一下,如果有一些异步操作必须要 CPU 的参与才能完成呢, 即我开始的那个线程是走不开的,这该怎么办呢,在.NET 中,有线程池去 完成,线程池会高效率的开启一个新的线程去完成异步操作,在 python 中这是系统自己去安排的,无需人工干预,这就比自己创建很多的线程更 加高效。
总结:
(1)“多线程”,第一、最大的问题在于线程本身的调度和运行需 要很多时间,因此不建议自己创建太大量的线程;第二、共享资源的调度 比较难,涉及到死锁,上锁等相关的概念。
(2)“异步” ,异步最大的问题在于“回调”,这增加了软件设 计上的难度。 在实际设计时,我们可以将两者结合起来:
(1)当需要执行 I/O 操作时,使用异步操作比使用线程+同步 I/O 操作更合适。I/O 操作不仅包括了直接的文件、网络的读写,还包括数据 库操作、Web Service、HttpRequest 以及.net Remoting 等跨进程的调用。 异步特别适用于大多数 IO 密集型的应用程序。(2)而线程的适用范围则是那种需要长时间 CPU 运算的场合,例如 耗时较长的图形处理和算法执行。但是往 往由于使用线程编程的简单和 符合习惯,所以很多朋友往往会使用线程来执行耗时较长的 I/O 操作。这 样在只有少数几个并发操作的时候还无伤大雅,如果需要处 理大量的并 发操作时就不合适了。
2,回调函数
定义:回调函数的定义非常的简单:当一个函数被当做一个实参传入到另外一个函数(外部函数),并且这个函数在外不函数内被调用,用来完成某些任务的函数,就被称为回调函数
实例:
setTimeout(()=>{console.log("三秒后调用我")},3000)
这段代码中的setTimeout
就是一个消耗时间较长的过程,它的第一个参数是个回调函数
,第二个参数是毫秒数
运行结果是三秒钟后控制台输出"三秒后调用我"
3,回调地狱
如果存在异步任务的代码,不能保证能按照顺序执行,那我们如果非要代码顺序执行呢?
我要输出一句话,语序是这样的:”今天中午12点吃饭,有青椒炒肉,有大米饭“
我们必须要只要操作:
setTimeout(function () {//第一层console.log('今天中午12点吃饭');setTimeout(function () {//第二程console.log('有青椒炒肉');setTimeout(function () {//第三层console.log('有大米饭');}, 1000)}, 2000)}, 3000)
可以看到,代码中的回调函数套回调函数,居然套了3层,这种回调函数中嵌套回调函数的情况就叫做回调地狱。
回调地狱会操作代码的可读性非常差,后期不好维护,于是就引入Promise
4,Promise
Promise 是异步编程的一种解决方案,其实是一个构造函数,自己身上有all、reject、resolve这几个方法,原型上有then、catch等方法
promise对象有以下两个特点
对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
4.1,prmise的语法格式:
new Promise(function (resolve, reject) {// resolve 表示成功的回调// reject 表示失败的回调}).then(function (res) {// 成功的函数}).catch(function (err) {// 失败的函数})
出现new关键字,就明白了Peomise对象其实就是一个构造函数,用来生成Promise实例,这个构造函数接收一个函数作为参数,该函数就是Promise的回调函数,该函数中有两个参数resolve,reject,这两个参数也分别是两个函数
简单的去理解的话resolve
函数的目的是将Promise对象状态变成成功状态
,在异步操作成功时调用,将异步操作的结果,作为参数
传递出去。reject
函数的目的是将Promise对象的状态变成失败状态
,在异步操作失败时调用,并将异步操作报出的错误,作为参数
传递出去。
Promise实例生成以后,可以用then方法
分别指定resolved状态
和rejected状态
的回调函数。
const promise = new Promise((resolve,reject)=>{//异步代码setTimeout(()=>{// resolve(['111','222','333'])reject('error')},2000)})promise.then((res)=>{//兑现承诺,这个函数被执行console.log('success',res);}).catch((err)=>{//拒绝承诺,这个函数就会被执行console.log('fail',err);})
上面说Promise构造函数中传入一个参数是函数,然后这个函数内的两个参数又是两个函数(reslove
,reject
),是不是感到很绕,可以看下面一种写法
let p = new Promise(function(resolve, reject){//做一些异步操作setTimeout(function(){console.log('执行完成Promise');// resolve('要返回的数据可以任何数据例如接口返回数据');reject('失败结果')}, 2000);}).then(function(data){console.log(data)},function(data){console.log(data)});
其实then方法中就可以把上面需要的两个函数(reslove,reject)传入进去
看下面可以快速理解
这两个函数分别做为参数(reslove
和reject
)传到上方的函数中去啦
4.2,Promise链式
then方法返回的是一个新的Promise实例(注意不是原来那个Promise实例),因此可以采用链式写法,即then方法后面再调用另一个then方法
let p = new Promise(function(resolve, reject){//做一些异步操作setTimeout(function(){console.log('执行完成Promise');resolve('要返回的数据可以任何数据例如接口返回数据');reject('失败结果')}, 2000);}).then(function(data){console.log(data)return new Promise(function(resolve,reject){resolve("我是第二个回调")})}).then(function(data){console.log(data)});
4.3,Promise.all()
Promise.all()
方法用于将多个 Promise 实例,包装成一个新的 Promise 实例
。
语法格式:
const p=Promise.all([p1,p2,p3]);
Promise.all()
方法接受一个数组
作为参数,p1、p2、p3都是 Promise 实例
,如果不是,就会调用Promise.reslove() [该方法可自行了解]
自动将参数转为 Promise 实例
案例:
function promiseClick1(){let p = new Promise(function(resolve, reject){setTimeout(function(){var num = Math.ceil(Math.random()*20); //生成1-10的随机数console.log('随机数生成的值:',num)if(num<=10){resolve(num);}else{reject('数字太于10了即将执行失败回调');}}, 2000);})return p}function promiseClick2(){let p = new Promise(function(resolve, reject){setTimeout(function(){var num = Math.ceil(Math.random()*20); //生成1-10的随机数console.log('随机数生成的值:',num)if(num<=10){resolve(num);}else{reject('数字太于10了即将执行失败回调');}}, 2000);})return p}function promiseClick3(){let p = new Promise(function(resolve, reject){setTimeout(function(){var num = Math.ceil(Math.random()*20); //生成1-10的随机数console.log('随机数生成的值:',num)if(num<=10){resolve(num);}else{reject('数字太于10了即将执行失败回调');}}, 2000);})return p}Promise.all([promiseClick3(), promiseClick2(), promiseClick1()]).then(function(results){console.log(results);});js