Promise详解-1:初识Promise

news/2024/12/12 1:55:44/

最近在回顾ES6的知识,想整理下跟Promise相关的内容。我准备整一个Promise解读的系列,看看能深入到什么程度吧。由浅入深,先认识下Promise

痛苦的回忆:回调地狱

假如现在让你维护一个“古老”的项目,缺少脚手架的加持,只能使用es5的语法来开发,我相信你一定会深刻的体会到es6添加Promise是多么伟大的行为。在es5的时代,异步编程需要依赖回调函数来实现,这就会导致一个问题:回调地狱(callback hell),代码结构会变成复杂,可读性、可维护性差到极致。例如:如果有一个业务需要连续调用三个接口,那么就需要这么写:

ajax('api1', function(res1){if(res1.success) {ajax('api2', function(res2) {if (res2.success) {ajax('api3', function(res3){if(res3.success) {// dosomething....}});}})}
})

如果场景更复杂的话,可能就会变成这样:

除了接口调用,事件、异步加载等等需要等待的操作基本都需要通过回调来实现。整个项目中充斥着这样的代码基本就只能靠记忆去维护。

Promise是什么?

es6的规范中新增Promise对象,是异步编程的一个解决方案。Promise相当于提供一个容器,这个容器会封装一个单独的“时间线”,这个独立的“时间线”与主线程的执行是并行,当容器内发生变化的时候,容器会更新其状态,外部可以通过API获取到状态变更的结果,并且这个状态会被固定在容器中,保持不变,无论什么时候都可以获取。

Promise对象提供了更清晰的异步操作处理,可以将异步操作封装到Promise内,通过统一的方法来处理异步操作。当包装在Promise中的异步操作有结果时,Promise对象会根据异步操作的结果(正确或是错误)更新状态,并返回结果,我们可以通过Promisethen或者catch方法来获取异步操作的结果。针对上面的多个异步串联的操作,Promise对象提供链式调用的方式,通过then方法将多个异步操作串联起来,形成一个操作序列。这种方式可以避免回调地狱,将多层嵌套变成使得代码更加清晰和易于理解。针对其他多个异步操作的场景,Promise对象还提供了一些静态方法,如Promise.all和Promise.race,用于处理多个异步操作的结果。这些功能使得Promise对象成为处理异步操作的强大工具,能够大大简化异步操作的处理流程,提高代码的可维护性和可读性。

Promise的状态

Promise通过状态来定义包裹的异步操作的目前所处的阶段,Promise提供了三个互斥的状态:fulfilled(已成功) 、rejected(已失败)、pending(进行中),任何Promise对象都处于这三种状态之一。

当我们创建一个Promise对象时,这个Promise对象处于pending状态,再异步操作产生结果后,会有以下两种状态的变化:

  • 异步操作成功:pending转换到fulfilled
  • 异步操作失败:pending转换到rejected

这个状态一旦变更,就不会再改变,而且无论是什么时候都可以获取到对应的结果。就是说在Promise对象的状态发生变化后,在任何时候都可以为Promise添加回调函数都可以会立即得到这个结果。这与事件是完全不同,在事件的模式下,如果你错过了它,就无法在获取结果。

Promise的基础用法

按照ES6的规范,Promise对象内置的Promise构造函数来实例化:

let promise = new Promise(function(resolve, reject) {// do sync resolve(); // 将promise的状态置为fulfilled(已成功)reject(); // 将promise的状态置为rejected(已失败)
});

这时,我们就得到一个`Promise`对象的实例:`promise`,然后通过这个实例,可以在任何时候取得异步操作的结果:

promise.then(() => {// do something
}).catch((error) => {console.error(error);
});

通过thencatch两个方法分别捕获Promise对象中包裹的异步操作的结果和错误。

这里需要注意的是,异步操作结束后会通过resolvereject来修改实例的状态。那么这里就不一定是真正反应异步操作的实际结果,而是异步操作想要外界知道的结果。这样说可能有点绕,举个例子,在请求一个接口时,请求是一个异步动作,按照ajax工具思路,应该是根据状态码来修改状态。但是在实际操作中,接口返回200,但是内容是操作失败,虽然这里的接口是请求成功(就http请求而言),但是在业务上是失败的。一般这里也会直接处理成失败的状态而不是成功状态(这就是异步操作真正想要给外界的结果)。

先整个例子

上面是Promise的基础用法,为了更好的说明,这里我先整个例子:现在假设在某个业务系统中,所有接口的请求的返回格式都是:

{"errCode": 0,			// 0 成功 1 失败"data": [],				// 数据"message": ''     // 信息
}

我们通过Promise + XmlHttpRequest简单封装一个ajax方法:

function createUrlParams(data) {return Object.keys(data).map((v) => {return `${v}=${data[v]}`;}).join('&');
}function ajax(url = '',  data = {}, type = 'GET') {return new Promise((resolve, reject) => {const xhr = new XMLHttpRequest();type = type.toUpperCase();if (type === 'GET') {url += '?' + createUrlParams(data);data = null;}xhr.open(type, url, true);xhr.onload = function() {if (xhr.status >= 200 && xhr.status < 300) {let res = JSON.parse(xhr.response);if (res.errCode !== 0) {reject({status:'error', result: res});} else {resolve({status:'success', result: res})}} else {reject(`请求失败:${xhr.status}`);}}xhr.onerror = function () {reject(`请求失败:${xhr.status}`);};xhr.send(data);});
}

上面的ajax方法最终会返回一个Promise实例,在请求完成的时候根据请求的状态和结果修改Promise的状态。例如获取用户的列表可以这么写:

ajax('http://xxxx.com/user/get-list')
.then(res => {console.log(res);
}).catch(err => {console.error(err);
});

基础方法说明

then方法

上面讲到我们可以通过then方法来获取异步的成功的结果,但其实then方法是接收两个回调方法,如果Promise状态为fulfilled则执行第一个回调方法,失败则执行第二个:

ajax('http://xxxx.com/user/get-list').then(() => {console.log('success');},() => {console.log('error');});

不管成功还是失败,then都会返回一个新的promise:

我们先修改下上面的代码,以便更好的观察:

const pAjax = ajax('http://xxxx.com/user/get-list');
const p1 = pAjax.then(() => {console.log('success');},() => {console.log('error');}
);

如果没有特殊操作(这个等会讲),不管成功失败,这个新的promise的状态会被置为fulfilled:

const p2 = p1.then(() => {console.log('success 2');
});

从上面的例子可以看出,在then的回调函数中我们并没有返回任何东西,但是then依然会返回一个promise。

例如这样:

const p2 = p1.then().then(() => {console.log('success');});

这是then的机制,如果回调函数中没有return的话,则会默认返回一个成功的promise,如果回调函数中有return,则会判断return是否为一个promise,如果不是promise,则会将其包装成promise返回,并且状态为成功。如果是promise,则直接返回,链式调用后面的方法会根据这个promise的状态触发对应的回调函数。

返回非promise

const p1 = pAjax.then(() => {return {result: 'success'};},() => {return {result: 'error'};}
);const p2 = p1.then((res) => {console.log(res);
});

返回promise

可以看到,如果then中返回的是promise,链式后面的函数会等待这个promise的状态变更,再根据promise的状态来执行对应的回调。

状态传递

从上面的例子也可以看到,then主要是受到上一个promise的影响,在pAjax错误的时候,p1会触发错误回调,而p2触发的时候成功的回调。那么如果p1没有捕获错误的话,错误会在p2被捕获到吗?

const p1 = pAjax.then(() => {console.log('success');}
);const p2 = p1.then(() => {console.log('success');}, () => {console.log('error');}
)

可以看到,p2捕获到错误,说明错误是会向下传递的。

总结

then函数支持传入两个回调方法,分别为成功的回调和失败的回调,then会根据回调函数的返回值返回一个新的promise,如果返回值不是promise,封装成promise返回,如果是promise则直接返回。如果then没有捕获错误,则会将错误向下传递,这是新的promise的状态为失败。

catch方法

catch方法其实是then(null/undefined, reject())的变相写法,在指定发生错误的时候执行回调函数。

const pAjax = ajax('http://xxxx.com/user/get-list');
const p1 = pAjax.then(() => {console.log('success');}).catch((err) => {console.log(err);});

因为catchthen捕获错误的变相写法,所以then具备的特性catch都具备,这里就不多赘述了。

值得一提的是,跟传统的try/catch不一样,在promise中产生的错误,只能在promise的链路上捕获到,不会扩散到外部:

function err() {throw new Error('error');
}try {err(); // 抛出错误console.log('next'); // 不执行,被错误阻塞
} catch(err) {console.log(err); // error对象
}
try {promise = ajax('http://xxxx.com/user/get-list'); // 抛出错误setTimeout(() => {console.log('next');}, 5000); // 5s后仍执行
} catch(err) {console.log(err); // 无法捕获到错误
}

另外,如果在promise中抛出错误,但是没有执行reject方法,promise还是会将状态处理成rejected

function err(){return new Promise((resolve, reject) => {throw new Error('promise error');});
}err().catch(err => {console.log(err);   // Error: promise error
});

但是,如果已经修改了promise的状态,再抛出错误,则是无效的:

function err(){return new Promise((resolve, reject) => {resolve('success');throw new Error('promise error'); // 无效});
}err().then((res) => {console.log(res); // success
}).catch(err => {console.log(err);   // 无
});

需要注意的,如果已经使用catch捕获了错误,那么错误就不会在向下传递,除非前面的catch将错误重新抛出。

ajax('http://xxxx.com/user/get-list').catch(err => {console.log(err); // error信息}).catch(err => {console.log(err); // undefined});
 ajax('http://xxxx.com/user/get-list').catch(err => {throw err; // 也可以写成return Promise.reject(err); }).catch(err => {console.log(err); // error信息});
编写建议

前面讲到,promise的链路上错误是会向下传递的,所以呢,我建议promise的写法是按照需要在指定位置使用 catch捕获错误,不要再then中使用rejected的回调去捕获错误,这样整体的可读性更高:

ajax('http://xxxx.com/user/get-list').then(() => {// 步骤1}).then(() => {// 步骤2}) // ....catch((err) => {console.log(err);});

这样看起来更像同步的写法,更清晰可读性更高。

如果是多个promise嵌套调用的话,建议只在最外层捕获错误,这样可以在统一的地方处理错误,埋日志,错误调试等操作都会简化。当然如果不希望中间某些操作的错误终端整个promise,可以在这些操作上添加catch拦截掉错误。

这里假设是一个添加用户的场景,有几个步骤:

  1. 获取录入的用户名(正常是同步的,这里假设是一个promise)
  2. 调用接口校验用户名是否重复
  3. 提交用户名
  4. 刷新用户列表
function getForm().then(() => {}){return form();
}function ApiCheckName(name) {return ajax('http://xxxx.com/user/check-name', {name}).then(isExists => {if (isExists) {throw new Error('用户名已存在');} else {return true;}})
}function ApiAddUser(data) {return ApiCheckName(data.name).then(() => {return ajax('http://xxxx.com/user/add', data, 'POST');});
}function ApiGetUserList() {return ajax('http://xxxx.com/user/get-list');
}// 封装添加用户的方法
function addUser() {return getForm().then((form) => {return ApiAddUser({name: 'skkk'});}).then(() => {return getUserList();});
}// 外部调用
addUser()
.catch((err) => {console.log('失败', err);
})

finally方法

finally方法是在ES2018规范引入的,所以在一些版本较低的浏览器是不支持的。finally方法是不管promise最终是成功还是失败,都会执行该方法。

ajax('http://xxxx.com/user/get-list').then(() => {console.log('success');}).catch(() => {console.log('error');}).finally(() => {console.log('finally');})

如上面的代码,无论接口请求是否成功,都会执行finally

finally本质上还是then的变种,其实现的效果等于与在then的成功失败回调中分别添加相同的代码:

ajax('http://xxxx.com/user/get-list').finally(() => {dosomething();
});

等同于:

ajax('http://xxxx.com/user/get-list').then(() => {dosomething();}, () => {dosomething();}
)

明显finally更简洁。

then不同的是finally是不接收参数的。

ajax('http://xxxx.com/user/get-list').then(() => { return 1;}).catch(() => { return 2; }).finally((res) => {console.log(res);})  // undefined

这样我们无法通过传参来判断promise最终的状态,从而进行一些针对的操作。但是还是可以用不过来做一些终结操作。例如:实现一个带进度条的下载功能,不管下载成功或失败都要关闭重置进度条,就可以在finally中实现。

同样的,finally也是返回一个promise

const pAjax = ajax('http://xxxx.com/user/get-list');
const p1 = pAjax.then(() => { return 1;}).catch(() => { return 2; });
const p2 = p1.finally(res => {console.log(res);});
const p3 = p2.then(res => {console.log(res);});

可以看到返回值会越过finally传递。

同样,状态也是会向下传递的

const pAjax = ajax('http://xxxx.com/user/get-list');
console.log('pAjax ====>', pAjax);
const p1 = pAjax.finally(res => {console.log(res);});
console.log('p1 ====>', p1);
const p2 = p1.then(() => {console.log('success');},() => {console.log('error');}
);
console.log('p2 ====>', p2);

除了不接收参数,finally里返回的参数或成功的promise都也不会被传递下去:

const pAjax = ajax('http://localhost:3010');
const p1 = pAjax.finally(() => {return 'finally';
});
const p2 = p1.then((res) => {console.log('p2 res', res);},(res) => {console.log('p2 res', res);}
);
const p3 = pAjax.finally(() => {return Promise.resolve('finally');
});
const p4 = p3.then((res) => {console.log('p4 res', res);},(res) => {console.log('p4 res', res);}
);

p2p4都获取到pAjax的结果。

但是如果finally返回了Promise或抛出错误就会向下传递:

const p1 = pAjax.finally(() => {throw new Error('finally error');  // 抛出错误
});
const p2 = p1.then((res) => {console.log('p2 res',res);},(res) => {console.log('p2 res',res);}
);
const p3 = pAjax.finally(() => {throw Promise.reject('finally');  // 返回成功的promise
});
const p4 = p3.then((res) => {console.log('p4 res',res);},(res) => {console.log('p4 res',res);}
);

可以看到,无论pAjax是否成功的,但是p2p4都是捕获到p1p3抛出的错误。

当然,实际的开发中千万别这么写,finally就做一些统一的终结操作就行了。如果统一操作也是promise的话,建议单独处理。

Promise.resolve() & Promise.reject()

如果想要返回一个promise,但又不想使用new Promise的方式来实现,就可以使用这两个方法。基本上其功能与字面意义一致,Promise.resolve()会直接返回一个状态为fulfilled的promise,Promise.reject()则返回一个状态为rejected的promise。

Promise.resolve

上面说的是基本功能,不带任何参数会返回一个成功的promise。但是resolve方法可以接受一个参数,其会根据这个参数的不同返回不同的promise。

  1. 传入promise实例

传入promise实例,会直接返回这个实例,不做任何修改。

const p = Promise.resolve(ajax('http://xxxx.com/user/get-list'));
p.then(res => {console.log(res);  // {status: 'success', result: {…}}
}); 
  1. 传入thenable对象

thenable对象就是带then方法的对象,像这样的:

{then: function(resolve, reject){setTimeout(() => {resolve('then');}, 500);}
}

将这样的对象传给Promise.resolve(),它会将其转换为promise对象,并立刻执行then函数。像这样:

const p = Promise.resolve({then(resolve, reject) {setTimeout(() => {resolve('thenable');}, 500);}
});
p.then(res => {console.log(res);   // thenable
});

then没有resolve或reject,只是一个方法甚至不是一个方法会是什么情况呢?

const p1 = Promise.resolve({then() {console.log('then function');return 'then';}
});
p1.then(res => {console.log('p1', res); 
});const p2 = Promise.resolve({then: {a: 1}
});
p2.then(res => {console.log('p2', res); 
});const p3 = Promise.resolve({then: '1'
});
p3.then(res => {console.log('p3', res); 
});

可以看到,如果then方法没有修改promise的状态的话,then方法会被执行,但是返回的promise会处于pending的状态。而then是非方法的话,会返回一个fulfilled的promise,对象被当做返回值返回。

  1. 其他类型的参数

传入对象、数组、字符串、数字等非方法类的参数,Promise.resolve会立即返回一个fulfilled的promise,并且将参数作为返回值。

Promise.resolve([1,2,3]).then(res => {console.log(res); // [1,2,3]})

传入的参数是一个非promise的方法,会获取该方法的返回值(如果有)做为返回值,同样也是返回一个状态为

fulfilled的promise。

function p1() {console.log('function p1'); // function p1return 'p1';
}Promise.resolve(p1()).then(res => {console.log(res); // p1})function p2() {console.log('function p2'); // function p2
}Promise.resolve(p2()).then(res => {console.log(res); // undefined})

如果在方法中抛出错误呢?会不会走到catch呢?

function p1() {throw new Error('p1 error');
}Promise.resolve(p1()).then(res => {console.log(res);}).catch(err => {console.log(err);});

答案是啥都没执行,因为p1在执行是就抛出错误了,后续代码终止执行。

Promise.reject

reject无论传不传入都是返回一个状态为rejected的promise。传入的任何参数都会做为失败的理由返回。

// 传入对象
Promise.reject({name: 'reject'}).catch(err => {console.log('object', err); // object {name: 'reject'}});
// 传入字符串
Promise.reject('reject').catch(err => {console.log('string', err); // string reject});function p1() {console.log('function p1');  // function p1return 'p1';
}// 有返回值的方法
Promise.reject(p1()).catch(err => {console.log('p1', err); // p1 p1});function p2() {console.log('function p2'); // function p2
}// 无返回值的方法
Promise.reject(p2()).catch(err => {console.log('p2', err); // p2 undefined});// promise
Promise.reject(ajax('http://xxxx.com/user/get-list')).catch(err => {console.log('ajax', err);  // ajax promise});

这里跟resolve不一样的是,传入的参数是promise,也会将promise实例理由返回。而不是返回promise实例的结果。

总结

本文介绍了Promise最基础的知识,了解了Promise的基础应用。接下会继续深入了解Promise,敬请期待吧。


http://www.ppmy.cn/news/1554369.html

相关文章

Spring Boot 3.4.0 发布:功能概览与示例

Spring Boot 3.4.0 带来了许多增强功能&#xff0c;使现代应用开发更加高效、便捷和强大。以下是最新功能的完整概述&#xff0c;以及一些帮助您快速入门的代码示例。 1. 应用程序版本管理 Spring Boot 引入了 spring.application.version 属性&#xff0c;方便开发者设置和访…

Android CoordinatorLayout:打造高效交互界面的利器

目录 一、CoordinatorLayout 介绍及特点 二、使用方法 2.1 创建 CoordinatorLayout 布局 2.2 添加需要协调的子视图 2.3 自定义 Behavior 三、结语 相关推荐 在Android开发中&#xff0c;面对复杂多变的用户界面需求&#xff0c;CoordinatorLayout以其强大的交互管理能力…

Android13应用在后台录音无声音

最近在做项目&#xff0c;对讲应用放在后台&#xff0c;录音无声音&#xff0c;最后解决。 一 现象 对讲应用运行在后台&#xff0c;录音无效查看日志&#xff0c;AudioRecorder录音回调全是0&#xff1b;状态栏无通知&#xff0c;无申请通知权限。 二解决 看了现象应该能够…

Open AI 推出 ChatGPT Pro

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

ensp实验-vrrp多网关配置

一、交换机与路由的配置区别 1. 角色定义交换机&#xff1a; Master 或 Backup: 交换机通常作为 Master 或 Backup 设备参与 VRRP&#xff0c;负责在主设备故障时接替其工作。路由器&#xff1a; Master 或 Backup: 路由器同样可以作为 Master 或 Backup 设备…

Paddle Inference部署推理(二十四)

二十四&#xff1a;Paddle Inference推理 &#xff08;C&#xff09;API详解 9. 启用内存优化 API定义如下&#xff1a; // 开启内存/显存复用&#xff0c;具体降低内存效果取决于模型结构 // 参数&#xff1a;None // 返回&#xff1a;None void EnableMemoryOptim();// 判…

现代C++16 pair

文章目录 1. **概述**2. **成员类型和成员对象**3. **构造函数**4. **成员函数**5. **非成员函数**5.1 **make_pair**5.2 **比较运算符**5.3 **std::swap**5.4 **std::get** 6. **辅助类**6.1 **std::tuple_size 和 std::tuple_element**6.2 **std::common_type 和 std::basic_…

书生浦语第四期L1G4000——InternLM + LlamaIndex RAG 实践

1.环境、模型准备 1.1 配置基础环境 安装python依赖包 pip install einops0.7.0 protobuf5.26.1 1.2 安装Llamaindex pip install llama-index0.11.20 pip install llama-index-llms-replicate0.3.0 pip install llama-index-llms-openai-like0.2.0 pip install llama-ind…