-
Notifications
You must be signed in to change notification settings - Fork 0
Description
回调介绍
Javascript中许多动作都是异步的。
举个例子,来看看loadScript(src):
function loadScript(src) {
let script = document.createElement('script');
script.src = src;
document.head.append(script);
}这个函数的目的是加载一个新的script。当在document中插入了<script src="...">时,浏览器就会加载并且执行这个脚本。
应用如下:
// 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);
}如果我们要在脚本中调用一些新的函数,就要在回调中写入:
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-based"风格。一个函数要异步的做些事情,那必须把它作为回调传入另外一个函数,在这个函数做完某些事情后再运行回调。
在这里我们用loadScrip演示,但回调是一个常用的方法。
回调中的回调
如何有序的加载2个脚本:先加载一个,接着再加载第二个?
显然可以通过把第二个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。
那么再加一个脚本呢?
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的用法也是非常常见。这叫做"error-first callback"风格。
约定如下:
1.回调的第一个参数是处理错误,然后callback(err)被调用。
2.第二个参数处理成功(如果需要继续再加参数),调用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.我们加载1.js,然后如果没有报错。
2.我们加载2.js,然后如果没有报错。
3.我们加载3.js,然后如果没有报错就做一些其他的事情(*)。
作为嵌套调用,代码变得更深切增加了管理的难度,特别是如果我们有更多的代码代替上面例子中的...,可能是循环,条件等语句A。
这就是所谓的“回调地狱“或者叫做”厄运金字塔“。
随着嵌套回调,如上图"pyramid"在每次异步动作后往右生长,代码逐渐缠绕致使失控。
所以这种代码不好。
我们能够通过声明独立的函数处理每一步动作来缓和问题,如下:
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 (*)
}
};看到没?我们做到了,并且不再有深度嵌套了,这是因为我们为每一步动作声明了单独的顶级函数。
虽然实现了功能,但代码却看起来像是撕散割裂的部分。你可能注意到了它难于阅读。必须在2个区块来回切换来阅读理解。这非常的不方便,特别是阅读者不熟悉代码,不知道要哪些区块是功能相关可联系切换的。
另外为命名为step*的函数仅仅单纯地回避了”厄运金字塔“的问题。没有人会在这一系列的动作链之外重复使用它们。所以这会稍微扰乱命名空间。
我们想要更好的方式。
幸运的是有其他的方法避免”厄运金字塔”。其中一个最好的方式就是使用“promises”,详见下章。
任务
用回调动态生成圆
这个任务动态生成圆。
我们不仅需要一个圆,并且在里面显示消息。这个信息需要在动画完成后再显示,不然看起来很丑。
为了完成任务,定义一个`showCircle(cx,cy,radius)`函数画圆,但是没有监控它完成与否。
添加一个回调作为参数:showCircle(cx,cy,radius,callback),当动画完成后调用。这个callback要接受<div>作为参数。
举例如下:
showCircle(150, 150, 100, div => {
div.classList.add('message-ball');
div.append("Hello, world!");
});