异步编程
异步背景
基于回调的异步编程
有一些异步行为的例子,例如加载脚本和模块:
index.js
function loadScript(src) {
const script = document.createElement('script');
script.src = src;
document.head.append(script);
}
loadScript('./foo.js');
console.log(1);
foo.js
console.log(2);
function foo() {
console.log('foo');
}
打印顺序是 1、2。这说明脚本 foo.js 会在 loadScript 函数执行完成后才运行。如果 loadScript 函数后面有其他代码,它们不会等到脚本加载完成后再执行。
如果我想在脚本加载后立即使用脚本里的 foo
函数,直接调用会报错:
loadScript('./foo.js');
foo();
接下来,添加一个 callback 函数作为 loadScript 的第二个参数,该函数应在脚本加载完成时执行。
function loadScript(src, callback) {
const script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}
loadScript('./foo.js', script => {
foo();
});
将 foo 函数放在回调函数中就能正常工作了。
这就是基于回调的异步编程。异步执行某项功能的函数应该提供一个 callback 参数,并在相应事件完成时调用。
如果需要多个异步操作,可以嵌套回调函数:
loadScript('1.js', function (script) {
loadScript('2.js', function (script) {
loadScript('3.js', function (script) {
// ...加载完所有脚本后继续
});
});
});
处理 Error
考虑脚本加载失败的情况
function loadScript(src, callback) {
const script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));
document.head.append(script);
}
loadScript('./foo.js', (error, script) => {
if (error) {
// 处理error
} else {
// 脚本加载成功
foo();
}
});
callback 函数里接收两个参数,加载成功时调用callback(null, script)
,否则调用callback(error)
回调地狱
如果有多个异步操作,回调函数会嵌套在回调函数里,导致代码难以阅读和理解,这就是「回调地狱」。
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 {
// ...加载完所有脚本后继续
}
});
}
});
}
});
改造 loadScript
使用Promise
改造前面的示例,假设有 3 个脚本需要加载
function loadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = () => resolve(script);
script.onerror = () => reject(new Error(`Script load error for ${src}`));
document.head.append(script);
});
}
loadScript('./1.js')
.then(() => loadScript('./2.js'))
.then(() => loadScript('./3.js'))
.then(() => {
foo1();
foo2();
foo3();
});
注意:下面是没有使用链式调用的写法,会有和使用回调函数一样的问题。
loadScript('./1.js').then(() => {
loadScript('./2.js').then(() => {
loadScript('./3.js').then(() => {
foo1();
foo2();
foo3();
});
});
});
Promise
promise
用同步编程的方式来编写异步代码,解决回调嵌套问题。
new Promise((resolve, reject) => {});
三种状态
pending
进行中fulfilled
成功rejected
失败
特性:
- 初始状态是
pending
pending
状态可以转化为fulfilled
或者rejected
状态- 状态不可逆
- 最终状态是
fulfilled
并返回一个值,或者是rejected
并返回一个原因。
then 方法
- 分别指定
fulfilled
状态和rejected
状态的回调函数,第二个参数可选(不推荐使用)。 - 返回的是一个新的
Promise
,支持链式调用
function pro(params) {
return new Promise((resolve, reject) => {
if (params) {
resolve('hahaha');
} else {
reject('error');
}
});
}
pro(true).then(
res => {
console.log(res);
},
err => console.log(err)
);
注意
Promise
本身是同步的,then、catch 和 finally 都是异步的
const p = new Promise((resolve, reject) => {
console.log(1);
resolve(3);
});
p.then(res => console.log(res));
console.log(2);
结果是 1、2、3
catch 方法
function Cat(ready) {
return new Promise((resolve, reject) => {
if (ready) {
resolve('Tom');
} else {
reject('Kitty');
}
});
}
Cat(false)
.then(res => {
console.log(res);
})
.catch(err => console.log(err));
catch
方法可以捕获错误,作用和 then(onFulfilled, onRejected)
当中的 onRejected
函数类似。
Cat(false)
.then(res => {
console.log(tom);
})
.catch(err => console.log(err));
示例未定义变量 tom,如果不使用 catch 会直接报错,终止程序。使用后不会报错,但会将错误信息传递到 catch 方法中,方便处理。
再次抛出错误
- 如果在 catch 中 throw,那么控制权就会被移交到下一个最近的 catch
- 如果能处理 error ,那么控制权将继续到最近的 then
new Promise((resolve, reject) => {
throw new Error('Whoops!');
})
.catch(error => {
// 这里抛出错误,会执行下一个最近的catch
throw error;
})
.then(() => console.log('then'))
.catch(error => {
console.log(error);
});