一文看懂JS的Promise对象

你越是认真生活,你的生活就会越美好——弗兰克·劳埃德·莱特
《人生果实》经典语录

异步执行

JavaScript的世界中,所有代码都是单线程执行的。

由于这个“缺陷”,导致JavaScript 的所有网络操作浏览器事件,都必须是异步执行

异步执行可以用回调函数实现:
复制下面的代码在console那里执行,观察代码块的底部

// 将结果打印在代码块下方
function log (text) {
	var prettyprint = document.querySelectorAll('.prettyprint ')[0]
    var p= document.createElement('p');
    p.style.color="orange"
    prettyprint.appendChild(p)
    p.innerHTML =  text;
}

function callback() {
    log('Done');
}
log('before setTimeout()');
setTimeout(callback, 1000); // 1秒钟后调用callback函数
log('after setTimeout()');

可见,异步操作在将来的某个时间点触发一个函数调用。

AJAX就是典型的异步操作。以下面的代码为例:
复制下面的代码在console那里执行,观察代码块的底部

function success(text) {
	var prettyprint = document.querySelectorAll('.prettyprint ')[1]
    var p= document.createElement('p');
    p.style.color="orange"
    prettyprint.appendChild(p)
    p.innerHTML = '请求成功' + text;
}
function fail(code) {
    var prettyprint = document.querySelector('.prettyprint ')
    var p= document.createElement('p');
    p.style.color="red"
    prettyprint.appendChild(p)
    p.innerHTML = '请求失败' + code;
}
var request = new XMLHttpRequest(); // 新建XMLHttpRequest对象
request.onreadystatechange = function () { // 状态发生变化时,函数被回调
    if (request.readyState === 4) { // 成功完成
        // 判断响应结果:
        if (request.status === 200) {
            // 成功,通过responseText拿到响应的文本:
            return success(request.responseText);
        } else {
            // 失败,根据响应码判断失败原因:
            return fail(request.status);
        }
    } else {
        // HTTP请求还在继续...
    }
}
// 发送请求:
request.open('GET', '/api/ArticleHighWords/list');
request.send();

回调函数 success()fail()写到一个 AJAX 操作里很正常,但是不好看,而且不利于代码复用

有没有更好的写法?比如写成这样:

var ajax = ajaxGet('http://...');
ajax.ifSuccess(success)
    .ifFail(fail);

这种链式写法的好处在于,先统一执行AJAX逻辑,不关心如何处理结果,然后根据结果是成功还是失败,在将来的某个时候调用 success 函数或 fail 函数。

Promise对象

古人云:“君子一诺千金”,这种“承诺将来会执行”的对象在 JavaScript 中称为 Promise 对象

Promise 有各种开源实现,在ES6中被统一规范,由浏览器直接支持。

Promise例子

我们先看一个最简单的 Promise 例子:生成一个 0-2 之间的随机数,如果小于1,则等待一段时间后返回成功,否则返回失败:

// 将结果打印在代码块下方
function log (text) {
	var prettyprint = document.querySelectorAll('.prettyprint ')[3]
    var p= document.createElement('p');
    p.style.color="orange"
    prettyprint.appendChild(p)
    p.innerHTML =  text;
}
function test(resolve, reject) {
    var timeOut = Math.random() * 2;
    log('set timeout to: ' + timeOut + ' seconds.');
    
    setTimeout(function () {
        if (timeOut < 1) {
            log('call resolve()...');
            resolve('200 OK');
        }
        else {
            log('call reject()...');
            reject('timeout in ' + timeOut + ' seconds.');
        }
    }, timeOut * 1000);
}

这个test()函数有两个参数,这两个参数都是函数,如果执行成功,我们将调用 resolve('200 OK'),如果执行失败,我们将调用 reject('timeout in ' + timeOut + ' seconds.')

可以看出,test()函数只关心自身的逻辑,并不关心具体的 resolve 和 reject 将如何处理结果

有了执行函数,我们就可以用一个 Promise对象来执行它,并在将来某个时刻获得成功或失败的结果:

复制上面和下面的代码在 console 那里执行,观察上面代码块的底部

var p1 = new Promise(test);
var p2 = p1.then(function (result) {
	log('成功:' + result);
})
var p3 = p2.catch(function (reason) {
	log('失败:' + reason);
})

变量p1是一个Promise对象,它负责执行 test 函数。由于test 函数在内部异步执行

当test函数执行成功时,我们告诉 Promise 对象:

// 如果成功,执行这个函数:
p1.then(function (result) {
    log('成功:' + result);
});

当test函数执行失败时,我们告诉 Promise 对象:

p2.catch(function (reason) {
    log('失败:' + reason);
});

Promise 对象可以串联起来,所以上述代码可以简化为:

new Promise(test).then(function (result) {
    log('成功:' + result);
}).catch(function (reason) {
    log('失败:' + reason);
});

实际测试一下,看看 Promise 是如何异步执行的:
复制下面的代码在console那里执行,观察代码块的底部

// 将结果打印在代码块下方
function log (text) {
	var prettyprint = document.querySelectorAll('.prettyprint ')[8]
    var p= document.createElement('p');
    p.style.color="orange"
    prettyprint.appendChild(p)
    p.innerHTML =  text;
}

