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
对象需要两个特定的键——list
和delimiter
。这个 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 实际上是一个承诺,但对于程序员来说,它更像是同步代码。
从我们目前的代码中,让我们摆脱then
async/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回调地狱 相关