深入探究JS中的异步编程和事件循环机制

 更新时间:2023年05月23日 10:33:06   作者:阿托  
js是单线程事件循环模型,同步操作与异步操作时代码所依赖的核心机制,异步行为是为了优化因计算量大而时间长的操作,本文详细给大家介绍了JS中的异步编程和事件循环机制,文中有详细的代码示例,需要的朋友可以参考下

异步编程

js是单线程事件循环模型,同步操作与异步操作时代码所依赖的核心机制。异步行为是为了优化因计算量大而时间长的操作。

同步:当一个进程在执行某个请求时,如果这个请求需要等待一段时间才能返回,那么这个进程会一直等下去,直到请求返回。

异步:当一个进程在执行某个请求时,如果这个请求需要等待一段时间才能返回,那么这个进程会直接向下执行,不需要等待,当消息返回时再通知进程处理。

Promise

Promise的出现主要时解决之间异步编程使用回调函数的回调地狱的问题。

Promise的状态

Promise有三种状态:

  • pending:待定
  • fulfilled:成功
  • rejected:失败

两种状态变化:

  • pending -> fulfilled:在promise中调用resolve()函数会产生此状态转变
  • pending ->rejected:在promise中调用reject()函数会产生此状态转变

promise的基本流程

Promise构造函数

用于创建Promise对象,需要传入一个执行器函数,函数中需要带两个参数:resolved,rejected;用于修改Promise的状态

let p = new Promise((resolved,rejected)=>{
    console.log(1);
    resolved();
})
// 1
console.log(p); //Promise {<fulfilled>: undefined}

Promise.prototype.then()

用于定义成功或失败状态的回调函数,并返回一个新的Promise。

let p = new Promise((resolved,rejected)=>{
    setTimeout(()=>{
        resolved("ok");
    },1000);
});
p.then(res=>{
    console.log(res);
},err=>{
    console.log(err);
})
// ok
let p = new Promise((resolved,rejected)=>{
    setTimeout(()=>{
        rejected("err");
    },1000);
});
p.then(res=>{
    console.log(res);
},err=>{
    console.log(err);
})
// err

Promise.prototypr.catch()

用于定义失败的回调,then方法的语法糖,相当于.then(undefined,onRejected);

let p = new Promise((resolved,rejected)=>{
    setTimeout(()=>{
        rejected("err");
    },1000);
});
p.then(res=>{
    console.log(res);
}).catch(err=>{
    console.log(err);
})
// err

Promise.resolve()

返回一个成功或者失败的Promise;

let p = Promise.resolve("ok"); //Promise {<fulfilled>: ok}
let p = Promise.resolve(Promise.reject('err')); //Promise {<rejected>: 'err'}

Promise.all()

返回一个新的Promise,只有所有的Promise都成功才成功,否则有一个失败就返回失败的promise

let p = Promise.all([
    Promise.resolve(1),
    Promise.resolve(2),
	Promise.resolve()
]) // Promise {<fulfilled>: [1,2,undefined]} 
let p = Promise.all([
    Promise.reject('err'),
    Promise.resolve(1),
    Promise.resolve(2)
]) // Promise {<rejected>: 'err'}

Promise.race()

返回一个新的Promise,第一个完成的Promise的结果状态就是最终的结果状态。

let p = Promise.race([
    Promise.resolve(1),
    Promise.resolve(2),
	Promise.resolve()
]) // Promise {<fulfilled>: 1} 
let p = Promise.all([
    Promise.reject('err'),
    Promise.resolve(1),
    Promise.resolve(2)
]) // Promise {<rejected>: 'err'}

Promise原理实现

