深入解析JavaScript异步编程与事件循环机制

作者:半夏时间:2026-10-30 19:21:53浏览:500

本文深入探讨JavaScript的单线程模型与异步编程核心机制。详细解析同步与异步的区别,阐述Promise的三种状态及链式调用原理,介绍async/await语法糖的优势。重点剖析事件循环(Event Loop)中宏任务与微任务的执行顺序,并结合常见面试题解析异步异常处理、定时器模拟及代码执行流程。

异步编程

异步编程

JavaScript采用单线程事件循环模型,这是同步与异步操作赖以运行的核心机制。引入异步行为的主要目的是优化那些计算量大或耗时较长的操作,避免阻塞主线程。

同步模式下,当一个进程执行某个请求且该请求需要等待一段时间才能返回结果时,进程会一直挂起等待,直到请求返回为止。

异步模式下,当进程执行需要等待的请求时,进程不会挂起等待,而是直接继续向下执行后续代码。当异步请求返回结果时,系统会通知进程进行处理。

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.prototype.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运行机制

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

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

异步编程

宏任务和微任务

在任务队列中,异步回调分为两种:微任务和宏任务。因此,任务队列也可以分为两种:微任务队列和宏任务队列。

宏任务包括:全局代码、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变为rejected,reason为抛出的异常。

  • 如果返回的是非Promise的任意值,新Promise变为fulfilled,value为返回的值。

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

promise异常穿透

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

中断promise链

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

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

async/await对比promise的优势

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

Promise.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异步编程和事件循环机制的资料请关注本站其它相关文章!

作者最新文章