JavaScript – Promise
前言
我学 Promise 的时候, 那时还没有 es6. 曾经还自己实现过. 但时隔多年, 现在 es6 的 promise 已经很完善了.
这篇作为一个简单的复习. (毕竟我已经 1 年多没有写 JS 了...)
以前写过相关的文章:
angular2 学习笔记 ( Rxjs, Promise, Async/Await 的区别 )
参考
Promise 解决的问题
什么是异步和回调 callback
JS 是单线程, 无法并发处理事情, 但是它可以异步. 比如发 http request, request 通过网卡发出去后, CPU 就去做别的事情, 而不是傻傻等网卡回复.
当 response 回来以后, 网卡通知 CPU, 这时再把任务接回来. 这个动作就叫 callback. 就是你别等我, 我好了会通知你.
callback 的写法长这样
const callback = () => {
console.log('timeout');
};
setTimeout(callback, 2000);
setTimeout 是一个异步函数, 调用后, 游览器会用另一个线程去计算时间, 主线程继续处理其它代码. 时间到, 主线程会被通知, 然后运行后续 (callback) 的代码.
大概是这个概念. 其它的异步函数包括 Ajax, FileReader 等等 (通常涉及到磁盘 IO, 网路请求都会是异步的. 因为做这些事情的时候不需要 CPU).
回调地狱
callback 的写法一旦嵌套就会变成很丑, unreadable.
比如, 我想写一个
delay 3 秒,
运行 console 'a'
再 delay 2 秒
运行 console 'b'
再 delay 1 秒
运行 console 'c'
写出来长这样:
setTimeout(() => {
console.log('a');
setTimeout(() => {
console.log('b');
setTimeout(() => {
console.log('c');
}, 1000);
}, 2000);
}, 3000);
丑不丑? Promise 就是用来解决丑这个问题的. 它可以把嵌套的回调 "打平" flat
Promise 基本用法
Promise 的核心是封装了异步函数的调用, 和 callback 的写法, 记住这个点.
promise 使用是这样的
const promise = new Promise((resolve, _reject) => {
setTimeout(() => {
resolve('return value 123');
}, 3000);
});
promise.then((returnValue) => {
console.log('returnValue', returnValue); // return value 123
});
分 2 个阶段看待
初始化 Promise
Promise 是一个 class. 实例化它会等到 promise 对象.
实例化时, 需要传入一个函数. 函数里面封装了要执行的异步代码.
比如上面的 setTimeout. 或者是 Ajax, FileReader 等等都行.
resolve 是一个 callback 代理, 当异步完成以后. 我们调用 resolve 告知 Promise 异步完成了. 并且返回异步函数的返回值 (比如 Ajax 后的 response data)
注册 callback
注册 callback 是通过 promise 对象来实现的. 调用 .then 函数把 callback 传进去
callback 会在 resolve 的时候被执行, 并且获得异步函数的返回值.
注: 有没有返回值都是 ok 的.
意义何在?
像这样把异步函数和 callback wrap 起来, 意义何在呢? 如果只是 1 个 callback 那么没有什么太大的意义.
记得, Promise 要解决的是嵌套的 callback (回调地狱)
.then Combo
我们把上面的 setTimeout 用 Promise 封装一下
function delayAsync(delayTime: number): Promise<void> {
const promise = new Promise<void>((resolve, _reject) => {
setTimeout(() => {
resolve();
}, delayTime);
});
return promise;
} delayAsync(3000).then(() => {
console.log('a');
});
相等于
setTimeout(() => {
console.log('a');
}, 3000);
return promise + .then combo
如果要再嵌套一个 delay, 你可能会认为是这样写
delayAsync(3000).then(() => {
console.log('a');
delayAsync(2000).then(() => {
console.log('b');
});
});
虽然这个也可以跑, 但是正确的用法不是这样. 而是这样
delayAsync(3000)
.then(() => {
console.log('a');
return delayAsync(2000);
})
.then(() => {
console.log('b');
});
在第一个 then 里, 我们返回了另一个 promise 对象.
然后再第一个 then 之后 combo 了另一个 then.
这样的写法就成功的把嵌套的回调 "打平" 了.
Promise 内部实现原理
其实没有必要懂底层逻辑, 会用就可以了. 简单了解一下到时可以啦.
Promise 对象的 then 负责注册 callback. 同时它返回另一个 promise. 你可以把它理解为一个 child promise (连续几个 .then 就变成了一个 promise chain)
callback 除了可以返回普通的 value 也可以返回一个 promise 对象.
当返回 promise 对象, Promise 就会等待这个 promise 对象 resolve 才执行 callback.
Promise 内部就是维护着 promise chain 和所以 callback 的执行顺序. 这样就做到了 "打平" 的写法了.
感悟
Promise 在 es6 之前就有了, 在 JS 的语法基础上, 通过封装实现另一种调用方式, 让代码更好写, 更好读.
jQuery 也是有这种 feel. 还有 Fluent Builder 模式 也是这样. 都是很聪明的实现.
Promise 的执行顺序
console.log('1');
const promise = new Promise<void>((resolve) => {
console.log('2');
resolve();
console.log('3');
});
promise.then(() => {
console.log('5');
});
console.log('4');
new Promise 传入的函数会马上被执行 (里面通常会调用异步函数, 但并没有强制, 你也可以直接调用 resolve 返回的)
上面我刻意搞了一个同步执行的情况, resolve 虽然马上被执行了, 但是 callback 并没有马上被执行.
一直等到 console.log(4) 完了以后 callback 才被执行.
也就是说任何 promise 的 callback 都会被押后执行, 即使 resolve 没有被异步调用. 这个是唯一需要特别注意的.
after resolve 依然执行代码 ?
best practice 的话, resolve 之后就不应该执行代码了.
刻意习惯性的在 resolve 前加上 return, 确保后续没有执行代码. (不然挺乱的)
const promise = new Promise<void>((resolve) => {
console.log('2');
return resolve();
});
Reject and Catch
上面提到的例子都是 succeed 的情况. 其实 Promise 还有一个强项, 那就是解决异步函数和回调 catch error 的问题.
异步的 catching error 问题
同步代码很容易 catch error
try {
throw new Error('erorr');
} catch (e) {
console.log('error 123');
}
换成异步的话
try {
setTimeout(() => {
throw new Error('error');
}, 2000);
} catch (e) {
console.log('error 123'); // won't be call
}
这时就无法 catch 到 error 了.
Promise Reject & Catch
const promise = new Promise<void>((resolve, reject) => {
if (Math.random() >= 0.5) {
resolve();
} else {
reject();
}
}); promise
.then(() => {
console.log('ok');
})
.catch(() => {
console.log('error');
});
除了 resolve, 还有一个 reject 函数用来告知 Promise 异步函数搞糟了.
除了 .then, 还有一个 .catch 用来捕获 reject 的返回.
它的逻辑是 resolve 就去找 .then 的回调. reject 就去找 .catch 的回调.
此外, 除了 reject, throw 也是可以被 catch 到,
delayAsync(3000)
.then(() => {
throw new Error('error');
return delayAsync(2000);
})
.then(() => {
console.log('b');
})
.catch(() => {
console.log('catch');
});
在第一个 then 回调中使用了 throw, 最后一个 catch 也是可以捕获到的哦. 只要在 promise chain 中, Promise 就会去找得到.
另外, promise chain 中, resolve 和 reject 是可以换来换去的. 在 catch 回调中也可以继续返回 resolve promise, 然后变回 succeed 进入下一个 .then 回调.
before and after
小心坑
上面提到,在 Promise 里面是可以使用 throw 的。
function doSomethingAsync(): Promise<void> {
return new Promise((resolve, reject) => {
throw new Error('test');
});
}
上面这样是 ok 的,不适用 reject 也可以,但是下面这样就不行
function doSomethingAsync(): Promise<void> {
return new Promise((resolve, reject) => {
setTimeout(() => {
throw new Error('test');
}, 3000);
});
}
因为 throw 被 setTimeout wrap 起来了(注:不只是 setTimeout,只要 wrap 起来作用域跑掉就不可以了)。这种情况下就必须使用 reject 才行。
.finally
.then for resolve
.catch for reject
.finally for resolve or reject.
就是说不管结果是 succeed 还是 failed 都执行 finally 的回调.
这个跟 try catch finally 概念差不多.
Promise.resolve & Promise.reject
Promise.resolve 是一个方法, 它接受一个值. 返回一个 promise.
console.log('1');
Promise.resolve('value').then((value) => {
console.log('3', value); // value
});
console.log('2');
通常会这样写有 2 个目的.
delay 操作
当想 delay 一个执行的时候, 通常会使用 setTimeout, 但是 setTimeout 默认是 4ms. 有没有一种方式就把执行押后到当前 stack 运行完呢?
那就是用 Promise
console.log('1');
setTimeout(() => {
console.log('4');
});
Promise.resolve().then(() => {
console.log('3');
});
console.log('2');
同步 / 异步函数
有些函数依据情况来决定是否是异步的. 但即使同步的情况下它依然得返回 Promise, 因为返回接口必须是一样的呀
比如上面的 delay. 当 0 的时候, 不希望用 setTimeout 的话可以这样写.
function delayAsync(delayTime: number): Promise<void> {
if (delayTime === 0) {
return Promise.resolve(); // 直接 resolve
} else {
const promise = new Promise<void>((resolve, _reject) => {
setTimeout(() => {
resolve();
}, delayTime);
});
return promise;
}
}
它和下面这个写法是等价的
return new Promise(resolve => resolve());
Promise.reject() 和 Promise.resolve() 概念是一样, 我就不说了.
Promise all, race, any, allSettled
Promise.all
Promise.all([delayAsync(1000), delayAsync(2000)]).then(([v1, v2]) => {
console.log('done', [v1, v2]);
});
当所有的 delayAsync 都 resolve 之后触发回调, 并且拿到所有的返回值.
注意: 所以 Promise.all 的成员会依照顺序去执行, 但不会等待前者 resolve. 像上面的例子, delayAsync(2000) 不会等 delayAsync(1000) 1 秒后才执行 (不会等哦).
当遇到 reject
只要其中一个 reject, catch 马上会被执行, Promise 不会等待其它 all 的成员.
Promise.all([
delayAsync(1000),
delayAsync(2000),
Promise.reject('c'),
Promise.reject('d'),
])
.then(([v1, v2]) => {
console.log('done', [v1, v2]);
})
.catch((value) => {
console.log('failed', value); // value only contain 'c' 因为其中一个 reject 后, catch 马上就执行了, Promise 不会等其成员了
});
Promise.race
.race 和 .all 的区别是, all 会等到所有成员 succedd 才返回所有值, 而 race 则是第一个 succeed 后直接返回它的 值. 其它的就不等了.
当遇到 reject 的情况就和 .all 一样. 其中一个 reject 立马执行 catch, 不等其它成员.
Promise.any (es2021)
这个方法很新哦.
它和 race 有点像, 主要的区别是面对 reject 的时候.
any 的意思是只要其中一个 succeed 就 ok. 所以当出现 reject 的时候, 它不会像 .all 或者 .race 那样立马去 catch. 只有当全部成员都 reject 才会进入 catch.
比如说, 3 个成员, 第一个 reject, 无所谓, 继续等第二个, 假设等 2 个 succeed resolve, 那么就直接进入 .then 的回调. 返回成员 2 resolve 的值, 不会等待成员 3.
Promise.allSettled (es2020)
allSettled 顾名思义, 就是等所以的 promise 执行完. 它和 .all 不同的地方是, 它不管成员是 succeed 还是 failed.
只要全部有结果以后就会触发 .then 回调, 然后把状态和返回值传进去
Promise.allSettled([delayAsync(1000).then(() => Promise.resolve('value1')), delayAsync(3000).then(() => Promise.reject())]).then(v => {
console.log('v', v);
}).catch((e) => {
console.log('e', e); // never run, even all failed
});
无论怎样 catch 都不会被执行哦, 所以不要写 catch.
.then 回调接受的参数, 表示了每个成员的结果.
请继续看下一篇
JavaScript – 用 Generator 运行异步函数 & await async
JavaScript – Promise的更多相关文章
- [Javascript] Promise
Promise 代表着一个异步操作,这个异步操作现在尚未完成,但在将来某刻会被完成. Promise 有三种状态 pending : 初始的状态,尚未知道结果 fulfilled : 代表操作成功 r ...
- Javascript Promise 学习笔记
1. 定义:Promise是抽象异步处理对象以及对其进行各种操作的组件,它把异步处理对象和异步处理规则采用统一的接口进行规范化. 2. ES6 Promises 标准中定义的API: ...
- 【译】JavaScript Promise API
原文地址:JavaScript Promise API 在 JavaScript 中,同步的代码更容易书写和 debug,但是有时候出于性能考虑,我们会写一些异步的代码(代替同步代码).思考这样一个场 ...
- JavaScript Promise:去而复返
原文:http://www.html5rocks.com/en/tutorials/es6/promises/ 作者:Jake Archibald 翻译:Amio 女士们先生们,请准备好迎接 Web ...
- javaScript Promise 入门
Promise是JavaScript的异步编程模式,为繁重的异步回调带来了福音. 一直以来,JavaScript处理异步都是以callback的方式,假设需要进行一个异步队列,执行起来如下: anim ...
- JavaScript Promise异步实现章节的下载显示
Links: JavaScript Promise:简介 1.一章一章顺序地下载显示下载显示 使用Array.reduce()和Promise.resolve()将各章的下载及显示作为整体串联起来. ...
- Javascript - Promise学习笔记
最近工作轻松了点,想起了以前总是看到的一个单词promise,于是耐心下来学习了一下. 一:Promise是什么?为什么会有这个东西? 首先说明,Promise是为了解决javascript异步编 ...
- Javascript Promise入门
是什么? https://www.promisejs.org/ What is a promise? The core idea behind promises is that a promise r ...
- JavaScript Promise API
同步编程通常来说易于调试和维护,然而,异步编程通常能获得更好的性能和更大的灵活性.异步的最大特点是无需等待."Promises"渐渐成为JavaScript里最重要的一部分,大量的 ...
- 【JavaScript】JavaScript Promise 探微
http://www.html-js.com/article/Promise-translation-JavaScript-Promise-devil-details 原文链接:JavaScript ...
随机推荐
- [oeasy]python0135_变量名与下划线_dunder_声明与赋值
变量定义 回忆上次内容 变量 就是 能变的量 上次研究了 变量标识符的 规则 第一个字符 应该是 字母或下划线 合法的标识符可以包括 大小写字母 数字 下划线 还研究了字符串(str)的函数 ...
- Swift开发基础08-高阶函数
高阶函数是指接受其它函数作为参数,或者返回其它函数的函数.Swift 提供了许多内置的高阶函数,这些函数在处理集合类型数据(如数组.集合等)时尤其有用.常见的高阶函数包括 map.filter.red ...
- ASP.NET Core 程序集注入(二)
public void ConfigureServices(IServiceCollection services) { string strValue = Configuration.GetSect ...
- 🚀RabbitMQ+redis+Redisson分布式锁+seata实现订单服务
引言 订单服务涉及许多方面,分布式事务,分布式锁,例如订单超时未支付要取消订单,订单如何防止重复提交,如何防止超卖.这里都会使用到. 开启分布式事务可以保证跨多个服务的数据操作的一致性和完整性, 使用 ...
- 手把手教你集成GraphRag.Net:打造智能图谱搜索系统
在人工智能和大数据发展的背景下,我们常常需要在项目中实现知识图谱的应用,以便快速.准确地检索和使用信息. 今天,我将向大家详细介绍如何在一个新的.NET项目中集成GraphRag.Net,这是一个参考 ...
- hadoop hive hbase flume sqoop基本操作
top 里的id为cpu空闲度 如果wa为99.8就是负担太重.得停掉一些任务 cat /proc/cpuinfo 查看cpu信息 cat /proc/meminfo 查看内存信息 hadoop基础操 ...
- a-from提交时遇到errorFields:[]验证错误(vue3)
应用场景:使用a-form组件,里面使用a-select组件:当a-select组件内的值发生改变时,调用a-form的验证表单,进而提交. 问题:提交时遇到errorFields:[]验证错误 解决 ...
- 【C】Re05 指针
一.变量 & 指针 变量 = 内存地址 + 存储值 指针变量 = 内存地址 + 存储值[变量的内存地址] 作用: 间接访问内存地址 内存地址 = 地址编号 地址编号:内存中的每个字节唯一的编号 ...
- gpg 密钥的导入、导出
参考: gpg 密钥生成.导入.导出.自动输入密码 How do I delete secret subkeys correctly? 注意: 本文不对GPG的操作做详细介绍,有深入了解者自行参考:g ...
- 强化学习中经典算法 —— reinforce算法 —— (进一步理解, 理论推导出的计算模型和实际应用中的计算模型的区别)
在奖励折扣率为1的情况下,既没有折扣的情况下,reinforce算法理论上可以写为: 但是在有折扣的情况下,reinforce算法理论上可以写为: 以上均为理论模型. ================ ...