《TypeScript实战指南》—2.3.3 回调函数和 promise
2.3.3 回调函数和 promise
为了能充分体会到 promise 的好处,先让我们回到过去,看看在 ES5时代如何处理下面这个看似简单的例子。这个例子将会为我们证明一件事,那就是使用回调函数来创建异步代码,会使代码的可读性变得非常糟糕。
例子是先同步读取文件然后将内容转换成 JSON 字符串:
const fs = require('fs');
function loadJSONSync(filePath: string) {
return JSON.parse(fs.readFileSync(filePath));
}
console.log(loadJSONSync('test.json'));
为了便于理解,我们忽略了一些可能发生的异常行为,比如 test.json 里面是一个无效的JSON 文件,或者 test.json 本身不存在。
现在再让我们写一个异步的版本:
const fs = require("fs");
function loadJSON(
filePath: string,
callback: (error: Error, data?: any) => void
) {
fs.readFile(filePath, function(error, data) {
if (error) {
callback(error);
} else {
callback(null, JSON.parse(data));
}
});
}
虽然我们处理了文件读写的异常,但在并没有处理 JSON.parse 的异常,这可能会导致抛出异常。
在使用基于回调方式的异步函数时需要记住如下准则:
一定不要调用两次回调。
一定不要抛出错误。
然而,上面示例中的函数违背了第二条准则。
一个最简单天真的解决方案是使用 try catch 将 JSON.parse 包裹起来,就像下面这样:
const fs = require("fs");
function loadJSON(
filePath: string,
callback: (error: Error, data?: any) => void
) {
fs.readFile(filePath, function(error, data) {
if (error) {
callback(error);
} else {
try {
callback(null, JSON.parse(data));
} catch (error) {
callback(error);
}
}
});
}
然而在这份代码中有一个非常奇妙的 bug,很难用肉眼发现。如果回调函数有错误,经过 try catch 捕捉后,将会再执行一次。所以同步代码需要放在 try catch 中,回调函数则要放在另外的位置。
遵循上面的准则,我们可以实现一个具有高完成度版本的异步执行函数 loadJSON,如下所示:
const fs = require("fs");
function loadJSON(
filePath: string,
callback: (error: Error, data?: any) => void
) {
fs.readFile(filePath, function(error, data) {
if (error) {
callback(error);
} else {
let result;
try {
result = JSON.parse(data);
} catch (error) {
callback(error);
}
callback(null, result)
}
});
}
是不是已经开始感觉有点复杂了,虽然我们只是写了一个读取文件再转换成 JSON 的小段代码,但回调方式使得问题变得复杂了许多。
接下来让我们看看如何使用promise 来更好地处理这个问题。
1.创建 promise
promise 是拥有过程状态的,这很像我们前面学习的迭代器。但不同的是它的状态更为简单,只有 pending、resolved、rejected三种情况。
promise 通过 Promise 构造器创建,reslove 和 reject 是两个参数,分别处理成功或失败的情况。让我们先来构造一个 promise:
const promise = new Promise((resolve, reject) => {
});
2.订阅 promise
promise可以使用then 或者 catch 来订阅:
const promise = new Promise((resolve, reject) => {
resolve(2333);
});
promise.then((res) => {
console.log(res); // 2333
});
promise.catch((err) => {
// 没有reject不被调用
});
const promise = new Promise((resolve, reject) => {
reject(new Error("something incorrect"));
});
promise.then((res) => {
// 没有 resolve 不被调用
});
promise.catch((err) => {
console.log(erre); // "something incorrect"
});
3. promise 的链式性
promise 的链式性是 promise 的核心优点。一旦你得到了一个 Promise 对象,从那一个 promise 起,使用 Promise.then 会不断地得到新的 promise,如下所示:
Promise.resolve(2333)
.then((res) => {
console.log(res); // 2333
return 23333333333;
})
.then((res) => {
console.log(res); // 23333333333
return Promise.resolve(2333333333333333333333);
})
.then((res) => {
console.log(res); // 2333333333333333333333
return Promise.resolve(23333333333333333333333333333333333333333333);
})
你可以将之前任何 promise 点上的异常都放在最后的 Promise.catch 中去处理,像下面这样:
Promise.reject(new Error('something incorrect'))
.then((res) => {
console.log(res); // 不会被调用
return 2333;
})
.then((res) => {
console.log(res); // 不会被调用
return Promise.resolve(23333333333);
})
.then((res) => {
console.log(res); // 不会被调用
return Promise.resolve(23333333333333333333333333333333333333333333);
})
.catch((err) => {
console.log(err.message); // something incorrect
});
Promise.catch 实际上仍然会返回一个新的 Promise 对象。
4.TypeScript 和 promise
TypeScript 的强大之处在于它可以通过 promise 链推测传递的值的类型:
Promise.resolve(2333)
.then(res => {
// (parameter) res: number
return true;
})
.then(res => {
// (parameter) res: boolean
});
这样的类型推导都难不倒 TypeScript。
下面我们将把回调风格的函数重构成为一个 promise。过程非常简单,只需将函数调用放到 promise 中,把错误挪到 Promise.reject 里,把没有报错的回调放到 Promise.resolve 里行了。好吧,让我们来动一下手吧:
const fs = require('fs');
function readFileAsync(filePath: string):Promise<any> {
return new Promise((resolve,reject) =>{
fs.readFile(filePath,(error,result) => {
if (error) {
reject(error);
} else {
resolve(result);
};
});
});
}
改造完毕文件读取之后,我们再来改造一下 loadJSONAysnc。当我们调用 readFileAsync 时,它已经是一个 promise了,我们直接将它作为返回进行操作就可以了,就像下面的例子一样:
function loadJSONAsync(filePath: string): Promise<any> {
return readFileAsync(filePath).then(function(result) {
return JSON.parse(result);
});
}
这个时候我们再来使用它,就变得非常优雅了,就像在同步调用一样:
loadJSONAsync("test.json")
.then(function(result) {
console.log(result);
})
.catch(function(error) {
console.log(error);
});
5.并行控制流
promise 为异步操作带来了便利性,这只是其优势的冰山一角。
如果你想执行一系列的异步任务,并在所有任务完成后执行操作,该怎么办呢?
这是非常常见的一个场景,比如,现在你有三个 API,分别是获取用户信息、获取购车信息、获取商品信息,三者信息同时拉取完你才能进行业务的渲染。
在这里,promise 提供了 Promise.all 函数,我们看一下如何使用它。
这需要运行一系列异步的任务,然后得到所有结果。promise 提供了静态的 Promise.all 函数,你可以使用它来等待 n 个 promise 完成。你提供给Promise.all一个包含了 n 个 promise 的数组,而Promise.all返回给你一个包括了 n 个 resolved 值的数组。如下所示:
// 通过 setTimeout 模拟向服务器拉取数据
function fethUserInfo(userId: string): Promise<{}> {
return new Promise(resolve => {
setTimeout(() => {
resolve({ userId });
}, 1000);
});
}
function fethCartInfo(userId: string): Promise<{}> {
return new Promise(resolve => {
setTimeout(() => {
resolve({ userId });
}, 1200);
});
}
function fethGoodInfo(goodId: string): Promise<{}> {
return new Promise(resolve => {
setTimeout(() => {
resolve({ goodId });
}, 1500);
});
}
Promise.all([fethUserInfo("1"), fethCartInfo("2"), fethGoodInfo("233")]).then(
res => {
console.log(res);
}
);
// (3) [{…}, {…}, {…}]
// 0: {userId: "1"}
// 1: {userId: "2"}
// 2: {goodId: "233"}
- 点赞
- 收藏
- 关注作者
评论(0)