面试官叫我聊聊异步,稳啦!全都稳啦!

浅聊一下

当面试官叫我聊聊异步的时候,我就知道稳啦!全都稳啦!面试的时间一般在1-2个小时,所以当面试官问到你所擅长的领域时,你可以把你所知道的全部一一列举出来...拖延一下时间,让他别问你这么多问题...

聊聊异步

其实在前面的文章中我就已经浅浅地聊过了异步(Promise:解决JavaScript异步编程难题 - 掘金 (juejin.cn)),这只是利于新手的入门,而对于掘友们这类老手,我们就得上硬货...

为什么需要异步?

因为 JavaScript 是一门单线程语言,同一时间只能执行一个任务,所以当一个任务正在等待时,先执行后面的任务,可以提高程序的效率...

js为什么不设计成多线程语言

  1. JavaScript 的初衷就是打造成一个浏览器的脚本语言,其实javaScript后来也是可以打造成一个多线程的语言的,但是当时已经存在了许多多线程语言如java,所以到最后还是没有让他也变成多线程语言
  2. 正因为js是一门单线程语言。所以不需要消耗多线程语言那么多的运行内存,可以节约内存
  3. 单线程语言没有多线程语言中的锁、解锁的过程,节约上下文切换的时间

异步的发展史

什么是异步发展史

js中从最早的异步处理方式到现在的最新的异步处理方式

发展史

回调函数

在ES6以前,程序员们使用回调函数来处理异步,把b函数塞到a函数里面调用

js 复制代码
function a(){
    setTimeout(()=>{
        console.log('a');
        b()
    },1000)
}
function b(){
    setTimeout(()=>{
        console.log('b');
    },500)
}
a()

很快程序员们发现,这样写出来的代码存在许多这样的回调调用,导致如果一处出错,处处出错,难以维护(被称为回调地狱),于是在ES6中官方就打造出来了Promise

Promise

在ES6里,官方打造出来了一个Promise来处理异步问题

js 复制代码
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()
})
  1. 手写Promise

Promise里维护了一个状态state,值为pending fulfilled rejected,目的是让Promise的状态一经改变,无法再次修改,也就保证了 then 和 catch 不可能同时触发

js 复制代码
class Promise{
    //从上面函数的调用可以看出,constructor中应该传入一个函数,并且加上state初始状态'pending',并且在最后我们得将这个函数执行完
    constructor(executor){
        this.state = 'pending';//promise 的状态
        executor(resolve,reject);
    }
}

传入的executor函数需要接收两个参数,这两个参数都为函数,并且两个函数分别接收一个参数

js 复制代码
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是干什么的

js 复制代码
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中要调用的函数根据状态存入了两个数组中

js 复制代码
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的思路是一样一样的

js 复制代码
 const reject = (reason)=>{
      if(this.state === 'pending'){
         this.state = 'rejected';
         this.reason = reason;
         this.onRejectedCallbacks.forEach(fn=>{
             fn(this.reason)
         })
     }
}

接下来实现then方法,先看看then方法的调用

js 复制代码
a().then(
(res)=>{
    console.log(res);
},
(err)=>{
    console.log(err);
})

思考一下,then中的函数什么时候触发?只有当a()函数调用完成以后才能触发,我们怎么才能知道a是否调用完成了呢?这就得提到我们维护的状态了,当a的状态为fulfilled或者reject的时候,说明a已经执行完了,这时我们就可以立即执行then中的函数了,当状态为pending时,就将then中的函数存起来,等a执行完再调用。注意,在这里我们使用setTimeout模拟了一个微任务...

js 复制代码
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方法

js 复制代码
Promise.race([a(),b()]).then((res)=>{
    console.log('d');
})

Promise.race()接收一个数组,当数组里的函数有一个调用完成以后,调用then中的函数

js 复制代码
    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中的函数

js 复制代码
Promise.all([a(),b()]).then((res)=>{
    console.log(res)
})
js 复制代码
    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,否则一直遍历

js 复制代码
 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的一种处理异步的方法

js 复制代码
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处理异步

js 复制代码
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出现的语法,来看看他是如何处理异步的

js 复制代码
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,但是我们可以来模拟一下

js 复制代码
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')
           
        })
    }
}
  1. generatorToAsync 函数接收一个 Generator 函数 generatorFn 作为参数,然后返回一个新的函数。

  2. 在新返回的函数内部:

    • 创建了一个 Generator 对象 gen,这个对象是通过调用 generatorFn() 得到的。
    • 返回了一个 Promise 对象,并在 Promise 的执行体中实现了对该 Generator 对象的遍历。
  3. loop 函数被定义来处理 Generator 对象的迭代:

    • 接收两个参数 keyargkey 表示 Generator 对象上的方法(比如 next),arg 表示传递给 Generator 方法的参数。
    • 调用 Generator 对象的方法 gen[key](arg) 来获取下一次迭代的结果,返回结果包含 valuedone 两个属性。
    • 如果 done 为 true,表示 Generator 函数已经完成执行,此时通过 Promise 的 resolve 方法返回该结果的 value 属性的值。
    • 如果 done 为 false,表示 Generator 函数还未完成执行。此时,将 value 属性的值通过 Promise.resolve() 转换为一个 Promise 对象,并在该 Promise 对象 resolve 时进行递归调用 loop('next', val),继续执行 Generator 函数的下一步迭代。
  4. 最后,在函数的最后部分,调用了 loop('next') 来启动了第一次遍历,从而开始执行 Generator 函数。

调用

js 复制代码
const asyncFn = generatorToAsync(gen)
asyncFn().then((res)=>{
    console.log(res)
})

总结

  1. 回调函数: 代码维护困难(回调地狱)

  2. 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执行

  3. Generator:

    <1> 可以分段执行

    <2> 可以控制每个阶段的返回值

    <3> 可以知道是否执行完毕 <4> 可以借助 Thunk 和 co 处理异步,但是写法复杂,所以Generator出现的意义其实是为了打造async/await语法

  4. async/await:

    <1> es6提供的一种新的处理异步代码的方案

    <2> 缺点:没有错误捕获机制

    <3> async/await 是由 promsie + generator 实现的,本质是在generator的基础上通过递归的方式来自动执行一个又一个的next函数,当done为true时结束递归

结尾

在面试的时候如果要你聊聊异步,请你把这些哐哐往外甩...

相关推荐
minDuck4 分钟前
ruoyi-vue集成tianai-captcha验证码
java·前端·vue.js
小政爱学习!25 分钟前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript
魏大帅。30 分钟前
Axios 的 responseType 属性详解及 Blob 与 ArrayBuffer 解析
前端·javascript·ajax
花花鱼36 分钟前
vue3 基于element-plus进行的一个可拖动改变导航与内容区域大小的简单方法
前端·javascript·elementui
k093340 分钟前
sourceTree回滚版本到某次提交
开发语言·前端·javascript
EricWang13581 小时前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端
September_ning1 小时前
React.lazy() 懒加载
前端·react.js·前端框架
时差9531 小时前
【面试题】Hive 查询:如何查找用户连续三天登录的记录
大数据·数据库·hive·sql·面试·database
web行路人1 小时前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
番茄小酱0011 小时前
Expo|ReactNative 中实现扫描二维码功能
javascript·react native·react.js