将"回调地狱"按在地上摩擦的Promise

这是一段旁白

“异步虐我千百遍,我待异步如初恋”!!
做前端的同学做异步肯定都不陌生。因为JavaScript是单线程语言(也就是说不支持多线程编程,这不是废话么啊喂!),所以在JavaScript中处理异步问题也是经过了几代人的踩坑和开荒才有了今天的“花里胡哨”的解决方案。

回调(CallBack)

利用回调来实现异步是一直以来非常有效的解决方案,而且经久不衰。其背后的原理很简单,就是利用JavaScript中可以将函数作为参数传入另一个函数(万物皆对象)。举个栗子:

function callBack() {
    console.log('回调啦回调啦!!!');
}

function main(cb) {
    console.log('我会运行很久!')
    cb();
}

main(callBack);

下面一段代码中实现两个函数 callBackmain。随后将 callBack 传入到 main 函数中,当 main 函数执行到一个阶段时候会调用传入的回调函数 ( 此处是当main函数运行到底部时候就调用了回调函数 )。运行结果不言而喻:

《将

这样的写法看起来貌似还行,写法简单明了,一看就懂。但是这里笔者要吐槽下去年自己的智商,且听慢慢道来:
去年在重构项目的时候,有一个页面需要展示 4 个下拉框而且下拉框的数据需要从后台拉取。所以笔者在ComponentWillMount(React项目)方法中执行了拉取数据的动作而且是分开独立拉取,类似于:

......

ComponentWillMount() {
    let data = {};
    fetchSelect1();
    fetchSelect2();
    fetchSelect3();
    fetchSelect4();
}

......

最后在四个方法中将数据存储到 data 对象中以供渲染选择框,但是后面出现了一个意想不到问题:总会有一个下拉框数据拉取失败。所以不得已采用了回调方式来处理,这里再狠狠得吐槽一下自己,如果那时候会用Promise,也不会那么尴尬。下面贴一下当时的代码:

/* fetch data source prop selects */
router.get("/fetch-selects", function(req, resp, next) {
    let path1 = config.BACKEND_API.BASE_URL + '/datasource/frequency';
    var reponseData = {};
    httpAgent.httpRequest({}, "JSON", config.BACKEND_API.TYPE, config.BACKEND_API.HOST, config.BACKEND_API.PORT, path1, "GET", function(data) {
        reponseData.frequency = data;
        let path2 = config.BACKEND_API.BASE_URL + '/datasource/category';
        httpAgent.httpRequest({}, "JSON", config.BACKEND_API.TYPE, config.BACKEND_API.HOST, config.BACKEND_API.PORT, path2, "GET", function(data) {
            reponseData.category = data;
            let path3 = config.BACKEND_API.BASE_URL + '/datasource/type';
            httpAgent.httpRequest({}, "JSON", config.BACKEND_API.TYPE, config.BACKEND_API.HOST, config.BACKEND_API.PORT, path3, "GET", function(data) {
                reponseData.type = data;
                let path4 = config.BACKEND_API.BASE_URL + '/datasource/process/type';
                httpAgent.httpRequest({}, "JSON", config.BACKEND_API.TYPE, config.BACKEND_API.HOST, config.BACKEND_API.PORT, path4, "GET", function(data) {
                    reponseData.process = data;
                    resp.json(reponseData);
                }, function(code, body) {

                })
            }, function(code, body) {

            })
        }, function(code, body) {

        })
    }, function(code, body) {

    })
});

当时用的Node项目做的中间层,这是一个路由。可以看出来其实就是在拉取完第一条数据后再调用另一个函数来拉取第二条数据,如此嵌套下去。好在只需要拉取 4 条数据,那如果有10条乃至100条数据需要拉取怎么办?那岂不是需要嵌套出一个很深很深的代码结构么?这就是臭名昭著的“回调地狱”。“回调地狱”的问题在于写法过于繁琐不够优雅、代码维护炒鸡蛋疼,所以一直被前端程序猿所诟病,尤其是维护类似代码的时候简直日了一群哈士奇。不仅仅是想死的心了,完全想删库走人啊喂!

