JavaScirpt回调地狱,是真实存在的。开发人员经常将回调视为纯粹的邪恶,甚至到了避免它们的地步。JavaScript 的灵活性对此毫无帮助。但没有必要避免回调。好消息是可以通过简单的步骤从回调地狱中拯救出来。

消除代码中的回调就像截掉一条好腿。回调函数是 JavaScript 的支柱之一,也是它的优点之一。当您替换回调时,您通常只是在交换问题。

有人说回调是丑陋的疣,是学习更好的语言的原因。那么,回调有那么难看吗?

在 JavaScript 中使用回调有它自己的一套奖励。没有理由避免使用 JavaScript,因为回调会变成丑陋的缺陷。我们只能确保这种情况不会发生。

让我们深入了解声音编程必须提供的回调。我们的偏好是坚持SOLID 原则,看看这会将我们带向何方。

1、什么是 JavaScript回调地狱?

您可能想知道回调是什么以及为什么要关心。在 JavaScript 中,回调是充当委托的函数。委托在未来的任意时刻执行。在 JavaScript 中,委托发生在接收函数调用回调时。接收函数可以在其执行过程中的任意点执行此操作。

简而言之,回调是作为参数传递给另一个函数的函数。没有立即执行,因为接收函数决定何时调用它。以下代码示例说明:

function receiver(fn) {
  return fn();
}

function callback() {
  return 'foobar';
}

var callbackResponse = receiver(callback); 
// callbackResponse == 'foobar'

如果您曾经编写过 Ajax 请求,就会遇到回调函数。异步代码使用这种方法,因为无法保证回调何时执行。

setTimeout(function (name) {
  var catList = name + ',';

  setTimeout(function (name) {
    catList += name + ',';

    setTimeout(function (name) {
      catList += name + ',';

      setTimeout(function (name) {
        catList += name + ',';

        setTimeout(function (name) {
          catList += name;

          console.log(catList);
        }, 1, 'Lion');
      }, 1, 'Snow Leopard');
    }, 1, 'Lynx');
  }, 1, 'Jaguar');
}, 1, 'Panther');

看上面的代码,setTimeout得到一个一毫秒后执行的回调函数。最后一个参数只是为回调提供数据。这类似于 Ajax 调用,只是返回name参数来自服务器。

在我们的代码中,我们通过异步代码收集了一个凶猛的猫的列表。每个回调给我们一个猫的名字,我们将其附加到列表中。我们试图实现的目标听起来很合理。但考虑到 JavaScript 函数的灵活性,这是一场噩梦。

匿名函数

请注意前面示例中匿名函数的使用。匿名函数是未命名的函数表达式,它们被分配给一个变量或作为参数传递给其他函数。

某些编程标准不建议在您的代码中使用匿名函数。最好给它们命名,所以使用function getCat(name){}而不是function (name){}. 将名称放在函数中可以使您的程序更加清晰。这些匿名函数很容易输入,但它们会让你在高速公路上疾驰而下。当您发现自己正沿着这条蜿蜒的压痕之路前行时,最好停下来重新思考。

打破这种乱七八糟的回调的一种天真的方法是让我们使用函数声明:

setTimeout(getPanther, 1, 'Panther');

var catList = '';

function getPanther(name) {
  catList = name + ',';

  setTimeout(getJaguar, 1, 'Jaguar');
}

function getJaguar(name) {
  catList += name + ',';

  setTimeout(getLynx, 1, 'Lynx');
}

function getLynx(name) {
  catList += name + ',';

  setTimeout(getSnowLeopard, 1, 'Snow Leopard');
}

function getSnowLeopard(name) {
  catList += name + ',';

  setTimeout(getLion, 1, 'Lion');
}

function getLion(name) {
  catList += name;

  console.log(catList);
}

您不会在 repo 中找到此代码段,但可以在此提交中找到增量改进。

每个函数都有自己的声明。一个好处是我们不再看到可怕的金字塔。每个功能都被隔离开来,并专注于自己的特定任务。每个功能现在都有一个改变的理由,所以这是朝着正确方向迈出的一步。请注意getPanther(),例如,被分配给参数。JavaScript 不关心我们如何创建回调。但缺点是什么?