new Promise(function (resolve, reject) {
    log('start new Promise...');
    var timeOut = Math.random() * 2;
    log('set timeout to: ' + timeOut + ' seconds.');
    setTimeout(function () {
        if (timeOut < 1) {
            log('call resolve()...');
            resolve('200 OK');
        }
        else {
            log('call reject()...');
            reject('timeout in ' + timeOut + ' seconds.');
        }
    }, timeOut * 1000);
}).then(function (r) {
   log('Done: ' + r);
}).catch(function (reason) {
    log('Failed: ' + reason);
});

可见Promise 最大的好处是在异步执行的流程中把执行代码处理结果的代码清晰地分离了:
在这里插入图片描述
Promise 还可以做更多的事情,比如,有若干个异步任务,需要先做任务1如果成功后再做任务2任何任务失败则不再继续并执行错误处理函数。

要串行执行这样的异步任务,不用 Promise 需要写一层一层的嵌套代码。有了 Promise,我们只需要简单地写:

job1.then(job2).then(job3).catch(handleError);

其中,job1、job2 和 job3 都是 Promise 对象。
复制下面的代码在console那里执行,观察代码块的底部

// 打印到代码块下方
function log (text) {
	var prettyprint = document.querySelectorAll('.prettyprint ')[10]
    var p= document.createElement('p');
    p.style.color="orange"
    prettyprint.appendChild(p)
    p.innerHTML =  text;
}
// 0.5秒后返回input*input的计算结果:
function multiply(input) {
    return new Promise(function (resolve, reject) {
        log('calculating ' + input + ' x ' + input + '...');
        setTimeout(resolve, 500, input * input);
    });
}

// 0.5秒后返回input+input的计算结果:
function add(input) {
    return new Promise(function (resolve, reject) {
        log('calculating ' + input + ' + ' + input + '...');
        setTimeout(resolve, 500, input + input);
    });
}

var p = new Promise(function (resolve, reject) {
    log('start new Promise...');
    resolve(123);
});

p.then(multiply)
 .then(add)
 .then(multiply)
 .then(add)
 .then(function (result) {
    log('Got value: ' + result);
});

AJAX异步执行函数转换为Promise对象

setTimeout可以看成一个模拟网络等异步执行的函数。现在,我们把 AJAX 异步执行函数转换为 Promise 对象,看看用 Promise 如何简化异步处理:

// 打印到代码块下方
function log (text) {
	var prettyprint = document.querySelectorAll('.prettyprint ')[11]
    var p= document.createElement('p');
    p.style.color="orange"
    prettyprint.appendChild(p)
    p.innerHTML =  text;

}
// ajax函数将返回Promise对象:
function ajax(method, url, data) {
    var request = new XMLHttpRequest();
    return new Promise(function (resolve, reject) {
        request.onreadystatechange = function () {
            if (request.readyState === 4) {
                if (request.status === 200) {
                    resolve(request.responseText);
                } else {
                    reject(request.status);
                }
            }
        };
        request.open(method, url);
        request.send(data);
    });
}

var p = ajax('GET', '/api/ArticleHighWords/list');
p.then(function (text) { // 如果AJAX成功,获得响应内容
    log(text) ;
}).catch(function (status) { // 如果AJAX失败,获得响应代码
    log(`ERROR:${status}`)
});

Promise.all()

除了串行执行若干异步任务外,Promise还可以并行执行异步任务

试想一个页面聊天系统,我们需要从两个不同的 URL 分别获得用户的个人信息和好友列表,这两个任务是可以并行执行的,用Promise.all()实现如下:
复制下面的代码在 console 那里执行,观察代码块的底部

// 将结果打印在代码块下方
function log (text) {
	var prettyprint = document.querySelectorAll('.prettyprint ')[12]
    var p= document.createElement('p');
    p.style.color="orange"
    prettyprint.appendChild(p)
    p.innerHTML =  text;
}

var p1 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 600, 'P2');
});
// 同时执行p1和p2,并在它们都完成后执行then:
Promise.all([p1, p2]).then(function (results) {
    log(results); // 获得一个Array: ['P1', 'P2']
});

Promise.race()

有些时候,多个异步任务是为了容错。比如,同时向两个URL读取用户的个人信息只需要获得先返回的结果即可。这种情况下,用 Promise.race()实现:
复制下面的代码在 console 那里执行,观察代码块的底部

// 将结果打印在代码块下方
function log (text) {
	var prettyprint = document.querySelectorAll('.prettyprint ')[13]
    var p= document.createElement('p');
    p.style.color="orange"
    prettyprint.appendChild(p)
    p.innerHTML =  text;
}
// Promise.race()
var p1 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 600, 'P2');
});
Promise.race([p1, p2]).then(function (result) {
    log(result); // 'P1'
});

假如p1执行较快,Promise 的 then() 将获得结果 ‘P1’ 。p2 仍在继续执行,但执行结果将被丢弃。

如果我们组合使用 Promise ,就可以把很多异步任务并行串行的方式组合起来执行

实现一个符合 Promises/A+ 规范的 Promise

实现一个符合 Promises/A+ 规范的 Promise

参考

Promise-廖雪峰的官方网站
Promise 对象

推荐阅读

连点成线


谢谢你阅读到了最后~
期待你关注、收藏、评论、点赞~
让我们一起 变得更强

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值