怎么用es6 promise 兼容实现实现异步控制

这家伙很懒,什么个性签名都没有留下。
本文作者:IMWeb 孙世吉
原文出处:
未经同意,禁止转载
最近实践中需要用到大量的异步回调风格代码的编写,作者最近处于同步编程风格转为异步编程风格的状态。同时第一时间遇到了下面提到的代码,第一直觉就是该代码肯定有问题!但是问题在哪里有讲不出来,感觉非常蛋疼与疑惑。先上当时遇到的代码:
function del() {
return find().then(function(resultOfFind) {
if (!resultOfFind) {
return false;
return reallyDelete();
}, function(err) {
handle(err)
function deleteItem(req, res) {
del().then(function(resultOfDelete) {
res(resultOfDelete)
}, function(err) {
上面代码做的事情很简单:1、删除前检查是否存在;2、存在则执行删除操作 / 不存在则直接返回;3、使用删除的结果 / 处理删除产生的错误,返回响应;
你看出来哪里可能出现问题了吗?
OK,这里容作者买一个小小的关子。同学们如果有疑问,那就继续往下阅读吧。看出问题的大神们也请不吝指教。让作者为你带来打开Promise的正确姿势,让你使用Promise的时候用的更爽,后人接手你的代码看的更爽,也避免出现莫名其妙的问题而无法对问题进行定位的情况。
1、 Promise基础介绍2、 Promise与金字塔问题3、 Promise与循环4、 resolve(value) VS resolve(promise)5、 then返回的promise实例6、 Promise与错误处理7、 Promise状态透传
1. Promise基础介绍
Promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6将其写进了语言标准,统一了用法,原生提供了Promise对象。本文所描述的Promise指Promises/A+规范定义的Promise,可参考。
1.1 常见的用法
var promise = new Promise(function(resolve, reject) {
resolve(value);
reject(error);
promise.then(function(value) {
}, function(error) {
简单来说:1、Promise构造方法接受一个方法作为参数,该方法传入两个参数,resolve和reject。2、resolve用来将Promise对象的状态置为成功,并将异步操作结果value作为参数传给成功回调函数。3、reject用来将Promise对象的状态置为失败,并将异步操作错误error作为参数传给失败回调函数。4、then方法绑定两个回调函数,第一个用来处理Promise成功状态,第二个用来处理Promise失败状态。
1.2 Promise 状态
Promise对象有三种状态:
Pending(进行中)
Fulfilled(已完成,又称为Resolved)
Rejectd(已失败)
如上图所示,Promise对象有两个特点:1、对象状态只由异步操作结果决定。resolve方法会使Promise对象由pendding状态变为fulfilled状态;reject方法或者异常会使得Promise对象由pendding状态变为rejected状态。Promise状态变化只有上图这两条路径。2、对象状态一旦改变,任何时候都能得到这个结果。即状态一旦进入fulfilled或者rejected,promise便不再出现状态变化,同时我们再添加回调会立即得到结果。这点跟事件不一样,事件是发生后再绑定监听,就监听不到了。
举个例子:
var promise = new Promise(function(resolve, reject) {
resolve('value');
promise.then(function(value) {
console.log('fulfilled', value)
}, function(error) {
console.log('rejected', error)
当然也可以参考里面的定义:
2.1.Promise状态
  promise状态为pending, fulfilled和rejected中的其中一种。
  2.1.1.当promise状态为pending时:
  2.1.1.1.promise的状态可以转换为fulfilled或rejected。
  2.1.2.当promise状态为fulfilled时:
  2.1.2.1.无法转换为其他状态。
  2.1.2.2.必须有一个不可改变的值作为onFulfilled事件处理函数的入参
  2.1.3.当promsie状态为rejected时:
  2.1.3.1.无法转换为其他状态。
  2.1.3.2.必须有一个不可改变的值作为onRejected事件处理函数的入参
2. Promise与金字塔问题
金字塔问题指的是我们的程序中如果出现大量的回调任务需要顺序执行时,可能会出现这些代码慢慢向右侧屏幕延伸的问题。通常我们多次异步操作需要依赖上一次异步操作的结果时,我们会这样写。举个例子:
getUserAdmin().then(function(result) {
getProjectsWithAdmin().then(function(result) {
getModules(result.ids).then(function(result) {
getInterfaces(result.ids).then(function(result) {
上面的例子数据有项目-模块-接口这样的层次关系。为了获取接口列表,每一次操作都需要依赖上一个异步操作的结果。你会发现使用顺序调用的逻辑这样写使得代码层次嵌套过深,逻辑不清晰,很难进行阅读。如果我们像使用回调一样使用Promise,虽然结果是正确的,但是这完全没有利用到Promise的优势。
我们应该像下面这样写:
getUserAdmin().then(function(reult) {
return getProjectsWithAdmin();
return getProjectsWithUser();
}).then(function(result) {
return getModules(result.ids);
}).then(function(result) {
return getInterfaces(result.ids)
}).then(function(result) {
如果你再将这些操作封装到一个命名函数中,你就会得到下面这样的代码:
getUserAdmin()
.then(getProjects)
.then(getModules)
.then(getInterfaces)
.then(procResult)
是不是觉得赏心悦目,作者第一次看到这样的代码时简直惊为天人,这简直是在写诗好吗?
这种写法被称为 composing promises ,是 promises 的强大能力之一。这样写可以带来好处:
清晰的代码结构。
避免始料不及的错误。进行快速的问题定位,避免难以调试更甚至于失败了而没有任何反馈。
关于第二点,会在下面错误处理中进行说明。
3. Promise与循环
上面讲到了异步操作的顺序执行,那如果我们需要同时执行一组异步操作呢?举个例子,我们需要删除指定项目ID下的所有子模块。有的同学可能会这样写:
getModules(projectID).then(function(modules) {
modules.forEach(function(module) {
removeModule(module.moduleID);
}).then(function(result) {
这里存在一个问题,就是A位置并不会等待所有的removeModule方法结束,而是直接返回undefined,这意味着后面的方法即不会等待删除动作结束也无法获得删除动作的结果,所以你没办法保证删除动作已经完成。
关于在then方法绑定的回调函数中的返回值,我们会在第五节中进行讨论。
那我们怎么保证所有异步操作都成功了呢?
Promise提供了一个很方便的方法叫做Promise.all。我们可以这样做:
getModules(projectID).then(function(modules) {
var tasks = [];
modules.forEach(function(module) {
tasks.push(removeModule(module.moduleID));
return Promise.all(tasks);
}).then(function(result) {
Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例。Promise.all方法接受一个数组作为参数,数组里的元素都是Promise对象的实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为Promise实例,再进一步处理。(Promise.all方法的参数可以不是数组,但必须具有Iterator接口,且返回的每个成员都是Promise实例。)
当该数组里的所有Promise实例都进入Fulfilled状态,Promise.all返回的实例才会变成Fulfilled状态。并将Promise实例数组的所有返回值组成一个数组,传递给Promise.all返回实例的回调函数。
当该数组里的某个Promise实例都进入Rejected状态,Promise.all返回的实例会立即变成Rejected状态。并将第一个rejected的实例返回值传递给Promise.all返回实例的回调函数。
Promise.race方法跟Promise.all方法差不多。唯一的区别在于该方法返回的Promise实例并不会等待所有Proimse都跑完,而是只要有一个Promise实例改变状态,它就跟着改变状态。并使用第一个改变状态实例的返回值作为返回值。
当然前述的代码还可以更优雅一点:
getModules(projectID).then(function(modules) {
return Promise.all(modules.map(module) {
return removeModule(module.moduleID);
}).then(function(result) {
4. resolve(value) VS resolve(promise)
我们会在异步操作成功时调用resolve函数,其作用是将Promise对象的状态从Pending变为Resolved,并将异步操作的结果,作为参数传递给Fulfilled状态的回调函数。
而我们传入resolve的值实际上并不一定出现在Fulfilled状态的回调函数中,为什么呢?
我们来先看一个使用resolve传递操作结果的例子:
var d = new Date();
var promise = new Promise(function(resolve, reject) {
setTimeout(resolve, 1000, 'resolve from promise');
promise.then(
result =& console.log('result:', result, new Date() - d),
error =& console.log('error:', error)
大约过了一秒左右,我们可以看到在fulfilled状态的回调方法中,我们打印出了上面注释中的内容。我们能够通过resolve方法传递操作的结果,然后在回调方法中使用这些结果。
如果我们在resolve中传入一个Promise实例呢?
var d=new Date();
// 创建一个promise实例,该实例在2秒后进入fulfilled状态
var promise1 = new Promise(function(resolve, reject) {
setTimeout(resolve, 2000, 'resolve from promise 1');
// 创建一个promise实例,该实例在1秒后进入fulfilled状态
var promise2 = new Promise(function(resolve, reject) {
setTimeout(resolve, 1000, promise1); // resolve(promise1)
promise2.then(
result =& console.log('result:', result,new Date()-d),
error =& console.log('error:', error)
上面的例子中,你可能觉得再经过一秒左右后会打印出promise1实例。实际上上面的代码最后打印出:
result: resolve from promise 1 2002
OK,我们看一下是怎么说的:[[Resolve]](promise, x)中
2.3.2.如果x是一个promise实例, 则以x的状态作为promise的状态
  2.3.2.1.如果x的状态为pending, 那么promise的状态也为pending, 直到x的状态变化而变化。
  2.3.2.2.如果x的状态为fulfilled, promise的状态也为fulfilled, 并且以x的不可变值作为promise的不可变值。
  2.3.2.3.如果x的状态为rejected, promise的状态也为rejected, 并且以x的不可变原因作为promise的不可变原因。
2.3.4.如果x不是对象或函数,则将promise状态转换为fulfilled并且以x作为promise的不可变值。
简单来说呢,就是因为promise2中调用了resolve(promise1),此时promise1的状态会传递给promise2,或者说promise1的状态决定了promise2的状态。所以当promise1进入fulfilled状态,promise2的状态也变为fulfilled,同时将promise1自己的不可变值作为promise2的不可变值,所以promise2的回调函数打印出了上述结果。promise1进入rejected状态的结果,同学们可以自己试一试。
而当我们resolve(value)的时候就遵循Promise/A+中的2.3.4条规范,将value传递给了fulfilled状态的回调函数。
另外,通过这里例子我们也可以发现。运行时间是2秒而不是3秒。也就是说Promise新建后就会立即执行。
ps:resolve方法传入的实参不限于值类型或者Promise实例。更多内容请参考。
5. then返回的promise实例
then方法返回的是一个新的Promise实例then方法返回的是一个新的Promise实例then方法返回的是一个新的Promise实例重要的事情要说三遍。这也是我们能够使用第二节中的链式写法的最重要原因。举个例子:
var d = new Date();
var promise = new Promise(function(resolve, reject) {
setTimeout(resolve, 1000, 'resolve from promise');
var promise2 = promise.then(function(result) {
console.log(result);
promise.tag = '1';
console.log(promise)
console.log(promise2)
当然你可以在同一个Promise实例中多次调用.then绑定回调方法,当该Promise实例状态变化时,将按调用.then的顺序执行回调方法。
那在.then绑定的回调方法onFulfilled和onRejected中,不同的返回值对后续链式有什么影响呢?
5.1 回调方法返回 值
var d = new Date();
var promise1 = new Promise(function(resolve, reject) {
setTimeout(resolve, 1000, 'resolve from promise1');
var promise2 = promise1.then(function(result) {
console.log('promise1.then(resolve):', result);
}, function(error) {
console.log('promise1.then(reject):', error);
promise2.then(
result =& console.log('result:', result, new Date() - d),
error =& console.log('error:', error, new Date() - d)
通过运行上面的例子,我们会发现promise的状态无论是fulfilled或者rejected,其绑定的.then方法返回的Promise实例(即promise2)都只会执行它的onFulfilled回调方法。
5.2 回调方法返回 promise实例
var d = new Date();
var promise1 = new Promise(function(resolve, reject) {
setTimeout(resolve, 1000, 'resolve from promise1');
var promise2 = promise1.then(function(result) {
console.log('promise1.then(resolve):', result);
return new Promise(function(resolve, reject) {
setTimeout(resolve, 2000, 'from new promise');
promise2.then(
result =& console.log('result:', result, new Date() - d),
error =& console.log('error:', error, new Date() - d)
上面的例子中,promise1的回调方法onFulfilled返回的是一个新的promise实例,该实例在2秒后进入fulfilled状态。运行该例子,我们会发现最后promise2的回调方法近三秒钟后才执行。也就是promise1和promise2的总共运行时间,为什么呢?同时第二行打印的内容来自于promise2回调方法中返回的新Promise实例,这就是怎么一个过程呢?
Promise/A+规定
2.2.7. then方法必须返回一个promise实例
promise2 = promise1.then(onFulfilled, onRejected);
  2.2.7.1. 如果 onFulfilled 或 onRejected 函数返回值为x,那么执行Promise处理过程 [[Resolve]](promise2, x)。
  2.2.7.2. 如果 onFulfilled 或 onRejected 函数抛出异常e,那么promise2将执行 reject(e)。
查看其中2.2.7.1的规定,我们会发现在onFulfilled或者onRejected中,无论是return值或者return Promise实例,实际上都是去调用[[Resolve]](promise2, x)。
当我们在promise1的回调方法中返回x的时候,相当于调用promise2.resolve(x)。
所以结合本文第四节的内容,我们可以就知道
:当x为值的时候,promise2直接进入fulfilled状态,无论promise1的状态是fulfilled或者是rejected,并把x传给onFulfilled回调方法;当x为promise实例的时候,x的状态决定了promise2的状态。
5.3 回调方法没有返回语句
如果promise1的回调方法中没有返回语句,那promise2的回调方法中会打印什么内容呢?
var d = new Date();
var promise1 = new Promise(function(resolve, reject) {
setTimeout(resolve, 1000, 'resolve from promise1');
var promise2 = promise1.then(function(result) {
console.log('promise1.then(resolve):', result);
promise2.then(
result =& console.log('result:', result, new Date() - d),
error =& console.log('error:', error, new Date() - d)
当js函数中没有返回语句的时候,相当于是return undefined。也就是说相当调用了promise2.resolve(x),而这里的x为undefined,所以我们在promise2的回调方法中打印出了undefined。
所以这里作者给的建议是:在回调方法中一定要有return语句,放弃在回调方法中使用return,相当于放弃获取在该回调方法中的所有操作结果。
6. Promise与错误处理
.then(onFulfilled,onRejected).then传入的第二个回调方法在Promise实例状态变为rejected的时候会被调用,通常用于处理异步操作失败的情况。让我们再来瞄一眼本文开头的代码:
function del() {
return find().then(function(resultOfFind) {
if (!resultOfFind) {
return false;
return reallyDelete();
}, function(err) {
handle(err)
function deleteItem(req, res) {
del().then(function(resultOfDelete) {
res(resultOfDelete)
}, function(err) {
经过上面前五节的讲解,想必你一定可以找出问题:A位置:如果find方法返回的promise实例如果进入rejected状态,经过handle的处理后,没有明确的将错误返回,或者将该promise实例置为rejected状态。这种情况在第5.3节已经提过了,这里del方法返回的promise实例直接就是fulfilled状态,而且传入的回调参数为undefined,也就是说这边的resultOfDelete为undefined。这可能导致后续的回调函数没办法正常的工作,而且对这种异常情况完全没有做处理。
B位置:通过对A位置的分析,相比你也发现B位置的错误处理实际上并不能很好的处理到promise实例的异常。除了上面的情况下B位置无法处理到来自del方法的rejected状态,当C位置出现错误的时候,B位置的错误处理代码也同样无法处理。
那么我们应该怎么做呢?首先在A位置,如果你需要在这里对异常或者rejected状态做操作,例如记录系统日志。请考虑后续是否可能会用到del的操作结果,如果存在这种情况(例如上文我们的用法),那么一定要将该状态暴露到下一个promise中。
function del() {
return find().then(function(resultOfFind) {
}, function(err) {
handle(err);
return Promise.reject(err);
但是这样又可能出现对同一个错误多次处理的情况。作者这里推荐使用.catch方法。
Promise.prototype.catch方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。
somePromise.catch(function(err) {
somePromise.then(null, function(err) {
但是下面两段代码是不等价的
somePromise.then(function() {
return someOtherPromise();
}, function(err) {
somePromise.then(function() {
return someOtherPromise();
}).catch(function(err) {
为什么呢?举一个例子你可能就明白了:
somePromise.then(function() {
throw new Error('oh no');
}, function(err) {
somePromise.then(function() {
throw new Error('oh no');
}).catch(function(err) {
你会发现上一种写法你没办法处理onFulfilled回调函数抛出的异常,而第二种是可以处理这种情况的异常的。所以作者推荐大家都是用catch来处理失败情况,而不是then的第二个参数。你可以在你的promise最后都加上一个catch,以处理你可能没有察觉到的错误情况。当然有些情况下我们不得不使用then的第二个参数,这时候你就需要注意是否存在别人调用这个方法的可能,并做好错误处理。所以文章开头出现的问题可以这样解决:
function del() {
return find().then(function(resultOfFind) {
if (!resultOfFind) {
return false;
return reallyDelete();
function deleteItem(req, res) {
del().then(function(resultOfDelete) {
res(resultOfDelete)
}).catch(function(err) {
7.Promise状态透传
在看上一节内容的时候你可能会有疑问,为什么find.then中没有设置onRejected回调函数对rejected状态进行处理,后面可以使用catch直接捕获之前的结果呢?我们来先看个例子:
var d = new Date();
var promise1 = new Promise(function(resolve, reject) {
setTimeout(reject, 1000, 'reject from promise1');
var promise2 = promise1.then(result =& {
console.log('promise1.then(resolve):', result);
promise2.then(
result =& console.log('result:', result, new Date() - d),
error =& console.log('error:', error, new Date() - d)
是的,正如我们所想,promise2的onRjected回调方法正确的处理了来自promise1的rejected状态。也就是说promise1的rejected状态以及不可变原因都传递给了promise2。
来看是怎么说的:
2.2.7. then方法必须返回一个promise实例
promise2 = promise1.then(onFulfilled, onRejected);
  2.2.7.3. 如果 promise1的 onFulfilled 不是函数,那么promise1的不可变值将传递到promise2并作为promise2的不可变值。
  2.2.7.4. 如果 promise1的 onRejected不是函数,那么promise1的不可变原因将传递到promise2并作为promise2的不可变原因,并作为promise2的 onRejected 的入参。
这就是Promise的状态透传特点,如果当前的promise实例没有绑定回调函数,或者绑定的不是函数,那么当前实例就会把其状态以及不可变值或者不可变原因传递给当前实例调用.then方法返回的新promise实例。在上述例子中就表现为,promise1把它的不可变原因以及rejected状态传递给了promise2,所以promise2的onRejected回调方法就把promise1中reject的内容打印出来了。这也是为什么我们在第六节提到可以使用最后的catch捕获之前没有进行处理的rejected状态的原因了。
本文从项目中遇到的代码出发,先是讲解Promise链式调用与循环调用的常见用法,接着结合规范,对Promise使用过程中容易出现的疑点进行了剖析与验证,并解决了项目代码的问题。
请大家记住三点:
回调方法中一定要使用return语句,避免调用者丢失其处理状态与结果。
在promise实例的最后使用catch方法,用来做整体的异常捕获与处理。
利用Promise.then方法对回调函数返回结果的封装,写出清晰漂亮的链式调用代码。
一个IMWeb账号或者
才能进行评论。
已有账号,立即登录
通过以下账号登录:
IMWeb是真人前端社区,填写公司信息能帮助你更好的提升业内影响力。
公司邮箱仅作一次性的验证使用,您不必担心信息泄露与垃圾邮件。
如今,他们已入驻IMWeb:
填写公司信息
自由职业者,学生,或待业中?
返回上一步
IMWEB团队正式成立是时间是日,目前主要负责腾讯在线教育战略产品腾讯课堂,多人社交互动视频以及活动组织类项目的研发工作。用信号来控制异步流程 - WEB前端 - 伯乐在线
& 用信号来控制异步流程
我们知道,JavaScript 不管是操作 DOM,还是执行服务端任务,不可避免需要处理许多异步调用。在早期,许多开发者仅仅通过 JavaScript 的回调方式来处理异步,但是那样很容易造成异步回调的嵌套,产生 “Callback Hell”。
后来,一些开发者使用了 Promise 思想来避免异步回调的嵌套,社区将根据思想提出 Promise/A+ 规范,最终,在 ES6 中内置实现了 Promise 类,随后又基于 Promise 类在 ES2017 里实现了 async/await,形成了现在非常简洁的异步处理方式。
下面这段代码就是典型的 async/await 用法,它看起来和同步的写法完全一样,只是增加了 async/await 关键字。
module.exports = class extends think.Controller {
async indexAction(){
let model = this.model('user');
await model.startTrans();
let userId = await model.add({name: 'xxx'});
let insertId = await this.model('user_group').add({user_id: userId, group_id: 1000});
}catch(e){
await model.rollback();
12345678910111213
module.exports = class extends think.Controller {&&async indexAction(){&&&&let model = this.model('user');&&&&try{&&&&&&await model.startTrans();&&&&&&let userId = await model.add({name: 'xxx'});&&&&&&let insertId = await this.model('user_group').add({user_id: userId, group_id: 1000});&&&&&&await model.commit();&&&&}catch(e){&&&&&&await model.rollback();&&&&}&&}}
async/await 可以算是一种语法糖,它将
promise.then(res =& {
}).catch(err =& {
some error
promise.then(res =& {&&&&do sth.}).catch(err =& {&&&&some error})
res = await promise
}catch(err){
some error
try{&&&&res = await promise&&&&do sth}catch(err){&&&&some error}
有了 async,await,可以写出原来很难写出的非常简单直观的代码:
function idle(time){
return new Promise(resolve=&setTimeout(resolve, time))
(async function(){
//noprotect
traffic.className = 'stop'
await idle(1000)
traffic.className = 'pass'
await idle(1500)
traffic.className = 'wait'
await idle(500)
123456789101112131415
function idle(time){&&return new Promise(resolve=&setTimeout(resolve, time))}&(async function(){&&//noprotect&&do {&&&&traffic.className = 'stop'&&&&await idle(1000)&&&&traffic.className = 'pass'&&&&await idle(1500)&&&&traffic.className = 'wait'&&&&await idle(500)&&}while(1)})()
上面的代码中,我们利用异步的 setTimeout 实现了一个 idle 的异步方法,返回 promise。许多异步处理过程都能让它们返回 promise,从而产生更简单直观的代码。
网页中的 JavaScript 还有一个问题,就是我们要响应很多异步事件,表示用户操作的异步事件其实不太好改写成 promise,事件代表控制,它和数据与流程往往是两个层面的事情,所以许多现代框架和库通过绑定机制把这一块封装起来,让开发者能够聚焦于操作数据和状态,从而避免增加系统的复杂度。
比如上面那个“交通灯”,这样写已经是很简单,但是如果我们要增加几个“开关”,表示“暂停/继续“和”开启/关闭”,要怎么做呢?如果我们还想要增加开关,人工控制和切换灯的转换,又该怎么实现呢?
有同学想到这里,可能觉得,哎呀这太麻烦了,用 async/await 搞不定,还是用之前传统的方式去实现吧。
其实即使用“传统”的思路,要实现这样的异步状态控制也还是挺麻烦的,但是我们的 PM 其实也经常会有这样麻烦的需求。
我们试着来实现一下:
function defer(){
let deferred = {};
deferred.promise = new Promise((resolve, reject) =& {
deferred.resolve = resolve
deferred.reject = reject
return deferred
class Idle {
wait(time){
this.deferred = new defer()
this.timer = setTimeout(()=&{
this.deferred.resolve({canceled: false})
return this.deferred.promise
clearTimeout(this.timer)
this.deferred.resolve({canceled: true})
const idleCtrl = new Idle()
async function turnOnTraffic(){
//noprotect
traffic.className = 'stop'
state = await idleCtrl.wait(1000)
if(state.canceled) break
traffic.className = 'pass'
state = await idleCtrl.wait(1500)
if(state.canceled) break
traffic.className = 'wait'
state = await idleCtrl.wait(500)
if(state.canceled) break
traffic.className = ''
turnOnTraffic()
onoffButton.onclick = function(){
if(traffic.className === ''){
turnOnTraffic()
onoffButton.innerHTML = '关闭'
onoffButton.innerHTML = '开启'
idleCtrl.cancel()
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
function defer(){&&let deferred = {}; &&deferred.promise = new Promise((resolve, reject) =& {&&&&deferred.resolve = resolve&&&&deferred.reject = reject&&})&&return deferred}&class Idle {&&wait(time){&&&&this.deferred = new defer()&&&&this.timer = setTimeout(()=&{&&&&&&this.deferred.resolve({canceled: false})&&&&}, time)&&&&&return this.deferred.promise&&}&&cancel(){&&&&clearTimeout(this.timer)&&&&this.deferred.resolve({canceled: true})&&}}&const idleCtrl = new Idle()&async function turnOnTraffic(){&&let state;&&//noprotect&&do {&&&&traffic.className = 'stop'&&&&state = await idleCtrl.wait(1000)&&&&if(state.canceled) break&&&&traffic.className = 'pass'&&&&state = await idleCtrl.wait(1500)&&&&if(state.canceled) break&&&&traffic.className = 'wait'&&&&state = await idleCtrl.wait(500)&&&&if(state.canceled) break&&}while(1)&&traffic.className = ''}&turnOnTraffic()&onoffButton.onclick = function(){&&if(traffic.className === ''){&&&&turnOnTraffic()&&&&onoffButton.innerHTML = '关闭'&&} else {&&&&onoffButton.innerHTML = '开启'&&&&idleCtrl.cancel()&&}}
上面这么做实现了控制交通灯的开启关闭。但是实际上这样的代码让 onoffButton、 idelCtrl 和 traffic 各种耦合,有点惨不忍睹……
这还只是最简单的“开启/关闭”,“暂停/继续”要比这个更复杂,还有用户自己控制灯的切换呢,想想都头大!
在这种情况下,因为我们把控制和状态混合在一起,所以程序逻辑不可避免地复杂了。这种复杂度与 callback 和 async/await 无关。async/await 只能改变程序的结构,并不能改变内在逻辑的复杂性。
那么我们该怎么做呢?这里我们就要换一种思路,让信号(Signal)登场了!看下面的例子:
class Idle extends Signal {
async wait(time){
this.state = 'wait'
const timer = setTimeout(() =& {
this.state = 'timeout'
await this.while('wait')
clearTimeout(timer)
this.state = 'cancel'
class TrafficSignal extends Signal {
constructor(id){
super('off')
this.container = document.getElementById(id)
this.idle = new Idle()
get lightStat(){
return this.state
async pushStat(val, dur = 0){
this.container.className = val
this.state = val
await this.idle.wait(dur)
get canceled(){
return this.idle.state === 'cancel'
this.pushStat('off')
this.idle.cancel()
const trafficSignal = new TrafficSignal('traffic')
async function turnOnTraffic(){
//noprotect
await trafficSignal.pushStat('stop', 1000)
if(trafficSignal.canceled) break
await trafficSignal.pushStat('pass', 1500)
if(trafficSignal.canceled) break
await trafficSignal.pushStat('wait', 500)
if(trafficSignal.canceled) break
trafficSignal.lightStat = 'off'
turnOnTraffic()
onoffButton.onclick = function(){
if(trafficSignal.lightStat === 'off'){
turnOnTraffic()
onoffButton.innerHTML = '关闭'
onoffButton.innerHTML = '开启'
trafficSignal.cancel()
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
class Idle extends Signal {&&async wait(time){&&&&this.state = 'wait'&&&&const timer = setTimeout(() =& {&&&&&&this.state = 'timeout'&&&&}, time)&&&&await this.while('wait')&&&&clearTimeout(timer)&&}&&cancel(){&&&&this.state = 'cancel'&&}}&class TrafficSignal extends Signal {&&constructor(id){&&&&super('off')&&&&this.container = document.getElementById(id)&&&&this.idle = new Idle()&&}&&get lightStat(){&&&&return this.state&&}&&async pushStat(val, dur = 0){&&&&this.container.className = val&&&&this.state = val&&&&await this.idle.wait(dur)&&}&&get canceled(){&&&&return this.idle.state === 'cancel'&&}&&cancel(){&&&&this.pushStat('off')&&&&this.idle.cancel()&&}}&const trafficSignal = new TrafficSignal('traffic')&async function turnOnTraffic(){&&//noprotect&&do {&&&&await trafficSignal.pushStat('stop', 1000)&&&&if(trafficSignal.canceled) break&&&&await trafficSignal.pushStat('pass', 1500)&&&&if(trafficSignal.canceled) break&&&&await trafficSignal.pushStat('wait', 500)&&&&if(trafficSignal.canceled) break&&}while(1)&&&trafficSignal.lightStat = 'off'}&&turnOnTraffic()&onoffButton.onclick = function(){&&if(trafficSignal.lightStat === 'off'){&&&&turnOnTraffic()&&&&onoffButton.innerHTML = '关闭'&&} else {&&&&onoffButton.innerHTML = '开启'&&&&trafficSignal.cancel()&&}}
我们对代码进行一些修改,封装一个 TrafficSignal,让 onoffButton 只控制 traficSignal 的状态。这里我们用一个简单的 ,它可以实现状态和控制流的分离,例如:
const signal = new Signal('default')
;(async () =& {
await signal.while('default')
console.log('leave default state')
;(async () =& {
await signal.until('state1')
console.log('to state1')
;(async () =& {
await signal.until('state2')
console.log('to state2')
;(async () =& {
await signal.until('state3')
console.log('to state3')
setTimeout(() =& {
signal.state = 'state0'
setTimeout(() =& {
signal.state = 'state1'
setTimeout(() =& {
signal.state = 'state2'
setTimeout(() =& {
signal.state = 'state3'
12345678910111213141516171819202122232425262728293031323334353637
const signal = new Signal('default')&;(async () =& {&&&&await signal.while('default')&&&&console.log('leave default state')})()&;(async () =& {&&&&await signal.until('state1')&&&&console.log('to state1')})()&;(async () =& {&&&&await signal.until('state2')&&&&console.log('to state2')})()&;(async () =& {&&&&await signal.until('state3')&&&&console.log('to state3')})()&setTimeout(() =& {&&&&signal.state = 'state0'}, 1000)&setTimeout(() =& {&&&&signal.state = 'state1'}, 2000)&setTimeout(() =& {&&&&signal.state = 'state2'}, 3000)&setTimeout(() =& {&&&&signal.state = 'state3'}, 4000)
有同学说,这样写代码也不简单啊,代码量比上面写法还要多。的确这样写代码量是比较多的,但是它结构清晰,耦合度低,可以很容易扩展,比如:
class Idle extends Signal {
async wait(time){
this.state = 'wait'
const timer = setTimeout(() =& {
this.state = 'timeout'
await this.while('wait')
clearTimeout(timer)
this.state = 'cancel'
class TrafficSignal extends Signal {
constructor(id){
super('off')
this.container = document.getElementById(id)
this.idle = new Idle()
get lightStat(){
return this.state
async pushStat(val, dur = 0){
this.container.className = val
this.state = val
if(dur) await this.idle.wait(dur)
get canceled(){
return this.idle.state === 'cancel'
this.idle.cancel()
this.pushStat('off')
const trafficSignal = new TrafficSignal('traffic')
async function turnOnTraffic(){
//noprotect
await trafficSignal.pushStat('stop', 1000)
if(trafficSignal.canceled) break
await trafficSignal.pushStat('pass', 1500)
if(trafficSignal.canceled) break
await trafficSignal.pushStat('wait', 500)
if(trafficSignal.canceled) break
trafficSignal.lightStat = 'off'
turnOnTraffic()
onoffButton.onclick = function(){
if(trafficSignal.lightStat === 'off'){
turnOnTraffic()
onoffButton.innerHTML = '关闭'
onoffButton.innerHTML = '开启'
trafficSignal.cancel()
turnRed.onclick = function(){
trafficSignal.cancel()
trafficSignal.pushStat('stop')
turnGreen.onclick = function(){
trafficSignal.cancel()
trafficSignal.pushStat('pass')
turnYellow.onclick = function(){
trafficSignal.cancel()
trafficSignal.pushStat('wait')
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
class Idle extends Signal {&&async wait(time){&&&&this.state = 'wait'&&&&const timer = setTimeout(() =& {&&&&&&this.state = 'timeout'&&&&}, time)&&&&await this.while('wait')&&&&clearTimeout(timer)&&}&&cancel(){&&&&this.state = 'cancel'&&}}&class TrafficSignal extends Signal {&&constructor(id){&&&&super('off')&&&&this.container = document.getElementById(id)&&&&this.idle = new Idle()&&}&&get lightStat(){&&&&return this.state&&}&&async pushStat(val, dur = 0){&&&&this.container.className = val&&&&this.state = val&&&&if(dur) await this.idle.wait(dur)&&}&&get canceled(){&&&&return this.idle.state === 'cancel'&&}&&cancel(){&&&&this.idle.cancel()&&&&this.pushStat('off')&&}}&const trafficSignal = new TrafficSignal('traffic')&async function turnOnTraffic(){&&//noprotect&&do {&&&&await trafficSignal.pushStat('stop', 1000)&&&&if(trafficSignal.canceled) break&&&&await trafficSignal.pushStat('pass', 1500)&&&&if(trafficSignal.canceled) break&&&&await trafficSignal.pushStat('wait', 500)&&&&if(trafficSignal.canceled) break&&}while(1)&&&trafficSignal.lightStat = 'off'}&&turnOnTraffic()&onoffButton.onclick = function(){&&if(trafficSignal.lightStat === 'off'){&&&&turnOnTraffic()&&&&onoffButton.innerHTML = '关闭'&&} else {&&&&onoffButton.innerHTML = '开启'&&&&trafficSignal.cancel()&&}}&turnRed.onclick = function(){&&trafficSignal.cancel()&&trafficSignal.pushStat('stop')}&turnGreen.onclick = function(){&&trafficSignal.cancel()&&trafficSignal.pushStat('pass')}&turnYellow.onclick = function(){&&trafficSignal.cancel()&&trafficSignal.pushStat('wait')}
Signal 非常适合于事件控制的场合,再举一个更简单的例子,如果我们用一个按钮控制简单的动画的暂停和执行,可以这样写:
let traffic = new Signal('stop')
requestAnimationFrame(async function update(t){
await traffic.until('pass')
block.style.left = parseInt(block.style.left || 50) + 1 + 'px'
requestAnimationFrame(update)
button.onclick = e =& {
traffic.state = button.className = button.className === 'stop' ? 'pass' : 'stop'
1234567891011
let traffic = new Signal('stop')&requestAnimationFrame(async function update(t){&&await traffic.until('pass')&&block.style.left = parseInt(block.style.left || 50) + 1 + 'px'&&requestAnimationFrame(update)})&button.onclick = e =& {&&traffic.state = button.className = button.className === 'stop' ? 'pass' : 'stop'}
我们可以用 Signal 来控制异步流程,它最大的作用是将状态和控制分离,我们只需要改变 Signal 的状态,就可以控制异步流程,Signal 支持 until 和 while 谓词,来控制状态的改变。
上进一步了解关于 Signal 的详细信息。
可能感兴趣的话题
关于伯乐前端
伯乐前端分享Web前端开发,包括JavaScript,CSS和HTML5开发技术,前端相关的行业动态。
新浪微博:
推荐微信号
(加好友请注明来意)
– 好的话题、有启发的回复、值得信赖的圈子
– 分享和发现有价值的内容与观点
– 为IT单身男女服务的征婚传播平台
– 优秀的工具资源导航
– 翻译传播优秀的外文文章
– 国内外的精选文章
– UI,网页,交互和用户体验
– 专注iOS技术分享
– 专注Android技术分享
– JavaScript, HTML5, CSS
– 专注Java技术分享
– 专注Python技术分享
& 2017 伯乐在线

我要回帖

更多关于 angular promise 异步 的文章

 

随机推荐