Promise

当前端异步工作处于水深火热中时,一个英雄踏着七彩祥云而来,他,就是 Promise。让我们相信:一个承诺,终究会被兑现。

Promise的由来

Promise 先由社区提出来的概念,主要是用于解决前端的异步问题,庆幸的是它在ES6中也得到了实现。

什么是Promise

Promise 是一个状态机。这么说可能有点不好懂,上个代码先:

new Promise(function(resolve, reject) {
    try {
        resolve('Success')
    } catch (e) {
        reject(e);
    }
})

从上面可以看出几个重要的点:
1,Promise是一个构造函数
2,新建Promise对象需要传入执行器函数 (executor function)。
3,执行器函数中有两个参数 resolvereject。这两个也是执行器函数。

对此来解释下什么叫状态机
Promise对象有三个状态:pending, fulfilled, rejected,没图说个JB?

《将

从图中可以看出,Promise对象的初始状态是pending ,如果经过了 resolve 方法,状态置为 fulfilled ;如果经过了 reject 方法,状态置为 rejected 。而且有三点需要明确:
1,Promise对象的状态转换只有 pending--->fulfilled 或者 pending--->rejected。没有其它形式的转换。
2,Promise 对象的状态一经转换则永久冻结,意思就是说比如状态被置为 fulfilled 后,无法再回到 pending。
3,Promise对象状态以resolvereject为分水岭。调用这个两个方法之前,都处于pending状态。

Promise.resolve()

Promise.resolve(value)方法返回一个以给定值 value 解析后的
Promise 对象

摘自MDN对 Promise.resolve() 的解释。简单的理解就是它用来返回任务执行成功后的返回值。Promise对象调用完这个方法后状态就被置为 fulfilled。

Promise.reject()

Promise.reject(reason)方法返回一个带有拒绝原因reason参数的
Promise 对象

摘自MDN对 Promise.reject() 的解释。Promise对象调用完这个方法后状态就被置为 rejected。

Promise.prototype.then()

看到这里可能会有这么一个问题:既然Promise用 resolve 和reject 返回处理结果,那如何获取到这个结果呢?那么then()就大有可为了。从小标题可以看出 then 方法被放在Promise的原型上,也就是说任何一个Promise对象都可以调用这个方法,不管何时何地。then()方法的参数为两个个执行器函数,第一个函数用来处理 resolve() 返回值,第二个函数用来处理 reject() 返回值。并且then()返回的也是一个 Promise 对象。举个🌰:
首先模拟 resolve() 返回

new Promise(function(reslove, reject) {
    try {
        reslove('Success')
    } catch (e) {
        reject(e);
    }
}).then(function(reslove_response) {
    console.log(reslove_response);
}, function(reject_response) {
    console.log(reject_response);
})

执行结果:

《将

首先模拟 reject() 返回

new Promise(function(reslove, reject) {
    try {
        throw new Error('发生错误了!')
    } catch (e) {
        reject(e);
    }
}).then(function(reslove_response) {
    console.log(reslove_response);
}, function(reject_response) {
    console.log(reject_response);
})

运行结果:

《将

检查 then() 返回的对象类型

let promise = new Promise(function(reslove, reject) {
    try {
        throw new Error('发生错误了!')
    } catch (e) {
        reject(e);
    }
}).then(function(reslove_response) {}, function(reject_response) {})

console.log(promise instanceof Promise)

运行结果:

《将

运行情况如预想一样。

说到这里会不会有这样一个问题:为什么这几个方法都是返回一个Promise对象? 请接着往下看。

Promise 链式调用

什么叫链式调用?

简单的来说就是酱紫的:

new Promise(function() {}).then(function() {}).then(function()).....

就是调用完 then() 以后还可以继续 then 下去,而且我们前面说了只有Promise对象里有then()方法,所以每个方法都需要返回一个Promise对象以支持链式调用。并且下一个then()可以拿到前一个then()的返回值,前提是前一个then()的确返回了一个值,继续举🌰:

