异步JavaScript
🌎 参考:MDN-异步 JavaScript,《深入理解 ES6》- NICHOLAS C.ZAKAS,黑马程序员,王磊同学
同步和异步
同步
按顺序等待执行,代码传入调用栈,执行完毕后再从调用栈中移除。
1 | console.log("global begin"); |
输出结果为:
1 | global begin |
异步
📖 JavaScript 引擎是基于单线程事件循环的概念构建的,在同一时间只允许一个代码块在运行。如果一个函数依赖于另一个函数的结果,它只能等待那个函数结束才能继续执行,这样就容易造成代码阻塞。异步就是用于解决这些问题。
当前任务为异步的话,不会等待当前任务执行结束,而是立即执行下一个任务
在 JavaScript 中实现异步的方法有许多种:setTimeout()
和setInterval()
、事件模型、回调函数、Promise、Fetch、async/await、axios
setTimeout 作为例子
1 | console.log("global begin"); |
分为四个部分:调用栈,任务队列,事件循环,api 环境(浏览器、nodejs)
- 同步代码放进
调用栈
- 遇到计时器,将计时器放进
api环境
,计时器开始计时(异步操作,不影响第3
的进行) - 遇到同步代码,继续放进
调用栈
- 计时器时间到,将计时器(计时器中的回调函数)放进
任务队列
事件循环
检查调用栈
是否为空,不为空则继续执行调用栈
中的代码调用栈
为空,事件循环
检查任务队列
是否为空,不为空则将任务队列
中的代码放进调用栈
JavaScript 是单线程,但是运行它的环境并不是单线程。具体要看运行环境提供的 API 到底是同步还是异步
回调函数
被作为实参传入另一函数,并在该外部函数内被调用,用以来完成某些任务的函数,称为回调函数。
提一下事件模型(onclick、onmouseover 等等),其实和回调函数是类似的,比如按钮点击,代码都是在按钮点击的时候执行。不同的是回调函数中被执行的函数不是一赋值的形式传递(=),而是作为参数传入。
回调函数在Node.js
中广泛应用,所以下面的例子中
1 | readFile("example.txt", (err, contents) => { |
readFile()
会开始执行,读取参数 1 中指定的文件,读取结束后执行参数 2(回调函数),但是读取文件可以说是一个阻塞的过程,所以浏览器是先输出**Hi!**,然后当readFile()
执行结束的时候,会在任务队列末尾添加一个新任务,用于处理回调函数里面的内容。
回调函数是一个很好的异步操作,回调函数嵌套多的时候,就会造成回调地狱。如果想实现复杂的功能,这样的代码很难理解其意思。
1 | readFile("example.txt", (err, contents) => { |
Promise
- CommonJS 率先提出,ES6 标准化
promise 承诺在未来的某一时间会返回执行的内容,但是不确定在什么时候,不论执行的结果是对的还是错的都会有返回。
实例
使用 Promise()构造函数创建自己的 promise,这个构造函数接收一个参数,这个参数是一个执行器函数,这个执行器函数有两个参数,这两个参数是两个函数,这两个函数分别是:处理成功执行的 resolve()、处理失败执行的 reject()
1 | let promise = new Promise(function (resolve, reject) { |
上面代码依次输出你好,世界!
;
- 首先因为Promise 的执行器中的代码会立刻执行,然后再执行其他的代码
- 调用
resolve()
后触发一个异步操作,传入then
和catch()
的函数会被添加到任务队列中并异步执行 - 虽然上面的
then()
在console.log(',')
之前,但是与执行器不同的是并没有立即执行,这是因为完成处理程序和拒绝处理程序总是在执行器完成后被添加到任务队列的末尾。
promise 中可以使用 promise 的 原形 和 _静态方法_。👉 更多
promise.prototype.then()
:处理成功执行
promise.prototype.catch()
:处理失败执行
promise.prototype.finally()
:不管是成功还是失败都执行
promise.then(),接收两个参数(可选),这两个参数是处理函数,参数 1 是成功执行的处理函数,参数 2 是失败的处理函数,所以then(null, func) 和 catch()
的作用是一样的
1 | promise.then((value) => { |
- Promise 对象的
then()
方法返回一个新的 Promise 实例,所以可以链式调用 - 后面的
then()
就是为上一个then()
的返回的 Promise 添加处理函数 - 前面的
then()
返回的值会作为后面then()
的参数- 如果这个值是一个 Promise 实例,那么后面的
then()
会等待这个 Promise 实例执行完毕
- 如果这个值是一个 Promise 实例,那么后面的
▶️ 使用第二个参数作为catch()
而不是使用链式catch()
,区别是第二个参数只捕获当前的错误,而不是整条链的。有利有弊:
- 优点:可以在链中的任何位置处理错误,或者说并且容易找出错误发生的位置
- 缺点:如果在链中的多个位置都需要处理错误,就需要多次调用
catch()
,这样就会造成代码冗余
除了使用链条最后的catch()
捕获整条链的错误,还可以使用unhandledrejection
事件捕获整条链的错误
web环境中(全小写命名)
1 | window.addEventListener("unhandledrejection", (event) => { |
node环境中(驼峰式命名)
1 | process.on("unhandledRejection", (reason, promise) => { |
例子
加载图片
1 | function loadImage(url) { |
静态方法
Promise.resolve()
:返回一个通过的 promise
😅 如果里面传入了一个对象,里面又刚好有then()
方法,那么这个对象就会被当作一个 promise 实例,然后返回这个对象(thenable)
- Promise 为普及的时候,其他库可能会有自己的 promise 实现,这些实现可能不会遵循 Promise/A+ 规范,但是会有
then()
方法,这样就可以使用Promise.resolve()
将其转换为 Promise 实例
Promise.reject()
:返回一个拒绝的 promise
🎇 以下静态方法参数接收一个 数组 作为参数,数组里面的是 promise 实例(Array<Promise>
)
- 返回值是一个 promise 实例
Promise.all()
:只要有一个拒绝,就是拒绝。
Promise.race()
:只要有一个通过,就是通过
Promise.allSettled()
:不管是拒绝还是通过,都会执行
例子
ajax 请求超时
1 | const request = ajax("api/xxx"); |
node.js 中的读取文件
1 | let fs = require("fs"); |
async 和 await
对比生成器,
*
去掉,换成async
,yield
换成await
,不需要执行器函数,返回的是一个 promise 实例
async
和await
关键字让我们可以用一种更简洁的方式写出基于Promise
的异步行为,而无需刻意地链式调用promise
。
async
使用 async
关键字,把它放在函数声明之前,使其成为 async function
- async 函数是使用
async
关键字声明的函数,其中允许使用await
关键字。
将 async
关键字加到函数申明中,可以告诉它们返回的是 promise,而不是直接返回值。
await
- 原生 Promise,等待
- thenable 对象,构造成新的 Promise
- 不是 thenable 对象,包装成
Promise.resolve()
await 只在异步函数里面才起作用。它可以放在任何异步的,基于 promise 的函数之前。
nodejs 14.8.0
es 模块支持顶级await
; 浏览器环境,也是在模块顶级可用
它会暂停代码在该行上,直到 promise 完成,然后返回结果值。在暂停的同时,其他正在等待执行的代码就有机会执行了。(这样的异步代码看起来像同步代码)
例子
将上面的 promise 改为 async 模式
1 | async function load(url) { |
结合 axios 使用
1 | // 1. async 基础用法 |
宏任务和微任务
宏任务,重新回到任务队列的末尾,等待下一次事件循环;而微任务,会在当前任务执行结束后立即执行。
生成器
使用
知识点:yeild
关键字,generator.next()
,gernerator.throw()
,generator.return()
1 | function* foo() { |
在里面添加yield
关键字,可以让函数暂停执行,然后返回一个值
- 这个值是一个对象,包含两个属性:
value
和done
,value
是yield
后面的值,done
是一个布尔值,表示是否执行完毕。 - 和
return
不同,yield
不会立即结束函数的执行,而是返回一个值,然后暂停执行,等到下一次调用next()
的时候,再继续执行。
1 | function* foo() { |
如果调用next()
的时候传入了参数,那么这个参数会作为上一次yield
的返回值
1 | function* foo() { |
如果调用gernerator.throw()
,会抛出一个错误,这个错误在生成器函数内部抛出,可使用try...catch
捕获
管理异步
1 | function* main() { |
改为递归的方式
1 | function* main() { |
封装成执行器
1 | function run(generator) { |
有一个专门的库可以实现这个功能:co
不过后来出现了async
和await
,所以这个库用得就少了。
1 |
异步JavaScript