浅聊一下
当面试官叫我聊聊异步的时候,我就知道稳啦!全都稳啦!面试的时间一般在1-2个小时,所以当面试官问到你所擅长的领域时,你可以把你所知道的全部一一列举出来…拖延一下时间,让他别问你这么多问题…
聊聊异步
其实在前面的文章中我就已经浅浅地聊过了异步(Promise:解决JavaScript异步编程难题 – 掘金 (juejin.cn)),这只是利于新手的入门,而对于掘友们这类老手,我们就得上硬货…
为什么需要异步?
因为 JavaScript 是一门单线程语言,同一时间只能执行一个任务,所以当一个任务正在等待时,先执行后面的任务,可以提高程序的效率…
js为什么不设计成多线程语言
- JavaScript 的初衷就是打造成一个浏览器的脚本语言,其实javaScript后来也是可以打造成一个多线程的语言的,但是当时已经存在了许多多线程语言如java,所以到最后还是没有让他也变成多线程语言
- 正因为js是一门单线程语言。所以不需要消耗多线程语言那么多的运行内存,可以节约内存
- 单线程语言没有多线程语言中的锁、解锁的过程,节约上下文切换的时间
异步的发展史
什么是异步发展史
js中从最早的异步处理方式到现在的最新的异步处理方式
发展史
回调函数
在ES6以前,程序员们使用回调函数来处理异步,把b函数塞到a函数里面调用
function a(){
setTimeout(()=>{
console.log('a');
b()
},1000)
}
function b(){
setTimeout(()=>{
console.log('b');
},500)
}
a()
很快程序员们发现,这样写出来的代码存在许多这样的回调调用,导致如果一处出错,处处出错,难以维护(被称为回调地狱),于是在ES6中官方就打造出来了Promise
Promise
在ES6里,官方打造出来了一个Promise来处理异步问题
function a(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log('a');
resolve();
},1000)
})
}
function b(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log('b');
resolve();
},500)
})
}
a().then(()=>{
b()
})
- 手写Promise
Promise里维护了一个状态state,值为pending fulfilled rejected,目的是让Promise的状态一经改变,无法再次修改,也就保证了 then 和 catch 不可能同时触发
class Promise{
//从上面函数的调用可以看出,constructor中应该传入一个函数,并且加上state初始状态‘pending’,并且在最后我们得将这个函数执行完
constructor(executor){
this.state = 'pending';//promise 的状态
executor(resolve,reject);
}
}
传入的executor函数需要接收两个参数,这两个参数都为函数,并且两个函数分别接收一个参数
class Promise{
//从上面函数的调用可以看出,constructor中应该传入一个函数,并且加上state初始状态‘pending’,并且在最后我们得将这个函数执行完
constructor(executor){
this.state = 'pending';//promise 的状态
this.value = undefined // 接收resolve的参数
this.reason = undefined// 接收reject的参数
const resolve = (value)=>{
}
const reject = (reason)=>{
}
executor(resolve,reject);
}
}
来看看里面的resolve函数如何实现,先看resolve是干什么的
function a(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log('a');
resolve('666666666666');
},1000)
})
}
function b(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log('b');
resolve();
},500)
})
}
a().then((res)=>{
b()
console.log(res);
})
可以看出来resolve接收一个值,然后可以用这个值为参数去调用then中的函数,那么resolve如何拿到then中的函数并且调用的呢?其实Promise维护了两个数组,存入then中的函数,在这里我们先知道then中要调用的函数根据状态存入了两个数组中
class MyPromise{
constructor(executor){
this.state = 'pending';//promise 的状态
this.value = undefined // 接收resolve的参数
this.reason = undefined// 接收reject的参数
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
const resolve = (value)=>{
if(this.state === 'pending'){
this.state = 'fulfilled';
this.value = value;
// 把then里的回调函数触发
this.onFulfilledCallbacks.forEach(fn=>{
fn(this.value)
})
}
}
}
}
resolve中,首先要判断一下状态是否为pending,只有状态为pending时,才能调用,并且要将状态修改为已完成状态,把resolve中传入的参数赋值给value,接下来就可以拿着这个value去调用then中的函数了,遍历循环onFulfilledCallbacks,并且将value传入调用…
reject函数跟resolve的思路是一样一样的
const reject = (reason)=>{
if(this.state === 'pending'){
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn=>{
fn(this.reason)
})
}
}
接下来实现then方法,先看看then方法的调用
a().then(
(res)=>{
console.log(res);
},
(err)=>{
console.log(err);
})
思考一下,then中的函数什么时候触发?只有当a()函数调用完成以后才能触发,我们怎么才能知道a是否调用完成了呢?这就得提到我们维护的状态了,当a的状态为fulfilled或者reject的时候,说明a已经执行完了,这时我们就可以立即执行then中的函数了,当状态为pending时,就将then中的函数存起来,等a执行完再调用。注意,在这里我们使用setTimeout模拟了一个微任务…
then(onFulfilled,onRejected){
// 把onFulfilled 存起来 供resolve调用
onFulfilled = typeof onFulfilled === 'function'?onFulfilled:()=>this.value;
onRejected = typeof onRejected === 'function'?onRejected:()=>this.reason;
//返回一个Promise
return new MyPromise((resolve,reject)=>{
//then前面的promise对象状态是同步变更完成了
if(this.state === 'fulfilled'){
setTimeout(()=>{//模拟异步 官方是微任务,这里用宏任务简化
try{
const result = onFulfilled(this.value);
resolve(result)//应该放的是result中的resolve的参数
}catch(err){
reject(err)
}
})
}
if(this.state === 'rejected'){
setTimeout(()=>{
try{
const result = onRejected(this.reason);
resolve(result)
}catch(err){
reject(err)
}
})
}
if(this.state === 'pending'){
this.onFulfilledCallbacks.push((value)=>{
setTimeout(()=>{ // 为了保障将来onFulfilled在resolve中被调用时是一个异步
try{
const result = onFulfilled(value)
resolve(result)
}catch(err){
reject(err)
}
})
});
this.onRejectedCallbacks.push((reason)=>{
setTimeout(()=>{
try{
const result = onRejected(reason)
resolve(result)
}catch(err){
reject(err)
}
})
})
}
})
}
此时我们的Promise就算是打造好了,接下来再来看race方法
Promise.race([a(),b()]).then((res)=>{
console.log('d');
})
Promise.race()接收一个数组,当数组里的函数有一个调用完成以后,调用then中的函数
static race (promises){
return new MyPromise((resolve,reject)=>{
//判断promises里面哪个对象的状态先变更
for(let promise of promises){
promise.then(
(value)=>{
resolve(value)
},(reject)=>{
reject(reason)
}
)
}
})
}
promises是接收的promise数组,我们需要判断有没有promise的状态更改,于是我们遍历Promises数组,如果一个promise的状态更改了,说明他then中的函数要执行,通过resolve或者reject把value传递出去…
还有all函数,与race不同的是,要当数组中的函数全部调用完成以后才会调用then中的函数
Promise.all([a(),b()]).then((res)=>{
console.log(res)
})
static all(promises){
return new MyPromise((resolve,reject)=>{
let count = 0;
let arr = []
for(let i=0 ; i< promises.length ; i++){
promises[i].then(
(value)=>{
count++
arr[i]=value
if(count === promises.length){
resolve(arr)
}
},
(reason)=>{
reject(reason)
}
)
}
})
}
如果碰到错误就直接reject退出了,如果没错,就遍历promises,用一个count记录一下是否遍历完全返回最终结果,并且把每一个value存入数组,resolve传递出这个数组
还有一个Promise.any(),和Promise.all()不同的是,如果碰到一个状态为fulfilled的promise对象,就resolve,否则一直遍历
static any(promises){
return new MyPromise((resolve,reject)=>{
let count = 0;
let errors = []
for(let i=0 ; i< promises.length;i++){
promises[i].then(
(value)=>{
resolve(value)
},
(reason)=>{
count++
errors[i] = reason
if(count === promises.length){
reject(new AggregateError(errors))
}
}
)
}
})
}
Generator
使用Generator也是ES6的一种处理异步的方法
function* foo(){
yield 'a'
yield 'b'
yield 'c'
return 'ending'
}
let gen = foo()//得到一个generator的实例对象
console.log(gen.next());//{value: 'a', done: false}
console.log(gen.next());//{value: 'b', done: false}
console.log(gen.next());//{value: 'c', done: false}
console.log(gen.next());//{value: 'ending', done: true}
当程序运行到yield时停止,当调用next的时候,执行当前yield后面的逻辑,将执行结果赋值给value,并且运行到下一个yield,return则代表程序结束,done变为true
Generator处理异步
function request(num){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(num*10)
},1000)
})
}
function* gen(){
const num1 = yield request(1)
const num2 = yield request(num1)
const num3 = yield request(num2)
return num3
}
const gen1 = gen()
const next1 = gen1.next()
next1.value.then((res)=>{
const next2 = gen1.next(res)
console.log(res);
next2.value.then((res)=>{
const next3 = gen1.next(res)
console.log(res);
next3.value.then((res)=>{
console.log(res)
})
})
})
看起来十分的令人头疼,你说为什么会有这样一个方法呢?他都还不如Promise好用…实际上Generator出现的意义是为了打造async/await
async/await
async是ES7出现的语法,来看看他是如何处理异步的
function request(num){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(num*10)
},1000)
})
}
async function test(){
let res1 = await request(1)
let res2 = await request(10)
console.log(res1,res2);
}
test()
async是使用Generator来实现的,我们无法打造async,但是我们可以来模拟一下
function generatorToAsync(generatorFn){//把generatorFn变更成具有async功能的函数
return function(){
const gen = generatorFn()
return new Promise((resolve,reject)=>{
function loop(key,arg){
let res = null
res = gen[key](arg)//gen.next(arg) {value:Promise{},done:fasle}
const {value,done} = res
if(done){
return resolve(value)
}else{
Promise.resolve(value) // Promise.resolve()接收一个Promsie对象会直接读取该参数对象中resolve的值
.then((val)=>{
loop('next',val)
})
}
}
loop('next')
})
}
}
-
generatorToAsync
函数接收一个 Generator 函数generatorFn
作为参数,然后返回一个新的函数。 -
在新返回的函数内部:
- 创建了一个 Generator 对象
gen
,这个对象是通过调用generatorFn()
得到的。 - 返回了一个 Promise 对象,并在 Promise 的执行体中实现了对该 Generator 对象的遍历。
- 创建了一个 Generator 对象
-
loop
函数被定义来处理 Generator 对象的迭代:- 接收两个参数
key
和arg
,key
表示 Generator 对象上的方法(比如next
),arg
表示传递给 Generator 方法的参数。 - 调用 Generator 对象的方法
gen[key](arg)
来获取下一次迭代的结果,返回结果包含value
和done
两个属性。 - 如果
done
为 true,表示 Generator 函数已经完成执行,此时通过 Promise 的resolve
方法返回该结果的value
属性的值。 - 如果
done
为 false,表示 Generator 函数还未完成执行。此时,将value
属性的值通过Promise.resolve()
转换为一个 Promise 对象,并在该 Promise 对象 resolve 时进行递归调用loop('next', val)
,继续执行 Generator 函数的下一步迭代。
- 接收两个参数
-
最后,在函数的最后部分,调用了
loop('next')
来启动了第一次遍历,从而开始执行 Generator 函数。
调用
const asyncFn = generatorToAsync(gen)
asyncFn().then((res)=>{
console.log(res)
})
总结
回调函数: 代码维护困难(回调地狱)
Promise:
<1> 维护了一个状态state,值为pending fulfilled rejected,目的是让Promise的状态一经改变,无法再次修改,也就保证了 then 和 catch 不可能同时触发
<2> 内部的resolve函数会修改state为fulfilled,并触发then中的回调
<3> then:
1> 默认返回一个promise对象,状态为fullfilled
2> 当then前面的promise的状态为fullfilled时,then中的回调直接执行
当then前面的promise的状态为rejected时,then中的第二个回调直接执行
当then前面的promise的状态为pending时,then中的回调需要被缓存起来交给resolve或者reject执行Generator:
<1> 可以分段执行
<2> 可以控制每个阶段的返回值
<3> 可以知道是否执行完毕
<4> 可以借助 Thunk 和 co 处理异步,但是写法复杂,所以Generator出现的意义其实是为了打造async/await语法async/await:
<1> es6提供的一种新的处理异步代码的方案
<2> 缺点:没有错误捕获机制
<3> async/await 是由 promsie + generator 实现的,本质是在generator的基础上通过递归的方式来自动执行一个又一个的next函数,当done为true时结束递归
结尾
在面试的时候如果要你聊聊异步,请你把这些哐哐往外甩…
原文链接:https://juejin.cn/post/7341401110631596084 作者:滚去睡觉