# 前言
异步是 JavaScript 里一个非常重要的基本功。因为 js 是单线程的,为了提高 CPU 利用率,异步是必要的,否则的话要是某一个任务耗时非常长(比如 IO 设备读写),后一个任务就不得不等待很久,浪费了处理器的能力。
# 异步方案一:回调
前面说到,异步其实是让一些非常耗时的任务释放 CPU 的占用,去让下一个任务先执行,但我们怎样才能知道那个耗时的任务执行完没有呢?这就要通过回调函数来通知了,回调函数是我们给这个任务的参数,当这个任务结束时,它会调用这个回调函数。
形象的比喻就是,有一个场景,我放衣服进洗衣机去洗,这期间我不用一直看着洗衣机看它洗完衣服没有,而是可以去忙自己的事情,洗衣机洗完就会自动发出 “哔哔哔” 的声音提醒我们它洗完了,要来处理衣服了,等我们腾出手来就可以去处理洗完的衣服了。这个声音,就是回调函数的形象比喻。
来看这么一个需求:
假设有一个红绿灯,其中红灯 3 秒亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次,如何让三个等不断交替重复亮呢?
// 已知三个亮灯函数已经存在,如下 | |
function red(){ | |
console.log('red'); | |
} | |
function green(){ | |
console.log('green'); | |
} | |
function yellow(){ | |
console.log('yellow'); | |
} |
如果用回调方案的话,我们写出来的代码应该是这样的。
/** | |
* @params {number} time 延迟时间 | |
* @params {string} light | |
* @params {function} callback | |
*/ | |
const task = (time,light,callback) => { | |
setTimeout(() => { | |
if(light === 'red'){ | |
red(); | |
} else if(light === 'green'){ | |
green(); | |
} else if(light === 'yellow'){ | |
yellow(); | |
} | |
callback();// 结束调用回调函数 | |
},time); | |
} |
每一个任务函数接收一个延迟时间、一个灯色、以及结束后调用的回调函数。
然后这样调用:
const step = () => { | |
task(3000,'red',()=> { | |
task(1000,'green',()=> { | |
task(2000,'yellow',step); | |
}); | |
}); | |
} | |
step(); |
但很明显,上面回调的写法会造成一种现象叫做 “回调地狱”。当我们有许多任务有关联的时候,它嵌套的写法不仅使得代码不美观,更重要的是,这样的代码非常难以理解和维护。
# 异步方案二:Promise
# Promise 是什么?
Promise 是一个对象,保存着未来将要结束的事件。它主要用来进行异步计算,可以将异步操作队列化。
# Promise 特征
Promise 对象是由构造函数构造出来的一个对象,每一个 Promise 对象代表着一个异步操作。
let promise1 = new Promise((resolve,reject) => { | |
// 异步操作,成功后调用 resolve (),失败后调用 reject () | |
}); | |
promise1.then((response) => { | |
// 成功后的回调函数操作 | |
}); | |
promise1.catch((error)=>{ | |
// 失败后的回调函数操作 | |
}); | |
// 也可以选择将传给 catch 的函数作为 then 方法的第二个参数,只是写法不同。如下 | |
promise1.then((response)=>{ | |
// 成功后,fulfill 的操作 | |
},(error)=> { | |
// 失败后 | |
}) |
promise 构造函数的执行是同步的,then 和 catch 才是异步的。
Promise 对象由三个状态,从上述代码也可看出一二。分别是 pending (进行中),fulfill (已成功),rejected (已失败)。
一旦 promise 的状态改变,状态就凝固了,不会再改变。也就是说,promise 状态改变只有两种可能,从 pending 改成 fulfill 或者从 pending 改成 rejected。
promise 状态一旦改变,就会触发 then () 中相应的函数进行处理。
# Promise.resolve
promise 的 resolve 方法用于将 promise 对象从 pending 状态改成 fulfill 状态。
它可以接受参数,根据参数的类型进行处理并传递给 then 中相应处理的方法。
# 参数是一个 promise 对象
Promise.resolve 将不做任何修改,直接传递这个实例
# 参数是一个 thenable 的对象
Promise.resolve 会将该对象转化为一个 promise 对象,并立即执行这个对象的 then 方法。
# 参数不是 promise 对象,也不是 thenable 对象
比如传递一个字符串,Promise.resolve 会返回一个新的、状态为 resolved 的对象传递过去
const p = Promise.resolve('hello'); | |
p.then((s)=>{ | |
console.log(s); | |
}); | |
//hello |
# 不带任何参数
直接返回一个 resolved 状态的 promise 对象,而且 then () 函数内不返回值或者返回的不是 promise,那么 then 会自动返回一个 resolve 状态的 promise
# promise 的链式调用
promise 的 then 方法也会返回一个 promise 对象,该对象行为也是一样的。
如果我们有一些异步任务是相互关联的,比如读完任务一后去读任务二,再读任务三,就可以使用 promise 的链式调用。
// 假设我们有一个工具函数 ajax (url,callback); | |
function request(url){ | |
return new Promise((resolve,reject) => { | |
ajax(url,resolve); | |
}); | |
} | |
request('xxx').then((response)=>request('xxx'+response)).then((response)=>{console.log(response2)}); |
这样就能把它们的回调嵌套调用改成链式调用,代码变美观了许多,而且更易读了。
# Promise 改写红绿灯
先是红灯三秒一次,设置之后改变 promise 状态为 fulfill,调用 then 中对应的方法,同时 then 方法会自动将返回值封装为一个 promise 对象,供下次链式调用。
const task = (time,light) => new Promise((resolve,reject)=>{ | |
setTimeout(() => { | |
if(light === 'red'){ | |
red(); | |
} else if(light === 'green'){ | |
green(); | |
} else if(light === 'yellow'){ | |
yellow(); | |
} | |
resolve(); | |
},time); | |
}); | |
const step = () =>{ | |
task(3000,'red').then(() => task(1000,'green')).then(()=>task(2000,'yellow')); | |
} |
# Promise 相关重点 API
# Promise.all
Promise.all ([p1, p2, p3]) 用于将多个 promise 实例,包装成一个新的 Promise 实例,返回的实例就是普通的 promise。
它接收一个数组作为参数
数组里可以是 Promise 对象,也可以是别的值,只有 Promise 会等待状态改变
当所有的子 Promise 都完成,该 Promise 完成,返回值是全部值的数组(顺序和 all 方法内参数顺序一致)
有任何一个失败,该 Promise 失败,返回值是第一个失败的子 Promise 结果。
# Promise.race
Promise.race 类似于 Promise.all 方法,区别在于它有任意一个完成就算完成,
当 iterable 参数里的任意一个子 promise 被成功或失败后,父 promise 马上也会用子 promise 的成功返回值或失败详情作为参数调用父 promise 绑定的相应句柄,并返回该 promise 对象。——MDN 文档
# 异步方案三:Generator
# Generator 是什么
ES6 中的 Generator 其实不是为了解决异步问题而生的,但是它又非常适合解决异步问题。
Generator 函数与普通函数不同,它最大的特点,就是可以交出函数的执行权、将函数分段进行。
# Generator 的用法
generator 函数有两个区分于普通函数的部分。
Generator 函数的定义,在 function 关键字后面,函数名之前有个 "*"
函数内部有 yield 表达式
来看一个例子
function* func(){ | |
console.log('one'); | |
yield '1';// 根据 yield 关键字来分阶段 | |
console.log('two'); | |
yield '2'; | |
console.log('three'); | |
return '3'; | |
} |
generator 函数被调用时,会返回一个指向内部状态的指针,我们可以使用 next 方法,来进行下一阶段的执行。
// 因此我们可以 | |
let g = func(); | |
// 第一次调用 next 方法时,从函数头部开始执行 | |
g.next(); | |
//one | |
//{value:'1',done:false} | |
g.next(); | |
//two | |
//{value:'2',done:false} | |
g.next(); | |
//three | |
//{value:'3',done:true} | |
// 如果已结束再次调用 next,返回的 value 是 undefined,done 属性仍为 true |
g.next () 也会返回一个对象,格式为 {value:xxx,done:true/false},value 用于函数体内外数据的交换,像返回值一样返回即可,done 表示是否执行完毕
# Generator 的意义
利用 Generator 函数暂停执行的效果,可以把异步操作写在 yield 语句内,等到 next 方法后再往后执行。这实际上等同于不用再编写回调函数,因为异步操作的后续操作可以放在 yield 语句下面,等到调用 next 方法时再执行。
而且使用 Generator 函数写法写起来已经非常像同步了,但有一点,流程管理非常的不方便。
# 异步方案四:async/await
# async/await 概念?
学习 async/await 的话,熟悉 Promise 是前提。而且 async/await 其实是 Generator 函数的语法糖。
async 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数内。
async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。
await 其实是默认创建一个 Promise 对象(如果修饰的代码不是返回一个 Promise,则创建一个异步完成的 Promise,并且将 resolve 传的结果作为返回值),然后等待该 Promise 完成,将完成的结果作为返回值。
async function foo(){ | |
console.log('0'); | |
let a = await 1; | |
console.log(a); | |
console.log('2'); | |
} | |
console.log('start'); | |
foo(); | |
console.log('end'); | |
//start->0->end->1->2 |
当函数执行遇到 await 时,它会将主线程的控制权交出,先继续执行其他同步代码,等到 Promise 完成状态的时候通过事件循环(微任务那一套)来继续执行。
- await 仅能在 async 函数内部使用,否则会抛出语法错误
- async 函数也可以用 bind 二次绑定作用域
- 调用 async 函数时本质上返回的是一个 promise,可以进行 .then () .catch () 操作
- 在 async 函数中,可以在 while, for, for/in, for/of 等控制语句中循环执行 await
# async/await 改写红绿灯
const taskRunner = async () => { | |
await task(3000,'red'); | |
await task(1000,'green'); | |
await task(2000,'yellow'); | |
taskRunner(); | |
} | |
taskRunner(); |