目录

⛴ JavaScript 回调函数引入

# 更好地理解 JavaScript 中的回调函数

🌰 例子:

封装一个 加载脚本到页面 的函数:

function loadScript(src) {
	let script = document.createElement('script');
  script.src = src;
  document.head.append(script);
}
1
2
3
4
5

这个函数使用给定的 src 将一个新的、动态创建的标签 <script src="…"> 插入到文档中。浏览器将自动开始加载它,并在加载完成后执行它。

使用时,在给定路径下加载并执行脚本

loadScript('/my/script.js');
1

但是,脚本是异步调用的。因为它从现在开始加载,但是这个家在函数执行完成后才能运行。

如果 loadScript(...) 之下有 任何其他代码,它们不会等到 加载脚本完成后 才运行,如下:

假设脚本中有函数,想要在加载完成之后,立即执行它:

loadScript('/my/script.js'); // 这个脚本有 "function newFunction() {…}"

newFunction(); // 没有这个函数!
1
2
3

这样调用脚本里面的函数是没有效果的。因为浏览器可能没有时间家在脚本。并且 loadScript 函数并没有提供跟踪加载完成的方法。脚本加载并最终运行,仅此而已。

要使用脚本中的函数和变量,要了解脚本何时加载完成。给 loadScript 函数 添加第二个参数 作为回调函数,该函数应该在脚本加载完成时执行:

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

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

  document.head.append(script);
}
1
2
3
4
5
6
7
8

现在,要想调用脚本中的函数就可以在 callback 中写了:

loadScript('/my/script.js', function() {
  newFunction();
  // ...
});
1
2
3
4

完整的使用例子:

loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
  alert(`Cool, the script ${script.src} is loaded`);
  alert( _ ); // 所加载的脚本中声明的函数
});
1
2
3
4

通过这个 「加载脚本」的例子,了解到这是 「基于回调」的一步编程风格。异步执行某项功能的函数,应该提供一个 callback 参数用于在 相应事件完成时 调用。

# 回调中的回调

🌰 延续上面的 加载脚本 例子 / 如果想要在加载完第一个脚本后,加载另一个脚本、或者更多,把下一个加载写在回调中:

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`);
  });

});
1
2
3
4
5
6
7
8
9

谁用例子:

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

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

    loadScript('/my/script3.js', function(script) {
      // ...加载完所有脚本后继续
    });

  });

});
1
2
3
4
5
6
7
8
9
10
11

每一个加载脚本的行为都写在了 回调 中。这样的代码行为其实并不好。

# 回调中出错

🌰 在上面 加载脚本 的例子,并没有处理 加载出错(无法读取文件) 的情况。所以要添加处理错误的步骤:

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 for ${src}`));

  document.head.append(script);
}
1
2
3
4
5
6
7
8
9

如果加载成功就运行 callback 。加载失败,就调用 callback(error)

使用例子:

loadScript('/my/script.js', function(error, script) {
  if (error) {
    // 处理 error
  } else {
    // 脚本加载成功
  }
});
1
2
3
4
5
6
7

这种处理错误的风格成为「错误有限回调」(error-first callback):

  • callback 的第一个参数是为 error 而保留的。一旦出现 error, callback(err) 就会被调用。
  • 第二个参数(和下一个参数,如果需要的话)用于成功的结果。此时 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 {
            // ...加载完所有脚本后继续 (*)
          }
        });

      }
    });
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

随着代码的嵌套增加,代码层次变得更深层,维护难度提高。

虽然可以通过将每个回调划分出来独立的函数尝试解决这个问题:

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 {
    // ...加载完所有脚本后继续 (*)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

但是代码的可读性很差,需要在各个代码块之间跳转。并且这些函数应该是一次性使用的, step 之类的命名带来命名空间的混乱。

要避免 回调地狱 的问题,解决方法最好之一是 Promise 的使用。

# 更多异步编程的例子

异步编程相关例子(回调函数):

  • fs 文件操作:

    require('fs').readFile('./index.html', (err,data)=>{})
    
    1
  • 数据库操作;

  • AJAX 请求:

      $.get('/server', (data)=>{})
    
    1
  • 定时器:

    setTimeout(()=>{}, 2000);
    
    1
📢 上次更新: 2022/09/02, 10:18:16