function Promise(excutor){
    const self = this;
    self.status = "pending";
    self.data = undefined;
    self.callbacks = [] // 用来保存所有待调用的回调函数
    function resolve(value){
        if(self.status !== "pending"){
            return;
        }
        // 改变状态
        self.status = "fulfilled";
        self.data = value;
        //  异步调用回调函数
        if(self.callbacks.length > 0){
            setTimeout(()=>{
                self.callbacks.forEach((obj)=>{
                    obj.onResolved();
                })
            })
        }
    }
    function reject(reason){
        if(self.status !== "pending"){
            return;
        }
        // 改变状态
        self.status = "rejected";
        self.data = value;
        //  异步调用回调函数
        if(self.callbacks.length > 0){
            setTimeout(()=>{
                self.callbacks.forEach((obj)=>{
                    obj.onRejected();
                })
            })
        }
    }
    // 立即同步调用执行器函数
    try{
        excutor(resolve,reject);
    }catch(error){
        reject(error);
    }
}
Promise.prototype.then = function(onResolved,onRejected){
    const  self = this;
    onResolved = typeof onResolved === 'function'?onResolved:value=>value;
    onRejected = typeof onResolved === 'function'?onRejected:reason=>{throw reason};
    // 返回promise对象
    return new Promise((resolve,reject)=>{
        // 执行回调函数
        function handle(callback){
            try{
                const x = callback(self.data);
                // 如果返回对象为promise
                if(x instanceof Promise){
                    x.then(resolve,reject);
                }else{
                    resolve(x);
                }
            }catch(err){
                reject(err);
            }
        }
        // 定义回调之前,状态已经改变
        if(self.status == 'fulfilled'){
            setTimeout(()=>{
                handle(onResolved);
            })
        }else if(self.status == 'rejected'){
            setTimeout(()=>{
                handle(onRejected);
            })
        }else{
            // 定义回调之前状态还未改变
            self.callbacks.push({
                onResolved(){
                    handle(onResolved);
                },
                onRejected(){
                    handle(onRejected);
                }
            })
        }
    })
}
Promise.prototype.catch = function(onRejected){
    return this.then(undefined,onRejected)
}
Promise.resolve = function(value){
    return new Promise((resolve,reject)=>{
        if(value instanceof Promise){
            value.then(resolve,reject);
        }else{
            resolve(value);
        }
    })
}
Promise.reject = function(reason){
    return new Promise((resolve,reject)=>{
        reject(reason);
    })
}
Promise.all = function(promises){
    return new Promise((resolve,reject)=>{
        let count = 0;
        let plen = promises.length;
        let values = new Array(plen);
        for(let i = 0;i < plen;i++){
            Promise.resolve(promises[i]).then(value=>{
                count++;
                value[i] = value;
                if(count == plen) resolve(values);
            },err=>{
                reject(err);
            })
        }
    })
}
Promise.race = function(promises){
    return new Promise((resolve,reject)=>{
        for(let i = 0;i < promises.length;i++){
            Promise.resolve(promises[i]).then(value=>{
                resolve(value);
            },err=>{
                reject(err);
            })
        }
    })
}

异步函数async/await

ES8提出使用async和await解决异步结构组织代码的问题

async

async关键字用于声明异步函数,使用async关键字可以让函数具有异步特征,但总体上其代码仍然时同步求值。

async function fn1(){
    console.log(1);
}
fn1();
console.log(2);
// 1
// 2

async的返回值是一个Promise对象,如果函数中返回一个值value,则async则返回Promise.resolve(value);

await

因为异步函数主要针对不会马上完成的任务,所以自然需要一种暂停和恢复执行的能力。

await关键字必须在异步函数中使用。

async/await中真正起作用的是await。async关键字,无论从哪个方面来看,都是一个标识符。异步函数中不包含await关键字,其执行基本上跟普通函数没有区别。

async function fn(){
    console.log(1);
    let p = await Promise.resolve('ok');
    console.log(p);
    console.log(2);
}
fn();
console.log(3);
// 1
// 3
// ok
// 2

js事件循环机制

js运行机制

因为js时单线程的,在执行代码时,将不同函数的执行上下文压入栈中来保证代码的有序执行,在执行同步代码的时候,如果遇到异步代码,js并不会等待其返回结果,而是将异步代码交给对应的异步代码处理线程处理,继续执行栈中的任务。

当异步事件执行完毕之后,再将异步事件对应的对调加入异步执行队列,当主栈中的函数执行完毕后,会从异步执行队列中选择任务来执行。

宏任务和微任务

在任务队列中异步回调分为2中:微任务和宏任务。也就是说任务队列可以分为两种:微任务队列和宏任务队列。

宏任务:全局代码,settimeout/setinterval,UI渲染,

微任务:promise.then,catch,finally,process.nextTick

宏任务和微任务的执行顺序

  • 代码从开始执行调用全局执行栈,script标签内的代码做为宏任务执行。

  • 执行过程中同步代码立即执行,异步代码由异步处理线程处理然后放入任务队列中。

  • 执行过程中同步代码立即执行,异步代码放入任务队列中。

    • 先看任务队列中的微任务队列是否存在微任务

      有微任务:执行微任务队列中的所有微任务

      微任务执行过程中所产生的微任务放到微任务队列中,继续执行。

    • 如果没微任务,查看是否具有宏任务,有的话执行,没有的话事件轮询结束。

      执行过程中所产生的微任务放到微任务队列中。

      完成单个宏任务之后,执行微任务队列中的任务。