但是,缺点是每个函数声明不再在回调内限定范围。现在,每个函数都粘附到外部范围,而不是使用回调作为闭包。因此,为什么要catList在外部范围内声明,因为这会授予回调访问列表的权限。有时,破坏全局范围并不是一个理想的解决方案。还有代码重复,因为它将猫附加到列表并调用下一个回调。

这些是从回调地狱继承而来的代码味道。有时,努力进入回调自由需要毅力和对细节的关注。它可能开始感觉好像疾病比治疗更好。有没有办法更好地编码?

依赖倒置

依赖倒置原则说我们应该编码抽象,而不是实现细节。在核心,我们将一个大问题分解成小的依赖关系。这些依赖关系变得独立于实现细节无关的地方。

这个 SOLID 原则指出

当遵循这一原则时,从高级策略设置模块到低级依赖模块建立的传统依赖关系被逆转,从而使高级模块独立于低级模块实现细节。

那么这个文本块是什么意思呢?好消息是,通过为参数分配回调,我们已经在这样做了!至少在某种程度上,要解耦,请将回调视为依赖项。这种依赖关系成为一种契约。从现在开始,我们正在进行 SOLID 编程。

获得回调自由的一种方法是创建合约:

fn(catList);

这定义了我们计划如何处理回调。它需要跟踪一个参数——即我们的凶猛猫的列表。

这种依赖性现在可以通过一个参数得到:

function buildFerociousCats(list, returnValue, fn) {
  setTimeout(function asyncCall(data) {
    var catList = list === '' ? data : list + ',' + data;

    fn(catList);
  }, 1, returnValue);
}

请注意,函数表达式asyncCall的作用域为闭包buildFerociousCats。当与异步编程中的回调相结合时,这种技术非常强大。合约异步执行并获得data它需要的东西,所有这些都是通过合理的编程实现的。合约获得了它需要的自由,因为它与实现脱钩了。优美的代码利用了 JavaScript 的灵活性来发挥其自身优势。

其余需要发生的事情变得不言而喻。我们做得到:

buildFerociousCats('', 'Panther', getJaguar);

function getJaguar(list) {
  buildFerociousCats(list, 'Jaguar', getLynx);
}

function getLynx(list) {
  buildFerociousCats(list, 'Lynx', getSnowLeopard);
}

function getSnowLeopard(list) {
  buildFerociousCats(list, 'Snow Leopard', getLion);
}

function getLion(list) {
  buildFerociousCats(list, 'Lion', printList);
}

function printList(list) {
  console.log(list);
}

多态回调

好吧,让我们疯狂一点。如果我们想将创建逗号分隔列表的行为更改为竖线分隔列表怎么办?我们可以设想的一个问题是buildFerociousCats已经粘附到一个实现细节上。请注意使用list + ',' + data来执行此操作。

简单的答案是带有回调的多态行为。原则仍然是:将回调视为合同,并使实现无关紧要。一旦回调提升为抽象,具体的细节就可以随意改变。

多态性开辟了 JavaScript 中代码重用的新途径。将多态回调视为定义严格契约的一种方式,同时允许足够的自由,实现细节不再重要。请注意,我们仍在谈论依赖倒置。多态回调只是一个奇特的名字,它指出了一种将这个想法进一步发展的方法。

让我们定义契约。我们可以在这个合约中使用list和参数:data

cat.delimiter(cat.list, data);

然后我们可以做一些调整buildFerociousCats

function buildFerociousCats(cat, returnValue, next) {
  setTimeout(function asyncCall(data) {
    var catList = cat.delimiter(cat.list, data);

    next({ list: catList, delimiter: cat.delimiter });
  }, 1, returnValue);
}

JavaScript 对象cat现在封装了list数据和delimiter函数。回调链接异步回调——next以前称为fn. 请注意,可以自由地使用 JavaScript 对象对参数进行分组。该cat对象需要两个特定的键——listdelimiter。这个 JavaScript 对象现在是合约的一部分。其余代码保持不变。

要启动它,我们可以这样做:

buildFerociousCats({ list: '', delimiter: commaDelimiter }, 'Panther', getJaguar);
buildFerociousCats({ list: '', delimiter: pipeDelimiter }, 'Panther', getJaguar);

回调被交换。只要合同得到履行,实施细节就无关紧要了。我们可以轻松地改变行为。回调,现在是一个依赖,被转化为一个高级契约。这个想法将我们已经知道的关于回调的知识提升到了一个新的水平。减少对契约的回调提升了抽象并解耦了软件模块。

