JavaScript中的异步
你好,我是麦小子。
正如标题所讲,今天咱们要讨论的点是异步。
如果你对"异步"这个概念没有概念的话——正好,咱们从头开始。
生活中的异步
有个很有意思的问题,来自童年一部不错的动画片。
洗衣服(洗衣机)需要50min
烧水需要10min
吃饭需要20min
请问,做完这三件事一共需要几分钟?
如果你不加思考,张口就来。会不会回答80min呢?我想这是有可能的。因为当年我也是这么回答的,这个回答有错吗?显然是正确的,却<font color='carmine'>不合逻辑</font>。事实上,要做完以上三件事在最理想的状态下仅需50min.
80min和50min
两个答案都是正确的,不同的是思考方式或者生活方式。
前者需等某一件事做完才会去做第二件,后者在洗衣机工作时就已经开始烧水和吃饭了,做完这两件事甚至衣服还没洗完呢。
我们给这两种不同的方式以不同的名字
前者被称为同步
后者被称为<font color='carmine'>异步</font>
看起来,异步让小伙子显得更机灵。
JS中的异步
一颗栗子
吃完上面的栗子~下面来打另外一颗。这次咱们引入一段JS代码。
1console.log('hello'); 2setTimeout(() => { console.log('setTimeout') }, 2000); 3console.log('JavaScript');
它的打印结果可能会出乎意料
hello
JavaScript
setTimeout
原因很简单,setTimeout()函数是一个<font color='red'>异步函数</font>。
我们知道,作为一个定时器设置函数。setTimeout()可以让程序在一段时间后调用某个函数(或执行某段代码)。正如前文所表,我们设置了2000毫秒之后打印 setTimeout 。
接下来,我们把浏览器想象成一个小伙子。
首先,他看到第一行代码,指令是打印一个 hello ,于是他往控制台打印一个 hello
然后,他看到第二行代码,创建了一个定时器,于是他就在那等着,等时间到了就去执行代码。可是转念一想,老这么等着也不是个事,干脆,我先看看后面还有指令不。
接着,他暂时离开了定时器,发现了第三行代码,指令是打印一个 JavaScript ,于是他往控制台打印一个 JavaScript
接下来,他发现在此之后已经没有可执行的代码,猛然想起,还有一个定时器呢~得回去看看。当他再一次遇到那个定时器,发现时间还没到。
于是,他就又等了一段时间。
后来,时间总算到了。他终于可以执行定时器里的代码了,指令是打印一个 setTimeout 于是它打印了一个 setTimeout
吃完上面的栗子,相信你对JS异步已经有了轮廓。
回调函数
很久以前,JavaScript 仅支持回调函数来实现异步操作。那么,什么是回调函数?
回调函数是一个被传递到另一个函数中的会在适当的时候被调用的函数,如
1const callback = function(){ 2 console.log('this is a callback function'); 3} 4const test = function(callback){ 5 callback(); 6} 7test(callback);
异步返回值
某些情况下,setTimeout 会返回一个值,我们应该如何使用这个返回值?return 吗?
咱们打颗栗子
1const countNum = function (value) { 2 setTimeout(() => { 3 value *= 2; 4 }, 2000); 5 return value; 6}; 7console.log(countNum(2));
- 查看一下打印结果
好想获取异步返回值呀==>2
这样呢?
1const countNum = function (value) { 2 setTimeout(() => { 3 return value *= 2; 4 }, 2000); 5}; 6console.log(`好想获取异步返回值呀==>${countNum(2)}`);
抑或是这样?
1const countNum = function (value) { 2 return setTimeout(() => { 3 value *= 2; 4 }, 2000); 5}; 6console.log(`好想获取异步返回值呀==>${countNum(2)}`);
事实上,以上代码打印的结果都是不符合我们预想的。
试试看回调吧~
1const countNum = function (value, callback) { 2 value *= 2; 3 setTimeout(() => { 4 callback(value); 5 }, 2000); 6 console.log(`先打印一次计算结果=>${value}`); 7}; 8function printResult(value) { 9 console.log(`回调打印计算结果是=>${value}`); 10} 11countNum(2, printResult);
查看一下打印结果
先打印一次计算结果=>4
回调打印计算结果是=>4
显然,回调函数可以实现我们的需求。
失败处理
在异步函数中使用成功与失败的不同回调
1const workout = function(value,success,fail){ 2 setTimeout(() => { 3 try { 4 if (typeof value !== 'number') { 5 throw '欸嘿~怎么不是数字呀~~'; 6 } 7 success(value * 200); 8 } catch (exception) { 9 fail(value); 10 } 11 }, 1000); 12} 13const success = function(value){ 14 console.log(`算出结果了=>${value}`); 15} 16const fail = function(value){ 17 console.log(`${value}不是数字,算不了啊~~`); 18}
回调的嵌套
1function double(value,callback){ 2 setTimeout(() => { 3 callback(value*2); 4 }, 1000); 5} 6function doubleCallback(value){ 7 double(value,(result)=>console.log(`打印下结果=>${result}`)) 8} 9double(1,doubleCallback);
仔细看下上面的代码,或许你会觉得可读性并不高,事实也正是如此。当逻辑更加复杂,回调的嵌套会更多。比如,MDN的厄运金字塔实例
期约
期约 是 promise 的中文翻译。当然,你可以把它翻译成 约定 。需要保证的是,大家理解你的说法。
如果可能的话,我更愿意把它翻译成老板,他会告诉你将来会给你一个结果,却不知道什么时候给到,甚至也不确认的这结果的好坏。
比较好的应对方案是, 不给 rejectionFunc。
初见 Promise
Promise 是 ES6 新增的引用类型。可通过 new 进行实例化
1const pro = new Promise(()=>{});
打印一下 pro ,但见
Promise { <pending> }
以上的 pending 是 Promise 的一种状态——待定
一个 Promise 会处于以下三种状态之一
待定
- pending
兑现
- fulfilled OR 解决 -- resolved
拒绝
- rejected
pending 是 Promise 的初始状态,它可以 settled 为另一种状态,或者是 fulfilled 或者是 rejected。这种 settled 操作是不可逆的。
理解状态
-
pending -- 尚未开始或正在执行
-
fulfilled -- 已经完成
-
rejected -- 没有成功
当 Promise 状态转换为 fulfilled,会生成一个解决值 value ;
当 Promise 状态转换为 rejected,会生成一个拒绝理由 reason 。
二者可选,默认值为 undefined
关于构造
-
Promise 构造接收一个执行器函数 executor
-
executor 长这样
1function(resolutionFunc, rejectionFunc){ 2 // 通常是一些异步操作 3}
- resolutionFunc 和 rejectionFunc 长这样
1resolutionFunc(value) // 当被敲定时调用 2rejectionFunc(reason) // 当被拒绝时调用 3// 二者参数可为任意类型
可以通过调用 resolutionFunc 和 rejectionFunc 为 Promise 切换状态
调用 resolutionFunc 可以切换到 fulfilled ,反之切换到 rejected
1const one = new Promise((resolve,reject)=>resolve()) 2const two = new Promise((resolve,reject)=>reject()) 3console.log('one的状态是',one); 4console.log('two的状态是',two);

实例化非待定状态的 promise
-
Promise.resolve()
-
Promise.rejuect()
二者均可以接收一个参数,前者表示成功的结果,后者表示拒绝的理由
Promise 实例方法
-
Promise.prototype.then()
-
Promise.prototype.catch()
-
Promise.prototype.finally()
-
then()
可最多接收两个函数作为参数。
第一个参数处理兑现状态,第二个参数处理拒绝状态
返回一个新 Promise 实例
根据 then 的回调返回值决定 新Promise 的状态
-
返回fulfilled的promise,返回一个值和不返回值。新promise状态为fulfilled
-
返回rejected的promise,抛出错误。新promise状态为rejected
-
返回pending的promise。新promise状态为pending
1const excutor = function (resolve, reject) { 2 setTimeout(reject, 2000); 3}; 4const pro = new Promise(excutor); 5const newPro = pro.then( 6 () => { 7 console.log('resolve'); 8 }, 9 () => { 10 console.log('reject'); 11 }, 12); 13console.log(newPro); -
-
catch()
为 promise 添加拒绝处理程序
接收一个参数,即
promise.catch(onReject)等价于promise.then(null,onReject) -
finally()
<font color='red'>非重入</font>
non-reentrancy
一句话结论:
<font color='carmine'>promise 转换状态,处理程序被排期而非立即执行。</font>
换句话说就是,
<font color='carmine'>调用 then() 会立即把程序推入消息队列。</font>等到之后的同步代码全部执行完毕再来执行之前被排期的部分,会按照被排期的顺序执行即先排期先执行。
打一颗栗子:
1console.log('1.程序开始执行了~'); 2const pro = new Promise((resolve,reject)=>{ 3 console.log('2.Promise执行函数resolve之前'); 4 resolve(); 5 console.log('3.Promise执行函数resolve之后'); 6}) 7const newPro = pro.then(()=>console.log('5.pro--then')); 8newPro.then(()=>console.log('6.newPro--then')); 9console.log('4.同步代码结束了~');
11.程序开始执行了~ 22.Promise执行函数resolve之前 33.Promise执行函数resolve之后 44.同步代码结束了~ 55.pro--then 66.newPro--then
连锁与合成
- promise 的连锁也就是链式调用
由于 Promise 的实例方法都会返回新的 promise 实例,所以我们可以一直调用 实例方法
1pro.then(()=>console.log('5.pro--then')) 2.then(()=>console.log('6.newPro--then'));
- Promise.all() 和 Promise.race()
-
all()
接收一个可迭代对象作为参数,返回一个新 promise 实例
关于返回值:
0. 参数为空或参数不含 promise ,则返回的 promise 为 fulfilled
1. 其他情况,返回的 promise 为 pending。改状态会根据参数的 promise 状态异步变成 fulfilled 或 rejected。(全部完成转为前者,有一个失败转为后者)
promise.all返回的promise的完成状态的结果是一个数组,它包含所有的传入迭代参数对象的值(也包括非promise值) -
race()
接收一个可迭代对象作为参数,返回一个新 promise 实例
关于返回值:
0. 参数集合中最先解决或拒绝的 promise 的镜像
这俩方法本身很简单,这里不再赘述,值得一提是。
call() 和 race() 都具有异步性。
<font color='red'>当且仅当</font>传入的可迭代对象为空时为同步。
异步函数
即 async/await 为 ES8 新增
async
用于声明异步函数
异步函数始终返回一个 promise 对象
它的返回值:
可以是常规值或特别对象(实现thenable接口的对象)
若是前者情况,则被当作被解决的 promise
后者,则由 then() 的处理程序解包
打颗栗子:
1async function one(){ 2 return Promise.resolve('one'); 3} 4async function two(){ 5 return 'two' 6} 7console.log(one()); 8console.log(two());

await
await 后跟一个常规值或特别对象(实现thenable接口的对象)
若是前者情况,则被当作被解决的 promise
或者,则由 await 解包
1async function one(){ 2 const p = new Promise((resolve,reject)=>resolve(3)) 3 console.log(await p); 4} 5async function two(){ 6 console.log(await "await"); 7} 8one() 9two()

注意事项
await 必须在异步函数中使用
await 只能直接出现在异步函数的定义中
停止和恢复执行
一颗栗子:
1async function one(){ 2 console.log(await Promise.resolve('1')); 3} 4async function two(){ 5 console.log(await '2'); 6} 7async function three(){ 8 console.log('3'); 9} 10one() 11two() 12three()
13 21 32
另一颗栗子:
1async function foo() { 2 console.log(2); 3 console.log(await Promise.resolve(6)); 4 console.log(7); 5} 6async function bar(){ 7 console.log(4); 8 console.log(await 8); 9 console.log(9); 10} 11console.log(1); 12foo(); 13console.log(3); 14bar(); 15console.log(5);
11 22 33 44 55 66 77 88 99
值得一提的是,曾经这段代码的打印结果是
1
2
3
4
5
6
7
8
9
后来,TC39 对 await 后面 promise 的情况做了修改。修改之后栗子中的
Promise.resolve(6)只生成一个异步任务,较之前更简单一点。
异步函数策略
实现 sleep()
1async function sleep(delay){ 2 return new Promise((resolve)=>setTimeout(resolve,delay)); 3} 4async function test(){ 5 const t0 = Date.now(); 6 await sleep(2000); 7 console.log(Date.now()-t0); 8} 9test();
值得一提的是,打印的结果是会大于 2000 的。
至于为什么,我想咱们后面可以讨论一番。
好了,今天就写到这里吧,关于异步函数和约定,MDN有更多知识。
