JavaScript异步执行机制与回调地狱解析

作者:纸飞机

时间:2026-10-24

浏览:510

本文深入解析JavaScript单线程模型下的异步执行原理,阐述事件循环、任务队列及主线程的工作机制。通过代码示例展示同步与异步任务的执行顺序,并重点分析回调函数在处理异步逻辑时的应用及其带来的“回调地狱”与信任问题,为后续引入Promise方案奠定基础。

文章目录

一、JS异步的执行原理

JavaScript采用单线程运行模式,而浏览器环境则包含多个线程。由于单线程特性,任务必须按顺序依次执行。若某个任务耗时较长(例如Ajax请求),会导致主线程阻塞,后续任务无法执行,进而造成界面无响应。为解决此问题,异步机制应运而生。

理解异步机制,需先了解浏览器中三个常驻的核心线程:JS引擎线程、事件触发线程以及GUI渲染线程。

JS引擎线程与事件触发线程共同协作,构成了事件循环(Event Loop)机制。GUI渲染线程与JS引擎线程互斥:当JS引擎执行代码时,GUI渲染会被挂起;GUI的更新操作会被放入队列中,待JS引擎空闲时立即执行。

接下来从事件循环机制的角度进行解析:

一、JS异步的执行原理

JS引擎线程中的任务分为同步任务和异步任务两类:

1. 同步任务直接在主线程上执行,形成执行栈(Execution Stack)。

2. 异步任务则被交给异步进程(Web APIs)处理,例如事件触发线程或定时器线程等,这些任务形成任务队列(Task Queue)。

3. 当执行栈中的所有同步任务处理完毕,主线程处于空闲状态时,会从任务队列中取出任务放入执行栈中进行执行。

通俗而言,JavaScript除了主线程外,还存在一个任务队列,用于存放需要异步执行的内容。主线程执行完毕后,会不断循环扫描任务队列中的任务,直到队列为空。

图解说明:

一、JS异步的执行原理

如图所示,假设小明学习耗时较长,若未完成则无法玩DNF游戏,因此他将学习放入异步任务队列,先玩游戏(主线程),再学习(任务队列)。期间母亲添加的学习事件(DOM事件)会被处理,小明每完成一个学习任务便检查队列中是否有新任务(循环扫描),直至所有任务完成。

再看一个浏览器刷新并不断点击按钮的例子:


  let myData = null
  //ajax请求
  function ajax() {
  //腾讯新冠实时数据接口,仅做学习
  axios.get('https://api.inews.qq.com/newsqa/v1/query/inner/publish/modules/list?modules=chinaDayList,chinaDayAddList,nowConfirmStatis,provinceCompare')
   .then(data => {
   console.log("ajax返回成功");
   myData = data.data
   console.log(myData);

   })
   .catch(error => {
   console.log("ajax返回失败");
   })
  }
  console.log(myData);
  ajax()
  setTimeout(() => {
  console.log('定时器');
  }, 2000);
  console.log(myData);
  const btn = document.querySelector('button')
  btn.onclick = () => {
  console.log("点击了");
  }

null null ajax返回成功 Object 点击了 定时器 点击了

可见,console在主线程中是同步执行的,因此先输出结果。而主线程外的任务队列中存放着异步内容,包括setTimeout、Ajax和DOM事件,它们按照任务队列的顺序依次执行(通过循环扫描队列)。

为何需要循环扫描?

通过点击事件可以看出,当用户进行交互(如点击、滚动、窗口大小变化等)时,会向事件循环的任务队列添加新事件并等待执行,因此必须通过循环扫描来处理这些动态添加的事件。

二、JS异步中的回调

既然异步任务都放在最后的任务队列中执行,许多逻辑难以直接实现。此时我们需要处理这种异步逻辑,最常用的方式是回调(Callback),即“回头调用”。

回调函数:简单来说,当函数A传入函数B作为参数时,函数B即为A的回调函数。回调分为嵌套回调和链式回调两种形式。

以下是回调的一个简单用法示例:


   let myData = null
   console.log(myData);
   setTimeout(() => {
    console.log('定时器');
   }, 2000);
   const btn = document.querySelector('button')
   btn.onclick = () => {
    console.log("点击了");
   }
   let name = "张三"
   function hr(callback) {
    setTimeout(() => {
     console.log(`我是${name}`);
     callback();
    }, 2001);
   }
   console.log(myData);
   function gj() {
    console.log(`${name}你好,我是李四,认识一下吧`);
   }
   hr(gj)

null null 点击了 定时器 我是张三 张三你好,我是李四,认识一下吧 点击了

很明显,当函数需要获取数据时就会用到回调,此处使用的是异步回调。

虽然回调是解决异步问题的常用方法,但随着JS需求日益复杂,同步与异步逻辑需要越来越多的回调来实现。同步与异步混杂以及过多的回调嵌套和缩进,使得代码难以解读和维护,形成了所谓的“回调地狱”。

一、JS异步的执行原理

我们看一个具体的例子:


const verifyUser = function(username, password, callback){
  dataBase.verifyUser(username, password, (error, userInfo) => {
    if (error) {
      callback(error)
    }else{
      dataBase.getRoles(username, (error, roles) => {
        if (error){
          callback(error)
        }else {
          dataBase.logAccess(username, (error) => {
            if (error){
              callback(error);
            }else{
              callback(null, userInfo, roles);
            }
          })
        }
      })
    }
  })
};

大多数人仅看到上述代码便会感到困惑。如果一个项目中包含上百个这样的代码块,过一段时间后,即便是编写者也会感到头疼。维护此类项目如同置身地狱。

此外,回调还存在信任问题,它将执行控制权交给了第三方(如Ajax)。为解决信任问题,必须在程序中编写各种逻辑来应对回调带来的潜在风险。

· 调用过早

· 调用过晚

· 调用次数过多或过少,未能将所需参数成功传递给回调函数

· 可能出现的错误被吞没。

可以看出,编写特定逻辑来解决这些特定的信任问题,其难度已远超应用本身的价值,且会导致代码冗杂、可读性差等问题。

综上所述,回调解决异步存在以下缺陷:

1)不符合人类处理任务的逻辑思维

2)回调带来的信任问题。

面对回调日益明显的弊端,ES6引入了Promise来解决异步问题。下一篇将介绍ES6中的Promise。

总结

到此这篇关于JS异步的执行原理和回调的文章就介绍到这了,更多相关JS异步执行原理回调内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章