常见面试题

异步编程的实现方式有哪些

  • 回调函数:使用回调函数的方式的缺点是,是个回调函数嵌套的时候,会照成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码维护。
  • promise:使用promise的方式可以嵌套的函数调用做为链式调用,但是使用这种方法,有时会找出多个then的链式调用,可能造成代码的语义不够明确。
  • generator:配合yield实现,可以在函数内实现中断。
  • async/await:async函数是generator和promise实现的一个自动执行的语法糖,他内部自带执行器,当函数内部执行到await语句的时候,如果语句返回promise对象,那么函数将会等待promise对象的状态变为resolve后再基于向下执行。因此可以将异步逻辑转化为同步的顺序来写,并且这个函数可以自动执行。

如何改变promise的状态

  • 调用resolve(value),pending ->fulfilled
  • 调用reject(reason),pending ->rejected
  • 抛出异常:如果当前时pending就会变成rejected

改变promise状态和指定回调函数谁先谁后

都有可能,正常情况下是先指定回调再改变状态,但也可以先改变状态再指定回调。

什么时候能得到数据?

  • 先指定回调,当状态发生改变时,回调函数就会调用,得到数据。
  • 先改变状态,当指定回调函数时,回调函数就会调用,得到数据。

promise中改变状态和指定回调函数时做了什么事情

在promise内部定义了一个回调队列。

在状态改变时,也就是调用onresolved或者onrejected函数时,会执行回调队列中的函数。

当指定回调函数时,首先会判断当前的状态是否是pending,如果是则将回调函数加入回调队列,否则直接执行根据状态执行onresolved或者onrejected函数。

这就是为什么无论是先改变状态还是先指定回调函数,都能得到最终的结果的原因。

promise.then() 返回的新promise的结果由什么决定

  • 如果抛出异常,新的promise变为reject,reason为抛出的异常

  • 如果返回的时非promise的任意值,新promise变为fulfilled,value为返回的值

  • 如果返回promise的时另一个promise对象,此peomise的结果就会成为新promise的结果。

promise异常穿透

  • 当使用promise的then链式调用时,可以在最后指定失败的回调
  • 前面任何操作处理异常都会传到最后的回调中处理

中断promise链

当使用promise的then链式调用,在中间中断,不在调用后面的回调函数。

办法:在回调函数中返回一个pending状态的promise对象

async/await对比promise的优势

  • 代码读起来更像同步,Promise虽然摆脱了回调地狱,但是then方法的链式调用也会带来额外的阅读负担
  • Promise传递中间值非常麻烦,而async、await几乎是同步的写法

Promist.catch后面的.then还会执行吗

.then回执行,因为catch方法返回的还是一个promise对象,依然支持链式调用。

Promise中,resolve后面的语句是否还会执行?

会被执行。如果不需要执行,需要在 resolve 语句前加上 return。

JS为什么是单线程语言

JavaScript的诞生就是为了处理浏览器网页的交互(DOM操作的处理、UI动画等), 设计成单线程的原因就是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果(两个线程修改了同一个DOM节点就会产生不必要的麻烦),这对于一种网页脚本语言来说这就太复杂了。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

JS是怎么实现异步异常运行的

JavaScript是单线程的,但它所运行的宿主环境—浏览器是多线程,浏览器提供了各种线程供Event Loop调度来协调JS单线程运行时不会阻塞

谈一谈setInterval的问题

setTimeout的作用是每隔一段指定事件执行一个函数,但是这个执行不是真的到了时间立即执行,它真正的作用是每隔一段时间将事件加入事件队列中去,只有当当前的执行栈为空的时候,才能去从事件队列中取出事件执行。

所以可能会出现这样的情况,就是当前执行栈执行的时间很长,导致事件队列里边积累多个定时器加入的事件,当执行栈结束的时候,这些事件会依次执行,因此就不能到间隔一段时间执行的效果。

正对这种现象,我们可以使用setTimeout递归调用来模拟实现setInterval,这样就能确保了只有一个事件结束,我们才会触发下一个定时器事件。

function mySetInterval(fn,delay){
    var timer = {
        flag = true;
    };
    function interval(){
        if(timer.flag){
            fn();
            setTimeout(interval,delay);
        }
    }
    setTimeout(intetval,delay);
    return timer;
}
function myClearInterval(timer){
    timer.flag = false;
}

以上就是深入探究JS中的异步编程和事件循环机制的详细内容,更多关于JS异步编程和事件循环机制的资料请关注脚本之家其它相关文章!

相关文章

最新评论