Javascript回调(一):简述

本文深入讲解JavaScript中的回调机制,探讨其在异步编程中的应用,包括如何通过回调处理脚本加载、错误管理和复杂的异步任务流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Javascript回调(一):简述

Javascript中大多数操作都为异步执行。举例,看看loadScript(src):

function loadScript(src) {
  let script = document.createElement('script');
  script.src = src;
  document.head.append(script);
}

该函数的目的是加载一个新脚本,当把<script src="...">增加至document中,浏览器载入并执行。
我们可以这么使用:

// loads and executes the script
loadScript('/my/script.js');

这个函数是异步执行,因为加载脚本动作不一定立刻完成,但后面会加载完。调用开始加载脚本,然后继续执行。正在加载脚本的同时,下面的代码可能完成执行,如果加载耗时,其他代码也可能同时执行。

loadScript('/my/script.js');
// the code below doesn't wait for the script loading to finish

现在,假设当加载时需使用脚本,可能声明了新的函数,所以我们希望运行函数。
当在loadScript(...)代码后面立刻调用,可能不工作:

loadScript('/my/script.js'); // the script has "function newFunction() {…}"

newFunction(); // no such function!

自然地,浏览器可能没有时间加载脚本,loadScript函数没有提供一种方式跟踪加载完成事件。最终脚本加载运行,但最好加载好我们能知道,并使用脚本中函数或变量。

让我们增加callback函数作为loadScript函数的第二个参数,当脚本完成加载时执行:

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(script);

  document.head.append(script);
}

现在,如果我们想调用脚本中的函数,我们应该写在callback中:

loadScript('/my/script.js', function() {
  // the callback runs after the script is loaded
  newFunction(); // so now it works
  ...
});

这样理念是:第二个参数通常是函数,当动作完成时运行。这里是一个可运行的真实示例:

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;
  script.onload = () => callback(script);
  document.head.append(script);
}

loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
  alert(`Cool, the ${script.src} is loaded`);
  alert( _ ); // function declared in the loaded script
});

这就是所谓“基于回调”异步编程风格。需要异步处理业务的函数应该提供callback参数,即函数完成之后执行的代码。

这里我们示例loadScript,当然是一个通用的方法。

回调里面的回调

如何顺序载入两个脚本:先载入第一个,然后接着第二个?

自然的解决方案是放第二个载入脚本在第一个回调里,如下方式:

loadScript('/my/script.js', function(script) {

  alert(`Cool, the ${script.src} is loaded, let's load one more`);

  loadScript('/my/script2.js', function(script) {
    alert(`Cool, the second script is loaded`);
  });

});

外面的loadScript完成,开始调用里面的,如果有更多的脚本会怎么?

loadScript('/my/script.js', function(script) {

  loadScript('/my/script2.js', function(script) {

    loadScript('/my/script3.js', function(script) {
      // ...continue after all scripts are loaded
    });

  })

});

每个新的动作在回调里面,如果层次不多还好,当层次较多时很不好,所以我们将看其他方式。

处理错误

上面的示例没有考虑错误发生,如果脚本失败怎么?回调应该能够对失败情况有反应。
下面是loadScript的改进版本,跟踪错误发生:

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`Script load error ` + src));

  document.head.append(script);
}

加载成功调用callback(null, script),否则调用callback(error).用法示例:

loadScript('/my/script.js', function(error, script) {
  if (error) {
    // handle error
  } else {
    // script loaded successfully
  }
});

实际这样方式使用loadScript很常见,一般称为“错误优先调用”。约定为:

  1. callback的第一个参数用于发生错误时的错误处理逻辑,则callback(err)被调用。
  2. 第二个参数(如果需要可能有多个参数)为成功时调用,那么调用callback(null,result1,result2).

所以一个回调函数既可以报告错误,也可以传回调用结果。

金字塔

第一眼看到可行的异步方式代码,一般一个或两个嵌套调用还好。但多个一个接着一个的异步动作,代码大概如下:

loadScript('1.js', function(error, script) {

  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('2.js', function(error, script) {
      if (error) {
        handleError(error);
      } else {
        // ...
        loadScript('3.js', function(error, script) {
          if (error) {
            handleError(error);
          } else {
            // ...continue after all scripts are loaded (*)
          }
        });

      }
    })
  }
});

上面的代码:

  1. 载入1.js,如果没有错误。
  2. 载入2.js,如果没有错误。
  3. 载入3.js,如果么有错误,做其他事情(*号行)

当多个调用嵌套时,代码缩进更深,增加管理难度。特别如果有真实业务代码代替…时,因为可能会包括更多循环、条件语言等。
有时称为“回调地狱”或“末日金字塔”。

嵌套调用金字塔向右增长每个异步动作,很快螺旋方式失去控制。所以这种代码方式不好。

我们能试着解决这个问题,每个动作采用一个独立函数,如下:

loadScript('1.js', step1);

function step1(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('2.js', step2);
  }
}

function step2(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('3.js', step3);
  }
}

function step3(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...continue after all scripts are loaded (*)
  }
};

看到吗?效果一样,但没有深层的嵌套,因为我们分离每个动作作为一个顶级函数。
可以实现,但代码看起来想分裂的电子表格,很难阅读,你可能注意到,需要在不同代码片段间跳跃。确实不方便,特别当读者不属性代码,眼睛不知道往哪跳跃。

以step*方式命名函数为了独立使用,创建他们仅为了消除金字塔,并不打算在外面调用链中重用他们,所以可能会让命名混乱。

我们希望有其他更好的方法。

幸运的是,去有其他方式可以消除金字塔。最好的方式使用“promises”,在下一章进行描述。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值