new Promise(function(resolve, reject) {
    resolve(1);
}).then(function(result) {
    return result + 1;
}).then(function(result) {
    return result + 1;
}).then(function(result) {
    console.log(result + 1);
})

我们拿一个数字 1 出来,并且一直传递下去,每到一个then都给它加 1,最终结果是4。亲测无误,这就是Promise链式调用的基本形式,写法相对于回调函数那简直是一个优雅。另外,Promise的链式调用笔者觉得用的最多的地方就是连续处理多个任务,并且后一个任务需要用到前一个任务的返回值。如果不大理解,请再瞄一下刚刚的例子。

Promise.prototype.catch()

catch()这个方法大家都很熟悉,就是去捕获一个错误,当然在Promise里也是同样的作用。一般情况下会与链式调用搭配起来用,它会捕获前面任一步所抛出(reject 或者 throw)的错误。🌰来了:

new Promise(function(resolve, reject) {
    resolve(1);
}).then(function(result) {
    return result + 1;
}).then(function(result) {
    return result + 1;
}).then(function(result) {
    throw new Error('出错啦!')
}).catch(function(err) {
    console.log(err);
})

运行结果:
《将

注:catch()其实是then(null,function(){})的语法糖,将上述例子中的catch改成后者同样有效

Promise.all()

all()这个方法就厉害了。我们可以向其中传入一个可迭代的对象,对象中存有多个待执行的任务,Promise.all()会顺序执行并将结果按照原任务顺序存入数组返回或者当遇到任何一个任务出错的时候会抛出相应错误并且不做任何返回。🌰:

let p1 = 'Promise1';
let p2 = Promise.resolve('Promise2');
let p3 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 200, 'Promise3');
});

Promise.all([p1, p2, p3]).then(function(values) {
    console.log(values);
});

运行结果:

《将

Promise.race()

race()方法的参数与all()的参数一致,不过不同的是,all()方法需要全部执行直到全部执行成功或者任意一个任务执行失败。而race()只需要等到某一个任务执行完毕(不管是成功还是失败);🌰:

let p1 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 100, 'promise1');
});

let p2 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 200, 'promise1');
});

Promise.race([p1, p2]).then(function(value) {
    console.log(value);
});

执行结果:

《将

Promise走进科学

下面我们来打开脑洞,通过几个情景来猜测结果

问题:Promise链式调用时候(尾部包含catch()),如果中间的then()出错了会影响会面then的运行吗?

new Promise(function(resolve, reject) {
    resolve(1);
}).then(function(result) {
    console.log(result);
    return result + 1;
}).then(function(result) {
    console.log(result);
    return result + 1;
}).then(function(result) {
    throw new Error('中间报错啦!')
    console.log(result);
    return result + 1;
}).then(function(result) {
    console.log(result);
    return result + 1;
}).catch(function(err) {
    console.log(err);
})

运行结果:

《将

结论:使用Promise链式调用,一旦某个任务出错将会直接停止向下调用并抛出错误

如何把catch()放在第一个then()前面,后面的then出错的话,错误会被catch吗?

new Promise(function(resolve, reject) {
    resolve(1);
}).catch(function(err) {
    console.log(err);
}).then(function(result) {
    console.log(result);
    return result + 1;
}).then(function(result) {
    console.log(result);
    return result + 1;
}).then(function(result) {
    throw new Error('中间报错啦!')
    console.log(result);
    return result + 1;
}).then(function(result) {
    console.log(result);
    return result + 1;
})

运行结果:

《将

结论:catch()需要放在Promise链最后,不然无法catch到错误,只会被执行环境catch

最后笔者用Promise写了一个例子,功能是获取新闻数据并且存储到文本中,运行方法是命令行进入文件夹运行node index.js 就可以了前提是已经安装了Node传送门

好了,大概就这么多了。
在此做一下感慨:突然觉得写博客是多么享受的一件事,不管是自己的研究成果还是学习成果通过文字写出来,本身就是一件了不起的事情,不仅可以对自己懂得的技术的进一步升华,而且如果还能帮助到别人那更是功德无量的事呀;

    原文作者:风吹过的夏夜
    原文地址: https://segmentfault.com/a/1190000015952039
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