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
很常见,一般称为“错误优先调用”。约定为:
- callback的第一个参数用于发生错误时的错误处理逻辑,则
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 {
// ...continue after all scripts are loaded (*)
}
});
}
})
}
});
上面的代码:
- 载入1.js,如果没有错误。
- 载入2.js,如果没有错误。
- 载入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”,在下一章进行描述。
本文参考链接:https://blog.csdn.net/neweastsun/article/details/73007127