学编程,来源栈;先学习,再交钱
当前系列: ES进阶 修改讲义

回调地狱

在JavaScript中,异步通常就伴随着回调(复习:异步和回调

为什么呢?看下面的代码:

let result = false;

function loadSuccess() {
    setTimeout(function () {
        result = true;
    }, 1000)
}
loadSuccess();

console.log(result ? 'oh yeah!' : 'what happen?');

@想一想@:结果是什么?演示:并在控制台看一下result的值

但我们希望的,或者说更符合合理预期的结果应该是:result的值在loadSuccess()中被改变,然后再影响console.log()。


@想一想@:能不能由某个异步方法返回bool值?

        var result = loadSuccess();
这是不行的,只能把上述代码改成这个样子:
function loadSuccess(callback) {
    setTimeout(function () {
        //如果这个result也依赖于另一个异步函数,咋整?
        var result = true;
        console.log(result ? 'oh yeah!' : 'what happen?');
    }, 1000)
}

但是,假设console.log()也要异步执行,而且其返回值/改变的值也要供另一个异步方法使用(事实上很容易出现这种情况,尤其是node.js中),代码会演变成什么样子?

function loadSuccess(callback) {
    setTimeout(function () {     //获得result值的异步方法
        let result = true;
        setTimeout(function () {
            let words = result ? 'oh yeah!' : 'what happen?';
            setTimeout(function () {
                console.log(words);
            },10)
        }, 10)
    }, 10)
}

这就是大名鼎鼎的回调地狱(callback hell)

之所以称其为“地狱”,是因为:

  1. 代码本身就不“好”看,眼睛都花了……(メ`ロ´)/
  2. 书写/维护代码时,其内容和“正常”的流程是反的。比如:我需要一个result的值,
    正常的做法是:调用能获得result值的函数即可;
    但这里是要:把我当前的代码放到能获得result值的函数里面去?
  3. 代码无法封装重用:loadSuccess()之后如果要再做点什么,就不得不更改loadSuccess()的函数实现……
所以,我们迫切的需要引入:


Promise

promise是ES6推出的新的内置类。使用它的大致方式是:

let p = new Promise(function (resolve) {
    resolve('resolve');
});
p.then(function (value) {
    console.log(value);
});
console.log('after then');

注意:Promise的构造函数的参数是一个函数,而且函数的参数resolve还是函数

@想一想@:这个参数是不是必须名为resolve?

断点演示:

  1. 代码执行顺序
  2. console.log(value) 中value的值由resolve()传递

我们得到的规律是:

  • then()本身是异步执行的
  • then()一定会在resolve()之后才能执行;
  • 没有执行resolve(),就不会执行then()

但这有什么作用呢?


更强的可读性

用Promise重写之前地狱回调代码:

let p = new Promise(function (resolve) {
    setTimeout(function () {
        var result = true;
        resolve(result);    //先获得result的值
    }, 1000);
});
p.then(function (result) {
    console.log(result ? 'oh yeah!' : 'what happen?');  //一定会在得到result的值后才运行
})

这样,代码的可读性是不是变得更强了?


封装

有了Promise,我们把可以把loadSuccess封装成:

function loadSuccess(resolve, reject) {
    setTimeout(function () {
        var result = true;
        resolve(result);    //先获得result的值
    }, 10);
}

loadSuccess()就可以不用改了,它通过resolve()向外部提供了result,外部要想获得其值,只需要拿到相应的Promise对象:

let p = new Promise(loadSuccess);
然后继续调用p.then()就OK了。


Reject

Promise()中的回调函数不是总能成功执行,当这种情况出现时,应该将其反馈给外界。于是:

  • Promise()的构造函数参数除了resolve,还可以有第二个reject,reject一样是一个函数,可以传一个参数,通常命名为reason,用于说明出错的原因
  • then()也可以接收第二个回调函数参数,在Promise()中的reject()被调用执行时执行


内部原理

每一个Promise实例中都保存着一个状态(status)。状态值有三种:

  • pending(进行中):默认初始状态
  • fulfiled(成功):调用resolve(value)后获得,即resolve()将Promise实例的状态由pending转换为fulfiled
  • rejected(失败):调用reject(error)后获得:即reject()将Promise实例的状态由pending转换为rejected

注意:这三种状态在Promise实例外部无法改变,只能在Promise的构造函数中的代码实现。

方法then()总是会在Promise实例化之后,检查其状态:

  • 如果是pending,不予任何执行
    演示:注释掉resolve('resolve');
  • 如果是fulfiled,执行其第一个回调函数
  • 如果是rejected,执行其第二个回调函数


catch和finally

除了then(),promise对象还有两个方法:

  • then():两个callback function参数,第一个是resolved之后的调用,第二个(可选)是reject之后的调用
  • catch():等同于.then(null, rejection)或.then(undefined, rejection):推荐catch写法。
    注意:不显示catch,error就会被“吞”掉:console中仍然会报错,但不阻断后续(如setTimeout())代码的执行
  • finally():有错没错,最后都要执行的。
        let p = new Promise(function (resolve, reject) {
            setTimeout(function () {
                var result = true;
                if (true) {
                    throw new Error('......');
                    resolve(result);
                } else {
                    reject('error');
                }
                console.log('after error');
            }, 1000);
        });
        p.then(function (result) {
            console.log(result ? 'oh yeah!' : 'what happen?');
        }).catch(function (reason) {   //推荐:可读性更高
            console.log(reason);
        }).finally(function () {
            console.log('finally');
        })


then()的连缀

因为then()还会返回一个新的Promise实例,所以then()后面还可以紧接着继续调用then()……(复习:JQuery的链式调用)

需要return

        new Promise((resolve, reject) => {
            setTimeout(function() {
                console.log('第 1 次resolve');
                resolve(false);
            }, 500)
        }).then(function(result) {
            return new Promise((resolve, reject) => {
                setTimeout(function() {
                    console.log('第 2 次resolve');
                    resolve(result);
                }, 500)
            });
        }).then(function(result) {
            if (result) {
                console.log('oh yeah!');
            } else {
                console.log('what happen?')
            }
        })
此外,把error装进reject中,就可以轻松catch:
                try {
                    throw Error('heng!');
                } catch (e) {
                    reject(e);
                }

其他参考:MDN

学习笔记
源栈学历
键盘敲烂,月薪过万作业不做,等于没学

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 ES进阶 中继续学习:

下一课: JavaScript 移到ES

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码