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

浅聊一下

当面试官叫我聊聊异步的时候,我就知道稳啦!全都稳啦!面试的时间一般在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时结束递归

结尾

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

相关推荐
uhakadotcom2 小时前
视频直播与视频点播:基础知识与应用场景
后端·面试·架构
范文杰2 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪2 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪3 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy3 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom4 小时前
快速开始使用 n8n
后端·面试·github
uhakadotcom4 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom4 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom4 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom4 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试