JavaScript环境存在了许多异步函数,如网络请求、读取文件等。还有一些函数如setTimeout、setInterval等允许我们执行异步行为。换句话说,我们执行代码并不一定会按照代码的顺序来执行,如下面是一个异步计算两个数相加的函数
let count;
function asyncSum(num1, num2) {
setTimeout(() => {
count = num1 + num2;
}, 1000);
}
asyncSum(1,2);
console.log(count);
猜猜上面的执行结果是什么? 问题来了,既然我们执行代码并不一定会按照代码的顺序来执行,我们如何拿到asyncSum的计算结果呢?
callback
我们可以将其写在回调函数中,回调函数不是js中独有的概念,回调函数是指通过参数将函数传递到其它代码的,某一块可执行代码的引用。这一设计允许了底层代码调用在高层定义的子程序。
将上面的代码我们改造一下:
function asyncSum(num1, num2, callback) {
setTimeout(() => {
let count = num1 + num2;
callback(count);
}, 1000);
}
现在我们就可以通过下面这种方式使用count了
asyncSum(1,2,(val) => console.log(`我拿到count了:${val}`))
在上述示例中,我们并没有考虑出现 error 的情况,如果asyncSum执行失败怎么办?我们的回调应该能够对此作出反应。
function asyncSum(num1, num2, callback) {
setTimeout(() => {
let count;
try {
count = num1 + num2;
} catch (error) {
callback(error);
return;
}
callback(null, count);
}, 1000);
}
现在我们可以这样子使用了
asyncSum(1,2,(error, val) => console.log(`我拿到count了:${val}`))
上面所使用的方法其实很普遍。它被称为“Error优先回调(error-first callback)”风格。 约定是: callback 的第一个参数是为 error 而保留的。一旦出现 error,callback(err) 就会被调用。 第二个参数(和下一个参数,如果需要的话)用于成功的结果。此时 callback(null, result1, result2…) 就会被调用。 因此,单一的 callback 函数可以同时具有报告 error 和传递返回结果的作用 node中的大量api均是采用的Error优先回调如下面是官网的一段事例
fs.open('myfile', 'r', (err, fd) => {
if (err) {
if (err.code === 'ENOENT') {
console.error('myfile does not exist');
return;
}
throw err;
}
readMyData(fd);
});
乍一看,这是一种可行的异步编程方式。的确如此,对于一个或两个嵌套的调用看起来还不错。 但对于一个接一个的多个异步行为,代码将会变成这样:
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 {
// ...加载完所有脚本后继续 (*)
}
});
}
})
}
});
如果调用嵌套的增加,代码层次变得更深,维护难度也随之增加,尤其是我们使用的是可能包含了很多循环和条件语句的真实代码,有时这些被称为“回调地狱”,这也是前期js被广为诟病的一个地方。
promise
关于promise的详细介绍见developer.mozilla.org/zh-CN/docs/… Promises是一种模式,它能够让异步操作变得看起来更像是同步的,本质上Promise是一个函数返回的对象,我们可以在它上面绑定回调函数,这样我们就不需要在一开始把回调函数作为参数传入这个函数了。 不同于“老式”的传入回调,在使用 Promise 时,会有以下约定:
- 在本轮 事件循环 运行完成之前,回调函数是不会被调用的。
- 即使异步操作已经完成(成功或失败),在这之后通过 then() 添加的回调函数也会被调用。
- 通过多次调用 then() 可以添加多个回调函数,它们会按照插入顺序执行。
改造一下我们的代码:
function asyncSum(num1, num2) {
return new Promise((res, rej) => {
setTimeout(() => {
let count;
try {
count = num1 + num2;
res(count);
} catch (error) {
rej(error)
}
}, 1000);
});
}
现在我们可以去掉回调函数数了,Promise 很棒的一点就是链式调用(chaining)
asyncSum(1,3).then((val) => {
console.log(val)
return asyncSum(val, 4);
}).then(val => {
console.log(`第二层val${val}`);
//也可以返回同步代码
return val;
}).then(val => {
console.log(`第三层val${val}`);
}).catch(error => console.log(error));
promise + async/await
在 ECMAScript 2017 标准的 async/await 语法糖中,这种与同步形式代码的对称性得到了极致的体现:
(async() => {
let res1 = await asyncSum(1,3);
console.log(res1);
let res2 = await asyncSum(res1,4)
console.log(`第二层val${val}`);
})()
待完善。。。。。