《TypeScript实战指南》—2.3.3 回调函数和 promise

举报
华章计算机 发表于 2019/06/16 13:03:33 2019/06/16
【摘要】 本节书摘来自华章计算机《TypeScript实战指南》一书中的第2章,第2.3.3节,作者是胡桓铭。

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"}


【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。