-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ES6 系列之我们来聊聊 Promise #98
Comments
大大终于更新啦,可喜可贺,消灭零评论。 |
《你不知道的 JavaScript 中卷》这里有英文版,也可以看~ |
doA() |
不过如果doA 是异步的话当我没说 |
|
这个primitify代码看的似懂非懂的,想尝试一下怎么用,但是不会用,能否有个例子让观摩学习一下 |
如果不使用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)
}) |
红灯三秒亮一次,绿灯一秒亮一次,黄灯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() 效果一样的 |
用法大概是这样: 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);
});
};
} 如有错误,请指正 |
这样会好理解一点
|
@coderlxf 你这写法更容易理解,但剩余参数 function promisify(original, manyArgs = false) {
return function () {
return new Promise((resolve, reject) => {
original(...arguments, (err, ...values) => {
if (err) return reject(err);
return resolve(manyArgs ? values : values[0]);
});
});
};
} |
无法取消的问题,是不是可以使用Promise.race 实现的promise-abort来解决?(虽然还是会执行) https://www.npmjs.com/package/promise-abortable
|
想到一个问题,红绿灯的问题中,如果想实现中途操作一下,退出递归,该怎么实现呢。 |
|
感谢分享,一点小瑕疵 |
@keep-run 可以step里最后的then判断是否要执行step,比如外部设定执行次数,执行step之前累加 |
又看了一遍,发现题目描述是错误的,而且各个平台这个题目都是这样照搬过来的,“红灯三秒亮一次,绿灯一秒亮一次,黄灯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 如果不对,欢迎打脸😶 |
一般用于将一些第三方的 api的回调函数方式转化成 promise的写法,比如常见的 微信api wx.showModal({
title: '提示',
content: '这是一个模态弹窗',
success (res) {
if (res.confirm) {
console.log('用户点击确定')
} else if (res.cancel) {
console.log('用户点击取消')
}
}
}) 这种方式 就会有回调地狱的现象,换成下面的方式: let promiseify = function (originFn) {
return function () {
let args = [...arguments][0]
return new Promise((resolve, rejcet) => {
args.success = resolve
args.fail = rejcet
originFn.call(this, args)
})
}
}
let myMhowModal = promiseify(wx.showModal)
myMhowModal ({ title: '提示',
content: '这是一个模态弹窗',}).then(....) |
题目没错,不是亮一次亮几秒。 |
刚看了阮一峰的Promise 这个地方如果 return Promise.resolve(cache.get(url)); 那他并不会 打印 1,2,3 因为 Promise.resolve立马执行也是在当前 event loop 的结束前执行 |
同步就没有必要用地狱回调和Promise了,这里讲的回调默认指的是异步了。 |
队列这里是容易引起误解的。需要其实传入[函数1, 函数2],而不是Promise实例 [ new Promise(), new Promise()]。
这一行其实是指讲上一个result作为参数传递给下一个promise 函数
这个问题在【Promise 嵌套】一节也存在,bad里面表达的先后执行队列与Promise.all的同时执行差异是很大的。 |
还可以思考下如果按照队列执行,又想拿到所有的Promise实例结果怎么实现?
|
|
个人理解,Promise 反模式中有关于2.断开的 Promise 链两个例子应该是不相等的, 如下面
如果在then里面加上return会更明显
原因: bad里面的返回的是 |
红绿灯问题,绿灯和黄灯的时间写错了吧。
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){ function doF(){ doA(function(){ doF(); 简单代码来看,顺序和你说的不一致! |
题目描述确实是错误的,我个人测试情况如下: 红灯亮三秒,然后绿灯亮两秒,黄灯亮一秒。并不是隔几秒亮一次,同一种颜色的灯都是隔六秒亮一次。 |
题目:红灯三秒亮一次,绿灯一秒亮一次,黄灯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,对作者也是一种鼓励。
The text was updated successfully, but these errors were encountered: