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

这是一段旁白

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

回调(CallBack)

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

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

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

main(callBack);

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

clipboard.png

这样的写法看起来貌似还行,写法简单明了,一看就懂。可是这里笔者要吐槽下去年本身的智商,且听慢慢道来:
去年在重构项目的时候,有一个页面须要展现 4 个下拉框并且下拉框的数据须要从后台拉取。因此笔者在ComponentWillMount(React项目)方法中执行了拉取数据的动做并且是分开独立拉取,相似于:编程

......

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

......

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

/* 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的由来

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?函数

clipboard.png

从图中能够看出,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);
})

执行结果:

clipboard.png

首先模拟 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);
})

运行结果:

clipboard.png

检查 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)

运行结果:

clipboard.png

运行状况如预想同样。

说到这里会不会有这样一个问题:为何这几个方法都是返回一个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);
})

运行结果:
clipboard.png

注: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);
});

运行结果:

clipboard.png

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);
});

执行结果:

clipboard.png

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);
})

运行结果:

clipboard.png

结论:使用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;
})

运行结果:

clipboard.png

结论:catch()须要放在Promise链最后,否则没法catch到错误,只会被执行环境catch

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

好了,大概就这么多了。在此作一下感慨:忽然以为写博客是多么享受的一件事,不论是本身的研究成果仍是学习成果经过文字写出来,自己就是一件了不得的事情,不只能够对本身懂得的技术的进一步升华,并且若是还能帮助到别人那更是功德无量的事呀;

相关文章
相关标签/搜索