异步编程简介:
介绍:异步编程是一种编程范式,旨在提高程序的性能和响应能力。该模型允许程序在执行某些任务时,不必等待这些任务完成就可以进行下一步操作,从而提高了程序的效率。
作用:异步编程通常用于涉及网络请求、文件读写、数据库查询等I/O密集型操作,以及高计算量的任务。在传统的同步编程中,这些操作会阻塞程序的执行,导致程序变慢或失去响应。而使用异步编程可以使这些I/O操作并行执行,从而提高程序的性能和响应能力。
并发 Concurrency、 并行 Parallelism、异步 Asynchronous、同步 Synchronous 的区别
(1)并发 Sancufrericy :代表计算机能够同时执行多项任务
计算机怎么做到并发 Concurrency?
(1)对于单核处理器,计算机可以通过分配时间片的方式——让一个任务运行一段时间,然后切换另外一个任务,再运行一段时间,不同的任务会这样交替往复的一直执行下去。——>这个过程也被称作是进程或者线程的上下文切换(context switching)
(2)对于多核处理器,可以在不同的核心上真正并行地执行任务,而不用通过分配时间片的方式运行——> 并行(parallelism)
同步和异步是两种不同的编程模型
(1)"同步” Synchronous 代表需要等到必须前一个任务执行完毕之后,才能进行下一个任务。因此在同步中并没有并发或者并行的概念。
(2)异步 Asynchronous 则代表不同的任务之间并不会相互等待、先后执行,即执行任务A时,也可以同时执行任务B
一个典型实现异步的方式则是通过多线程编程
多线程 Multithreoding : 创建多个线程并且启动他们,在多核的环境下,每个线程就会被分配到独立的核心上运行,实现真正的并行。
如果使用单核心处理器,或者通过设置亲和力(AfFnty) 强制将线程绑定到某个核心上
操作系统则会通过分配时间片的方式来执行这些线程,不过这些线程依然是在“并发”地执行Javascript 本身是没有多线程的概念的,不过通过它的函数回调(function callback)机制,依然能够做到单线程的“并发”。
例:通过 fetch() 函数 同时访问多个网络资源
⭕注意:
虽然主程序和回调函数看起来是同时进行的,但它们依然是运行在同一个线程中。
多线程编程、单线程的异步编程如何选择
(1)单线程的异步编程:对于I / O 密集的应用程序,比如 web 应用就会经常执行网络操作,数据库访问,这类应用就非常适合使用异步编程的方式。
如果是使用多线程的方式,则可能浪费系统资源(如下图)
因为每个线程的绝大多数时间都是在等待这些 I / O 操作,而线程自身会占用额外的内存(线程内存开销,线程切换开销还有线程资源竞争问题)
(2)多线程编程:
多线程编程则非常适合于计算量密集的应用,如视频图像处理,科学计算等。它能够让每一个 CPU 核心发挥最大的功效,而不是消耗在空闲的等待上。
JavaScript 中有两种实现异步的方式
(1)回调函数 Callback Function
可以使用 setTimeout() 让一个函数在指定的时间后执行,这个函数本身会立刻返回,程序紧接着会执行之后的代码,而传入的回调函数则会等到预定的时间才会执行
⭕注意:
- 这里的()=>是箭头表达式,相当于函数定义的简化写法
- Javascript 从设计之初就是一个单线程的编程语言
即便看上去这里的回调函数和主程序在并发执行,但它们都运行在同一个主线程中。实际上主线程中还运行了我们写的其它代码,包括界面逻辑、网络请求、数据处理等等等等。虽然只有单个线程在执行,但这种单线程的异步编程方式其实有诸多优点。
由于所有操作都运行在同一个线程中,因此我们无须考虑线程同步或者资源竞争的问题,并且从源头上避免了线程之间的频繁切换。从而降低线程自身的开销。
缺点:
回调函数虽然简单好理解,但有一个明显的缺点:需要依次执行多个异步操作,会变成回调地狱 Callback Hell ——整个程序会一层接着一层的嵌套下去,可读性会非常差。
(2)Promise
Promise 就是为了解决这个问题——请求会在未来某个时刻返回数据,随后可以调用它的然后方法并传递一个回调函数。
Promise的API中的fetch就是很好的例子:
fetch 用来发起一个请求来获取服务器数据,可以用它动态更新页面的内容(即AJAX 技术 Asynchronous JavaScript and XML)
如果请求成功完成,则回调函数会被调起,请求的结果也会以参数的形式传递进来
而且 Promise的优点在于它可以用一种链式结构将多个异步操作串联起来!——链式调用 Chaining
response.json() 方法 也会返回一个 Promise,代表在未来某个时刻,将返回的数据转换成 JSON 格式。
如果想要等到它完成之后再执行其它的操作,可以在后面追加一个 then,然后执行接下来的代码。
Promise的链式调用避免了代码的层层嵌套,即便有很长的链,代码也不过是向下方增长而并非向右,因此可读性会提升不少。
使用异步操作也会遇到各种错误:各种网络问题或者返回的数据格式不正确等等
错误处理 Error Handling
如果想捕获这些错误,最简单的方法是附加一个 catch 在链式结构的末尾。
如果之前任意一个阶段发生了错误,那么将触发 catch ,之后的 then 将不会执行。这和同步编程中用到的 try/catch 块很类似。
类似的Promise还提供finally 方法,会在Promise 链结束之后调用。无论失败与否,都可以在这里做清理工作。
例如:如果我们用到了加载动画,则可以在这里关闭他。
新标准ECMA17 中加入的两个关键字 async、await
它们是基于 Promise之上的一个语法糖,使异步操作更加的简单。
首先我们需要使用 async 关键字将函数标记为异步函数。
异步函数就是指返回值为 Promise 对象的函数,比如之前用到的 fetch() 就是一个异步函数。
在异步函数中我们可以调用其它的异步函数,不过不再需要使用 then(),而是使用一个更加简洁的 await 语法。await 会等待 Promise 完成之后直接返回最终的结果。所以这里的 response 已经是服务器返回的响应数据了。
⭕注意:await 虽然看上去会暂停函数的执行,但在等待的过程中,JavaScript 同样可以处理其它的任务,比如说更新界面,运行其他的代码等等。因为 await 底层是基于 Promise 和事件循环机制实现的。
await 使用时的陷阱
(1)分别去 await 这两个异步操作,虽然不存在逻辑错误,但这样写会打破这两个 fetch() 操作的并行。因为会等到第一个任务执行完成之后才开始执行第二个任务。
因此更高效的做法是将所有 Promise 用 Promise.all 组合起来。
(2)如果需要在循环中执行异步操作,是不能够直接调用 forEach 或者 map 这一类方法的,尽管我们在回调函数中写了 await,但这里的 forEach 会立刻返回,它并不会暂停等到所有异步操作都执行完毕
如果我们希望等待循环中的异步操作都一一完成之后才继续执行,那我们还是应当使用传统的 for 循环。
更进一步,如果我们想要循环中的所有操作都并发执行 ——> for await
这里的 for 循环依然会等到所有的异步操作都完成之后才继续向后执行 。
(3)不能在全局或者普通函数中直接使用 await 关键字,await 只能被用在异步函数(asyncfunction)中。
如果我们想在最外层中使用 await,那么需要先定义一个异步函数,然后在函数体中使用它。
使用async和await 可以让我们写出更清晰、更容易理解的异步代码,因此我们几乎不再需要使用底层的Promise 对象
包括调用它的then() ,catch()函数等等。
即便是对于某些旧版本的浏览器(例如IE)不支持async语法 ,我们还是可以使用转译器将它们编译成旧版本也兼容的等效代码。