Repository navigation
ES6 系列之我们来聊聊 Promise #98
Description
Activity
大大终于更新啦,可喜可贺,消灭零评论。
《你不知道的 JavaScript 中卷》这里有英文版,也可以看~
Reacted by 哆啦好梦 and oudafeidoA()
doF()
doB()
doC()
doE()
doD()
这里的执行顺序是有问题的吧?不过如果doA 是异步的话当我没说
这个primitify代码看的似懂非懂的,想尝试一下怎么用,但是不会用,能否有个例子让观摩学习一下
Reacted by qiwei, hellobabyting, 哆啦好梦, longer and yangaolinReacted by yingye, hellobabyting and Tylerrrkd如果不使用setTimeout, 使用next和Promise能自动执行generator直至结束吗?而且还不阻塞线程也就是说是异步的
var fs = require('fs') // callback usage fs.readdir('./', function (err, files) { ... }) // promisify let promisifyReadDir = promisify(fs.readdir) promisifyReadDir('./').then(files => { console.log(files) })
Reacted by cixing, XiaoJG, ZxAin, hangfeng, 强啊强 and NieHangReacted by 强啊强红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次
这里是 红3 绿1 黄2 红3 绿1 黄2 红3....
难道没人发现这里不对吗?包括一道关于Promise应用的面试题
不应该是这个顺序吗?
var step = function() { Promise.resolve().then(function(){ return light(3000, red); }).then(function(){ return light(1000, green); }).then(function(){ return light(2000, yellow); }).then(function(){ step(); }); }
红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次
这里是 红3 绿1 黄2 红3 绿1 黄2 红3....
难道没人发现这里不对吗?包括一道关于Promise应用的面试题
不应该是这个顺序吗?
var step = function() { Promise.resolve().then(function(){ return light(3000, red); }).then(function(){ return light(1000, green); }).then(function(){ return light(2000, yellow); }).then(function(){ step(); }); }
这里没什么不妥啊, 只是跟你的那篇文章有些许差异而已,你step()里接收了一个立即resolve()的promise 跟Promise.resolve() 效果一样的
这个primitify代码看的似懂非懂的,想尝试一下怎么用,但是不会用,能否有个例子让观摩学习一下
用法大概是这样:
callback 方式: stat(path, (err, res) => .....)
promise 方式: promisify(stat)(path).then(res => console.log(res))function promisify(original) { // promisify(stat) 这一步 return 下一行 return function (...args) { // 将 original 函数接管,比如调用 promisify(stat)(path) 则 return 下一行的 promise return new Promise((resolve, reject) => { // 将 arguments 里面新增一个 original 的 callback,用来改变 promise 的状态 args.push(function callback(err, ...values) { if (err) { return reject(err); } return resolve(...values) }); // 执行原函数(args 已经新增了 callback 了) original.call(this, ...args); }); }; }
如有错误,请指正
Reacted by stray_bird_这个primitify代码看的似懂非懂的,想尝试一下怎么用,但是不会用,能否有个例子让观摩学习一下
这样会好理解一点
function promisify (original) { return function () { return new Promise((resolve, reject) => { original(...arguments, (err, ...values) => { if (err) return reject(err); return resolve(...values); }); }); }; }8 remaining items
又看了一遍,发现题目描述是错误的,而且各个平台这个题目都是这样照搬过来的,“红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次” 应该改为 “红灯亮一次亮三秒,绿灯亮一次亮一秒,黄灯亮一次亮2秒”
打印一下调用函数时候的秒数:
function red () { console.log('red: ', new Date().getSeconds()) console.log('red') } function green () { console.log('green: ', new Date().getSeconds()) console.log('green') } function yellow () { console.log('yellow: ', new Date().getSeconds()) console.log('yellow') } var light = function (timmer, cb) { return new Promise(function (resolve, reject) { setTimeout(function () { cb() resolve() }, timmer) }) } var step = function () { Promise.resolve().then(function () { return light(3000, red) }).then(function () { return light(2000, green) }).then(function () { return light(1000, yellow) }).then(function () { step() }) } step() // 打印结果 // red: 14 // red // green: 16 // green // yellow: 17 // yellow // red: 20 // red // green: 22 // green // yellow: 23 // yellow // red: 26 // red // green: 28 // green // yellow: 29 // yellow
如果不对,欢迎打脸😶
题目没错,不是亮一次亮几秒。
你说的是“红灯亮一次亮三秒,绿灯亮一次亮一秒,黄灯亮一次亮2秒”。如果按灯亮几秒算的话,以你提供的秒数为例:14秒红灯亮,16秒绿灯亮,17秒黄灯亮,20秒红灯亮。结果是红灯亮了2秒,绿灯亮了1秒,黄灯亮了3秒。
我的理解是这个题里的灯几秒钟亮一次无所谓,重点是1⃣️使用promise的链式调用来保证灯按顺序亮,2⃣️使用递归来重复亮灯。刚看了阮一峰的Promise
var cache = {...};
function downloadFile(url) {
if(cache.has(url)) {
// 如果存在cache,这里为同步调用
return Promise.resolve(cache.get(url));
}
return fetch(url).then(file => cache.set(url, file)); // 这里为异步调用
}
console.log('1');
getValue.then(() => console.log('2'));
console.log('3');这个地方如果 return Promise.resolve(cache.get(url)); 那他并不会 打印 1,2,3 因为 Promise.resolve立马执行也是在当前 event loop 的结束前执行
Reacted by 銀桑, yukuai123 and CMJ_YBFJ同步就没有必要用地狱回调和Promise了,这里讲的回调默认指的是异步了。
function workMyCollection(arr) { return arr.reduce(function(promise, item) { return promise.then(function(result) { return doSomethingAsyncWithResult(item, result); }); }, Promise.resolve()); }队列这里是容易引起误解的。需要其实传入[函数1, 函数2],而不是Promise实例 [ new Promise(), new Promise()]。
arr需要包含一系列异步函数而不是直接包含Promise实例,如果直接包含实例,Promise实例会数组在初始化时就开始执行,而不会等到第一个Promise执行完再执行第二个。return doSomethingAsyncWithResult(item, result);这一行其实是指讲上一个result作为参数传递给下一个promise 函数
return item(result)这个问题在【Promise 嵌套】一节也存在,bad里面表达的先后执行队列与Promise.all的同时执行差异是很大的。
还可以思考下如果按照队列执行,又想拿到所有的Promise实例结果怎么实现?
下面给出一个简单的版本,希望大家斧正。function loadSomething(){ return new Promise((resolve,reject)=>{ setTimeout(()=>{ console.log('reject loadSomething') resolve('loadSomethingDone') },2000) }) } function loadAnotherthing(){ return new Promise((resolve,reject)=>{ setTimeout(()=>{ console.log('reject loadAnotherthing') resolve('loadAnotherthingDone') },1000) }) } function promiseOrder(promises){ return new Promise((resolve, reject) => { const result = [] let promiseIndex = 0; const runner = ()=>{ if(promiseIndex === promises.length){ resolve(result); return; } promises[promiseIndex++]().then((res)=>{ result.push(res) runner() }).catch(e=>{ reject(e) }) } runner() }) } promiseOrder([loadSomething, loadAnotherthing]).then(results=>{ console.log(results,'allResult') })刚看了阮一峰的Promise var cache = {...}; function downloadFile(url) { if(cache.has(url)) { // 如果存在cache,这里为同步调用 return Promise.resolve(cache.get(url)); } return fetch(url).then(file => cache.set(url, file)); // 这里为异步调用 } console.log('1'); getValue.then(() => console.log('2')); console.log('3');
这个地方如果 return Promise.resolve(cache.get(url)); 那他并不会 打印 1,2,3 因为 Promise.resolve立马执行也是在当前 event loop 的结束前执行
个人理解,Promise 反模式中有关于2.断开的 Promise 链两个例子应该是不相等的, 如下面
// false let p1 = function anAsyncCall() { var promise = Promise.resolve(1); promise.then(function() { Promise.reject(1) }); return promise; } // good let p2 = function anAsyncCall() { var promise = Promise.resolve(1); return promise.then(function() { Promise.reject(1) }); } p1() === p2 ()// false如果在then里面加上return会更明显
// bad let p1 = function anAsyncCall() { var promise = Promise.resolve(1); promise.then(function() { return Promise.reject(1) }); return promise; } // good let p2 = function anAsyncCall() { var promise = Promise.resolve(1); return promise.then(function() { return Promise.reject(1) }); } p1(); // Promise {<fulfilled>: 1} p2(); // Promise {<rejected>: 1}原因: bad里面的返回的是
Promise.resolve这个promise, good里面返回的是promise.then这个promise, 这点对照参考源码比较清晰理解红绿灯问题,绿灯和黄灯的时间写错了吧。
题目:红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次;如何让三个灯不断交替重复亮灯?(用 Promse 实现)
const now = () => new Date().toLocaleTimeString() function red(){ console.log(now(), 'red'); } function green(){ console.log(now(), 'green'); } function yellow(){ console.log(now(), 'yellow'); } var light = function(timmer, cb){ return new Promise(function(resolve, reject) { setTimeout(function() { cb(); resolve(); }, timmer); }); }; var step = function() { console.log(now()) Promise.resolve().then(function(){ return light(3000, red); }).then(function(){ return light(1000, green); }).then(function(){ return light(2000, yellow); }) } step();
15:09:07 15:09:10 red 15:09:11 green 15:09:13 yellow
function doA(callback){
console.log("doA")
callback();
}
function doB(){
console.log("doB")
}
function doC(callback){
console.log("doC")
callback();
}
function doD(){
console.log("doD")
}
function doE(){
console.log("doE")
}function doF(){
console.log("doF")
}doA(function(){
doB();
doC(function(){
doD();
});
doE();
})doF();
简单代码来看,顺序和你说的不一致!
doA
doB
doC
doD
doE
doF又看了一遍,发现题目描述是错误的,而且各个平台这个题目都是这样照搬过来的,“红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次” 应该改为 “红灯亮一次亮三秒,绿灯亮一次亮一秒,黄灯亮一次亮2秒”
打印一下调用函数时候的秒数:function red () { console.log('red: ', new Date().getSeconds()) console.log('red') } function green () { console.log('green: ', new Date().getSeconds()) console.log('green') } function yellow () { console.log('yellow: ', new Date().getSeconds()) console.log('yellow') } var light = function (timmer, cb) { return new Promise(function (resolve, reject) { setTimeout(function () { cb() resolve() }, timmer) }) } var step = function () { Promise.resolve().then(function () { return light(3000, red) }).then(function () { return light(2000, green) }).then(function () { return light(1000, yellow) }).then(function () { step() }) } step() // 打印结果 // red: 14 // red // green: 16 // green // yellow: 17 // yellow // red: 20 // red // green: 22 // green // yellow: 23 // yellow // red: 26 // red // green: 28 // green // yellow: 29 // yellow
如果不对,欢迎打脸😶
题目没错,不是亮一次亮几秒。 你说的是“红灯亮一次亮三秒,绿灯亮一次亮一秒,黄灯亮一次亮2秒”。如果按灯亮几秒算的话,以你提供的秒数为例:14秒红灯亮,16秒绿灯亮,17秒黄灯亮,20秒红灯亮。结果是红灯亮了2秒,绿灯亮了1秒,黄灯亮了3秒。 我的理解是这个题里的灯几秒钟亮一次无所谓,重点是1⃣️使用promise的链式调用来保证灯按顺序亮,2⃣️使用递归来重复亮灯。
题目描述确实是错误的,我个人测试情况如下:
red 2024-04-12T10:46:58.217Z
green 2024-04-12T10:47:01.225Z
yellow 2024-04-12T10:47:03.227Z
red 2024-04-12T10:47:04.228Z
green 2024-04-12T10:47:07.230Z
yellow 2024-04-12T10:47:09.231Z
red 2024-04-12T10:47:10.233Z红灯亮三秒,然后绿灯亮两秒,黄灯亮一秒。并不是隔几秒亮一次,同一种颜色的灯都是隔六秒亮一次。
题目:红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次;如何让三个灯不断交替重复亮灯?(用 Promse 实现) 少了一个i
- 您好!您的邮件已收到,我会尽快查收!若有急事请拨打我的电话:13668913609(663609)谢谢!
- 这是来自QQ邮箱的假期自动回复邮件。你好,你的邮件我收到,谢谢。
前言
Promise 的基本使用可以看阮一峰老师的 《ECMAScript 6 入门》。
我们来聊点其他的。
回调
说起 Promise,我们一般都会从回调或者回调地狱说起,那么使用回调到底会导致哪些不好的地方呢?
1. 回调嵌套
使用回调,我们很有可能会将业务代码写成如下这种形式:
当然这是一种简化的形式,经过一番简单的思考,我们可以判断出执行的顺序为:
然而在实际的项目中,代码会更加杂乱,为了排查问题,我们需要绕过很多碍眼的内容,不断的在函数间进行跳转,使得排查问题的难度也在成倍增加。
当然之所以导致这个问题,其实是因为这种嵌套的书写方式跟人线性的思考方式相违和,以至于我们要多花一些精力去思考真正的执行顺序,嵌套和缩进只是这个思考过程中转移注意力的细枝末节而已。
当然了,与人线性的思考方式相违和,还不是最糟糕的,实际上,我们还会在代码中加入各种各样的逻辑判断,就比如在上面这个例子中,doD() 必须在 doC() 完成后才能完成,万一 doC() 执行失败了呢?我们是要重试 doC() 吗?还是直接转到其他错误处理函数中?当我们将这些判断都加入到这个流程中,很快代码就会变得非常复杂,以至于无法维护和更新。
2. 控制反转
正常书写代码的时候,我们理所当然可以控制自己的代码,然而当我们使用回调的时候,这个回调函数是否能接着执行,其实取决于使用回调的那个 API,就比如:
对于我们经常会使用的 fetch 这种 API,一般是没有什么问题的,但是如果我们使用的是第三方的 API 呢?
当你调用了第三方的 API,对方是否会因为某个错误导致你传入的回调函数执行了多次呢?
为了避免出现这样的问题,你可以在自己的回调函数中加入判断,可是万一又因为某个错误这个回调函数没有执行呢?
万一这个回调函数有时同步执行有时异步执行呢?
我们总结一下这些情况:
对于这些情况,你可能都要在回调函数中做些处理,并且每次执行回调函数的时候都要做些处理,这就带来了很多重复的代码。
回调地狱
我们先看一个简单的回调地狱的示例。
现在要找出一个目录中最大的文件,处理步骤应该是:
fs.readdir获取目录中的文件列表;fs.stat获取文件信息代码为:
使用方式为:
你可以将以上代码复制到一个比如
index.js文件,然后执行node index.js就可以打印出最大的文件的名称。看完这个例子,我们再来聊聊回调地狱的其他问题:
1.难以复用
回调的顺序确定下来之后,想对其中的某些环节进行复用也很困难,牵一发而动全身。
举个例子,如果你想对
fs.stat读取文件信息这段代码复用,因为回调中引用了外层的变量,提取出来后还需要对外层的代码进行修改。2.堆栈信息被断开
我们知道,JavaScript 引擎维护了一个执行上下文栈,当函数执行的时候,会创建该函数的执行上下文压入栈中,当函数执行完毕后,会将该执行上下文出栈。
如果 A 函数中调用了 B 函数,JavaScript 会先将 A 函数的执行上下文压入栈中,再将 B 函数的执行上下文压入栈中,当 B 函数执行完毕,将 B 函数执行上下文出栈,当 A 函数执行完毕后,将 A 函数执行上下文出栈。
这样的好处在于,我们如果中断代码执行,可以检索完整的堆栈信息,从中获取任何我们想获取的信息。
可是异步回调函数并非如此,比如执行
fs.readdir的时候,其实是将回调函数加入任务队列中,代码继续执行,直至主线程完成后,才会从任务队列中选择已经完成的任务,并将其加入栈中,此时栈中只有这一个执行上下文,如果回调报错,也无法获取调用该异步操作时的栈中的信息,不容易判定哪里出现了错误。此外,因为是异步的缘故,使用 try catch 语句也无法直接捕获错误。
(不过 Promise 并没有解决这个问题)
3.借助外层变量
当多个异步计算同时进行,比如这里遍历读取文件信息,由于无法预期完成顺序,必须借助外层作用域的变量,比如这里的 count、errored、stats 等,不仅写起来麻烦,而且如果你忽略了文件读取错误时的情况,不记录错误状态,就会接着读取其他文件,造成无谓的浪费。此外外层的变量,也可能被其它同一作用域的函数访问并且修改,容易造成误操作。
之所以单独讲讲回调地狱,其实是想说嵌套和缩进只是回调地狱的一个梗而已,它导致的问题远非嵌套导致的可读性降低而已。
Promise
Promise 使得以上绝大部分的问题都得到了解决。
1. 嵌套问题
举个例子:
使用 Promise 后:
而对于读取最大文件的那个例子,我们使用 promise 可以简化为:
2. 控制反转再反转
前面我们讲到使用第三方回调 API 的时候,可能会遇到如下问题:
对于第一个问题,Promise 只能 resolve 一次,剩下的调用都会被忽略。
对于第二个问题,我们可以使用 Promise.race 函数来解决:
对于第三个问题,为什么有的时候会同步执行有的时候回异步执行呢?
我们来看个例子:
在这个例子中,有 cahce 的情况下,打印结果为 1 2 3,在没有 cache 的时候,打印结果为 1 3 2。
然而如果将这种同步和异步混用的代码作为内部实现,只暴露接口给外部调用,调用方由于无法判断是到底是异步还是同步状态,影响程序的可维护性和可测试性。
简单来说就是同步和异步共存的情况无法保证程序逻辑的一致性。
然而 Promise 解决了这个问题,我们来看个例子:
即使 promise 对象立刻进入 resolved 状态,即同步调用 resolve 函数,then 函数中指定的方法依然是异步进行的。
PromiseA+ 规范也有明确的规定:
Promise 反模式
1.Promise 嵌套
2.断开的 Promise 链
3.混乱的集合
你可以写成:
如果你非要以队列的形式执行,你可以写成:
4.catch
如果 somethingElseAsync 抛出错误,是无法被捕获的。你可以写成:
红绿灯问题
题目:红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次;如何让三个灯不断交替重复亮灯?(用 Promse 实现)
三个亮灯函数已经存在:
利用 then 和递归实现:
promisify
有的时候,我们需要将 callback 语法的 API 改造成 Promise 语法,为此我们需要一个 promisify 的方法。
因为 callback 语法传参比较明确,最后一个参数传入回调函数,回调函数的第一个参数是一个错误信息,如果没有错误,就是 null,所以我们可以直接写出一个简单的 promisify 方法:
完整的可以参考 es6-promisif
Promise 的局限性
1. 错误被吃掉
首先我们要理解,什么是错误被吃掉,是指错误信息不被打印吗?
并不是,举个例子:
在这种情况下,因为 throw error 的缘故,代码被阻断执行,并不会打印 233333,再举个例子:
以上代码依然会被阻断执行,这是因为如果通过无效的方式使用 Promise,并且出现了一个错误阻碍了正常 Promise 的构造,结果会得到一个立刻跑出的异常,而不是一个被拒绝的 Promise。
然而再举个例子:
这次会正常的打印
233333,说明 Promise 内部的错误不会影响到 Promise 外部的代码,而这种情况我们就通常称为 “吃掉错误”。其实这并不是 Promise 独有的局限性,try..catch 也是这样,同样会捕获一个异常并简单的吃掉错误。
而正是因为错误被吃掉,Promise 链中的错误很容易被忽略掉,这也是为什么会一般推荐在 Promise 链的最后添加一个 catch 函数,因为对于一个没有错误处理函数的 Promise 链,任何错误都会在链中被传播下去,直到你注册了错误处理函数。
2. 单一值
Promise 只能有一个完成值或一个拒绝原因,然而在真实使用的时候,往往需要传递多个值,一般做法都是构造一个对象或数组,然后再传递,then 中获得这个值后,又会进行取值赋值的操作,每次封装和解封都无疑让代码变得笨重。
说真的,并没有什么好的方法,建议是使用 ES6 的解构赋值:
3. 无法取消
Promise 一旦新建它就会立即执行,无法中途取消。
4. 无法得知 pending 状态
当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
参考
ES6 系列
ES6 系列目录地址:https://github.com/mqyqingfeng/Blog
ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。
如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。