原文地址:https://dev.to/bhagatparwinder/callback-functions-callback-hell-79n
在之前我们讨论事件处理器时已经接触了一些回调函数的概念,这篇文章我们将深入的探究回调函数以及它们是如何解决异步编程,还有它们的缺点以及什么是回调地狱。
回调函数是被当做参数传递给其它函数的函数,回调函数可以在被调用的函数内执行一些任务。
function greeting(name) {
console.log(`Hello ${name}`);
}
function getUserName(callback) {
const name = prompt("Enter your name");
callback(name);
}
getUserName(greeting);
这个例子发生了什么?
getUserName
传入一个参数被调用,参数是 greet
函数;getUserName
让用户输入用户名且保存到变量 name
中;getUserName
调用回调函数且传入 name
作为参数;geeting
)传入参数 name
执行且打印出 "Hello name"。以上是一个简单的回调函数的例子,具体来说它是同步回调。一切都被逐行执行,一个接一个。
注意:JavaScript 是单线程语言,只有一个线程执行代码。
其他语言可以同时启动多个线程和执行多个进程,但是 JavaScript 不行。当执行耗时操作例如磁盘 I/O 或是网络请求时这可能会是一个明显的缺点。
因为同一时刻只能执行一件事,用户必须等到耗时较长的任务执行完毕后才能进一步执行后续的动作。
JavaScript 的 事件循环、回调栈、回调队列以及 web 接口组成了它的异步。
有许多耗时任务像磁盘 I/O、网络请求和数据处理,这些需要放到异步中去执行。我们可以举一些直观的例子来解释说明:
console.log("Hello");
console.log("Hey");
console.log("Namaste");
当执行上面代码后,控制台打印如下:Hello Hey Namaste
,这是正确的执行顺序。现在我们来引入setTimeout
来包裹 "Hey",期望等待 2 秒后打印 "Hey"。
console.log("Hello");
setTimeout(() => {
console.log("Hey");
}, 2000);
console.log("Namaste");
令我们惊奇的是,它打印如下:"Hello Namaste Hey",期望的是先打印"Hello",等待 2 秒后打印 "Hey",最后打印 "Namaste"。
及时 setTimeout 是等待 0 秒,打印的顺序依旧是 "Hello Namaste Hey"。奇怪的是 0 秒应该是立即执行,可事实上并非如此。它依旧会像上面提到的代码一样经历事件循环。执行如下代码:
console.log("Hello");
setTimeout(() => {
console.log("Hey");
}, 0);
console.log("Namaste");
随着我们有更好的方法来解决异步操作,回调函数则变得越来越令人讨厌,其实我们没有必要这样对回调函数有敌意。当我们只有 1-2 个异步操作时,回调函数还是很好用的。
当我们需要处理多余 2 个异步任务链时,回调函数则显得捉襟见肘,让我们从例子来了解一下。
我们假设希望每间隔 2 秒打印输出问候语,输出如下:Hello Hey Namaste Hi Bonjour
。
setTimeout(() => {
console.log("Hello");
setTimeout(() => {
console.log("Hey");
setTimeout(() => {
console.log("Namaste");
setTimeout(() => {
console.log("Hi");
setTimeout(() => {
console.log("Bonjour");
}, 2000);
}, 2000);
}, 2000);
}, 2000);
}, 2000);
这种级联嵌套的代码称为回调地狱,很难调试也很难捕获错误,同样降低了代码的可读性。
在最后我们会留一张图,用于在以后的日子里时刻提醒大家关于回调地狱。后面的文章我们将谈论其余的异步方法:promise 、 async/await 和 observables。