这里非常激进的是,单元测试自然地从独立的模块中流出。delimiter合约是一个纯函数。这意味着,给定多个输入,我们每次都会得到相同的输出。这种可测试性水平增加了解决方案将起作用的信心。毕竟,模块化独立性赋予了自我评估的权利。

管道分隔符周围的有效单元测试可能如下所示:

describe('A pipe delimiter', function () {
  it('adds a pipe in the list', function () {
    var list = pipeDelimiter('Cat', 'Cat');

    assert.equal(list, 'Cat|Cat');
  });
});

承诺

promise只是回调机制的包装器,并允许thenable继续执行流程。这使代码更可重用,因为您可以返回一个承诺并链接该承诺

让我们在多态回调之上构建并将其包装在一个承诺中。调整buildFerociousCats函数并让它返回一个承诺:

function buildFerociousCats(cat, returnValue, next) {
  return new Promise((resolve) => { //wrapper and return Promise
    setTimeout(function asyncCall(data) {
      var catList = cat.delimiter(cat.list, data);

      resolve(next({ list: catList, delimiter: cat.delimiter }));
    }, 1, returnValue);
  });
}

注意使用resolve: 而不是直接使用回调,这是解决承诺的原因。消费代码可以应用 athen来继续执行流程。

因为我们现在要返回一个承诺,所以代码必须在回调执行中跟踪这个承诺。

让我们更新回调函数以返回承诺:

function getJaguar(cat) {
  return buildFerociousCats(cat, 'Jaguar', getLynx); // Promise
}

function getLynx(cat) {
  return buildFerociousCats(cat, 'Lynx', getSnowLeopard);
}

function getSnowLeopard(cat) {
  return buildFerociousCats(cat, 'Snow Leopard', getLion);
}

function getLion(cat) {
  return buildFerociousCats(cat, 'Lion', printList);
}

function printList(cat) {
  console.log(cat.list); // no Promise
}

最后一个回调没有链接承诺,因为它没有返回承诺。跟踪承诺对于保证最后的延续很重要。打个比方,当我们做出承诺时,兑现承诺的最好方法就是记住我们曾经做出过承诺。

现在让我们用 thenable 函数调用更新主调用:

buildFerociousCats({ list: '', delimiter: commaDelimiter }, 'Panther', getJaguar)
  .then(() => console.log('DONE')); // outputs last

如果我们运行代码,我们会看到最后打印“DONE”。如果我们忘记在流程中的某处返回一个承诺,“完成”将出现乱序,因为它失去了对原始承诺的跟踪。

异步/等待

最后,我们可以将 async/await 视为 promise 的语法糖。对于 JavaScript,async/await 实际上是一个承诺,但对于程序员来说,它更像是同步代码。

从我们目前的代码中,让我们摆脱thenasync/await 并将调用包装起来:

async function run() {
  await buildFerociousCats({ list: '', delimiter: pipeDelimiter }, 'Panther', getJaguar)
  console.log('DONE');
}
run().then(() => console.log('DONE DONE')); // now really done

输出“DONE”在 之后立即执行await,因为它的工作方式很像同步代码。只要调用buildFerociousCats返回一个承诺,我们就可以等待调用。将async函数标记为一个返回承诺的函数,因此仍然可以将调用链接到run另一个then. 只要我们调用的内容返回一个承诺,我们就可以无限期地链接承诺。

请记住,所有这些异步代码都在单个线程的上下文中运行。JavaScript 回调非常适合这种单线程范例,因为回调以不会阻止进一步执行的方式排队。这使得 JavaScript 引擎更容易跟踪回调,并立即获取回调,而无需处理同步多个线程。

结论

掌握 JavaScript 中的回调就是了解所有细节。我希望你能看到 JavaScript 函数的细微变化。当我们缺乏基础知识时,回调函数就会被误解。一旦 JavaScript 函数清晰,SOLID 原则很快就会出现。它需要对基础知识有很强的掌握才能尝试 SOLID 编程。语言固有的灵活性将责任的重担放在了程序员身上。

JavaScirpt回调地狱 相关

使用函数表达式与函数声明的时机

适合初学者的PHP书籍

解决.NET 6应用程序中的性能瓶颈-性能优化