ES6 Promise原理总结
概要
学习一门技术,最好的方式就是先了解这门技术是如何诞生的,及它解决了什么问题?
接下来将从一下几个方面介绍
Promise
:异步编程的问题:代码逻辑不连续;
回调地狱:嵌套了太多的回调函数;
Promise
:消灭嵌套调用;Promise
:合并多个任务的错误处理;Promise
与微任务的关系;首先明确一下,
Promise
解决的是异步编码风格的问题,而不是一些其他的问题;
异步编程的问题:代码逻辑不连续
假设有一个请求,使用
XMLHttpRequest
来实现,代码如下:// 执行状态 function onResolve(response) { console.log(response); } function onReject(error) { console.log(error); } let xhr = new XMLHttpRequest(); xhr.ontimeout = function(e) { onReject(e); }; xhr.onerror = function(e) { onReject(e); }; xhr.onreadystatechange = function () { onResolve(xhr.response); }; // 设置请求类型,请求URL,是否同步信息 let URL = 'https://localhost:8080/getList'; xhr.open('Get', URL, true); // 设置参数 xhr.timeout = 3000; // 设置xhr请求的超时时间 xhr.responseType = "text"; // 设置响应返回的数据格式 xhr.setRequestHeader("X_TEST","time.geekbang"); // 发出请求 xhr.send(); 复制代码
可见上述这短短的一段代码里面竟然出现了五次回调,这么多的回调会导致代码的逻辑不连贯、不线性,非常不符合人的直觉,这就是异步回调影响到我们的编码方式;
回调地狱:嵌套了太多的回调函数
当然也可以把上述的异步代码封装成一个函数,调用的时候传入相应的请求参数、回调函数,就可以让处理流程变得线性,代码如下:
// request,请求信息,请求头,延时值,返回类型等 // resolve, 执行成功,回调该函数 // reject 执行失败,回调该函数 function XFetch(request, resolve, reject) { let xhr = new XMLHttpRequest(); xhr.ontimeout = function (e) { reject(e); }; xhr.onerror = function (e) { reject(e); }; xhr.onreadystatechange = function () { if (xhr.status = 200) resolve(xhr.response); }; xhr.open(request.method, request.url, request.sync); xhr.timeout = request.timeout; xhr.responseType = request.responseType; // 补充其他请求信息 // ... xhr.send(); } // 调用封装的异步代码 XFetch( { method: 'GET', url: 'https://localhost:8080/getList', sync: true }, function resolve(data) { console.log(data); }, function reject(e) { console.log(e); } ); 复制代码
封装异步代码在一些简单逻辑下可以满足需求,但如果逻辑复杂一点,会产生嵌套很多回调函数,从而陷入了回调地狱,代码如下:
XFetch( { method: 'GET', url: 'https://localhost:8080/getList', sync: true }, function resolve(response) { console.log(response); XFetch( { method: 'GET', url: 'https://localhost:8080/getList1', sync: true }, function resolve(response) { console.log(response); XFetch( { method: 'GET', url: 'https://localhost:8080/getList2', sync: true }, function resolve(response) { console.log(response); }, function reject(e) { console.log(e); }) }, function reject(e) { console.log(e); }) }, function reject(e) { console.log(e); } ); 复制代码
上述代码看上去很乱,归根结底有两点原因:
嵌套调用:下一个任务依赖上一个任务的请求结果,并在上一个任务的回调函数内部执行新的业务逻辑,当嵌套层次多了之后,代码的可读性就变得非常差;
任务的不确定性:执行每个任务都有两种可能的结果(成功或者失败),所以对每个任务都要进行一次额外的错误处理的方式,其明显增加了代码的混乱程度;
原因分析出来后,那么就需要解决这两个问题:
消灭嵌套调用;
合并多个任务的错误处理;
ES6引入
Promise
,就是为了解决上述这两个问题,下面具体介绍;
Promise
:消灭嵌套调用
Promise
主要通过下面两步解决嵌套回调问题:
Promise
实现了回调函数的延时绑定:// 创建Promise对象promise1,并在executor函数中执行业务逻辑 function executor(resolve, reject){ resolve(100); } let promise1 = new Promise(executor); // promise1延迟绑定回调函数onResolve function onResolve(value){ console.log(value); } promise1.then(onResolve); 复制代码
回调函数的延时绑定在代码上体现就是先创建
Promise
对象promise1
;通过
Promise
的构造函数executor
来执行业务逻辑;创建好
Promise
对象promise1
之后,再使用promise1.then()
来设置回调函数;如上所示:
总之,
Promise
实现回调函数的延时绑定,能把原来的回调写法分离出来,在异步操作执行完后,用链式调用的方式执行回调函数;Promise
将回调函数onResolve
的返回值穿透到最外层:// 创建Promise对象promise1,并在executor函数中执行业务逻辑 function executor(resolve, reject) { resolve(100); } const promise1 = new Promise(executor); // promise1延迟绑定回调函数onResolve function onResolve(value) { console.log(value); function executor2(resolve, reject) { resolve(value + 1); } return new Promise(executor2); } // promise2为内部返回值穿透到了最外层 const promise2 = promise1.then(onResolve); promise2.then((value) => { console.log(value); }); 复制代码
如上所示,把
onResolve
函数内部创建好的Promise
对象返回到最外层,这样就可以摆脱嵌套循环了;
Promise
:合并多个任务的错误处理
function executor(resolve, reject) { let rand = Math.random(); console.log(1); console.log(rand); if (rand > 0.5) resolve(); else reject(); } var p0 = new Promise(executor); var p1 = p0.then((value) => { console.log("succeed-1"); return new Promise(executor); }); var p3 = p1.then((value) => { console.log("succeed-2"); return new Promise(executor); }); var p4 = p3.then((value) => { console.log("succeed-3"); return new Promise(executor); }); p4.catch((error) => { console.log("error"); }) console.log(2); 复制代码
上述代码,链式调用了四个
Promise
对象:p0~p4
,无论哪个对象里面抛出异常,都可以通过最后一个对象p4.catch
来捕获异常;通过这种方式可以将所有
Promise
对象的错误合并到一个函数来处理,这样就解决了每个任务都需要单独处理异常的问题;之所以可以使用最后一个对象来捕获所有异常,是因为
Promise
对象的错误具有“冒泡”性质,会一直向后传递,直到被onReject
函数处理或catch
语句捕获为止;具备了这样“冒泡”的特性后,就不需要在每个
Promise
对象中单独捕获异常了;
Promise
与微任务的关系
function executor(resolve, reject) { resolve(100) } let demo = new Promise(executor) function onResolve(value){ console.log(value) } demo.then(onResolve) 复制代码
如上代码:
所以可以推测,
resolve
函数内部调用了通过demo.then
设置的onResolve
函数;首先执行
new Promise
时,Promise
的构造函数会被执行;接下来,
Promise
的构造函数会调用Promise
的参数executor
函数;然后在
executor
中执行了resolve
,执行
resolve
函数,会触发demo.then
设置的回调函数onResolve
;注:由于
Promise
采用了回调函数延迟绑定技术,所以在执行resolve
函数的时候,回调函数还没有绑定,那么只能推迟回调函数的执行;为了方便理解,这里实现了一个简单的Promise对象:
function Bromise(executor) { var onResolve_ = null; var onReject_ = null; //模拟实现resolve和then,暂不支持rejcet this.then = function (onResolve, onReject) { onResolve_ = onResolve; }; function resolve(value) { //setTimeout(()=>{ onResolve_(value); // },0) } executor(resolve, null); } 复制代码
调用上述定义的Bromise对象:
function executor(resolve, reject) { resolve(100); } // 调用Bromsie let demo = new Bromise(executor); function onResolve(value) { console.log(value); } demo.then(onResolve); 复制代码
这时会报错,因为在执行
executor
函数的时候,还没有通过demo.then()
设置回调函数,Bromise
中的onResolve_
还为空,所以就报错了;这时就需要改造
Bromise
中的resolve
方法,让resolve
延迟调用onResolve_
:要让
resolve
中的onResolve_
函数延后执行,可以在resolve
函数里面加上一个定时器,让其延时执行onResolve_
函数,代码如下:function Bromise(executor) { var onResolve_ = null; var onReject_ = null; // 模拟实现resolve和then,暂不支持rejcet this.then = function (onResolve, onReject) { onResolve_ = onResolve; }; function resolve(value) { // 使用setTimeout定时器来延时`onResolve_`函数的执行 setTimeout(() => { onResolve_(value); }, 0); } executor(resolve, null); } 复制代码
上面采用了定时器来推迟
onResolve
的执行,不过使用定时器的效率并不是太高;所以
Promise
又把这个定时器改造成了微任务了,这样既可以让onResolve_
延时被调用,又提升了代码的执行效率,这就是Promise
中使用微任务的原因;
作者:YuYu_Fish
链接:https://juejin.cn/post/7018845095239614477