一、什么是 Promise

1.1 Promise 的前世今生

Promise 最早出现在 1988 年,由 Barbara LiskovLiuba Shrira 首创(论文:Promises: Linguistic Support for Efficient Asynchronous Procedure Calls in Distributed Systems)。并且在语言 MultiLispConcurrent Prolog 中已经有了类似的实现。

JavaScript 中,Promise 的流行是得益于 jQuery 的方法 jQuery.Deferred(),其他也有一些更精简独立的 Promise 库,例如:QWhenBluebird

# Q / 2010
import Q from 'q' function wantOdd () {
const defer = Q.defer()
const num = Math.floor(Math.random() * 10)
if (num % 2) {
defer.resolve(num)
} else {
defer.reject(num)
}
return defer.promise
} wantOdd()
.then(num => {
log(`Success: ${num} is odd.`) // Success: 7 is odd.
})
.catch(num => {
log(`Fail: ${num} is not odd.`)
})

由于 jQuery 并没有严格按照规范来制定接口,促使了官方对 Promise 的实现标准进行了一系列重要的澄清,该实现规范被命名为 Promise/A+。后来 ES6(也叫 ES2015,2015 年 6 月正式发布)也在 Promise/A+ 的标准上官方实现了一个 Promise 接口。

new Promise( function(resolve, reject) {...} /* 执行器 */  );

想要实现一个 Promise,必须要遵循如下规则:

  1. Promise 是一个提供符合标准then() 方法的对象。
  2. 初始状态是 pending,能够转换成 fulfilledrejected 状态。
  3. 一旦 fulfilledrejected 状态确定,再也不能转换成其他状态。
  4. 一旦状态确定,必须要返回一个值,并且这个值是不可修改的。

ECMAScript's Promise global is just one of many Promises/A+ implementations.

主流语言对于 Promise 的实现:Golang/go-promisePython/promiseC#/Real-Serious-Games/c-sharp-promisePHP/Guzzle PromisesJava/IOUObjective-C/PromiseKitSwift/FutureLibPerl/stevan/promises-perl

旨在解决的问题

由于 JavaScript 是单线程事件驱动的编程语言,通过回调函数管理多个任务。在快速迭代的开发中,因为回调函数的滥用,很容易产生被人所诟病的回调地狱问题。Promise 的异步编程解决方案比回调函数更加合理,可读性更强。

传说中比较夸张的回调:

现实业务中依赖关系比较强的回调:

# 回调函数
function renderPage () {
const secret = genSecret()
// 获取用户令牌
getUserToken({
secret,
success: token => {
// 获取游戏列表
getGameList({
token,
success: data => {
// 渲染游戏列表
render({
list: data.list,
success: () => {
// 埋点数据上报
report()
},
fail: err => {
console.error(err)
}
})
},
fail: err => {
console.error(err)
}
})
},
fail: err => {
console.error(err)
}
})
}

使用 Promise 梳理流程后:

# Promise
function renderPage () {
const secret = genSecret()
// 获取用户令牌
getUserToken(token)
.then(token => {
// 获取游戏列表
return getGameList(token)
})
.then(data => {
// 渲染游戏列表
return render(data.list)
})
.then(() => {
// 埋点数据上报
report()
})
.catch(err => {
console.error(err)
})
}

1.2 实现一个超简易版的 Promise

Promise 的运转实际上是一个观察者模式,then() 中的匿名函数充当观察者,Promise 实例充当被观察者。

const p = new Promise(resolve => setTimeout(resolve.bind(null, 'from promise'), 3000))

p.then(console.log.bind(null, 1))
p.then(console.log.bind(null, 2))
p.then(console.log.bind(null, 3))
p.then(console.log.bind(null, 4))
p.then(console.log.bind(null, 5))
// 3 秒后
// 1 2 3 4 5 from promise

# 实现
const defer = () => {
let pending = [] // 充当状态并收集观察者
let value = undefined
return {
resolve: (_value) => { // FulFilled!
value = _value
if (pending) {
pending.forEach(callback => callback(value))
pending = undefined
}
},
then: (callback) => {
if (pending) {
pending.push(callback)
} else {
callback(value)
}
}
}
} # 模拟
const mockPromise = () => {
let p = defer()
setTimeout(() => {
p.resolve('success!')
}, 3000)
return p
} mockPromise().then(res => {
console.log(res)
}) console.log('script end')
// script end
// 3 秒后
// success!

二、Promise 怎么用

2.1 使用 Promise 异步编程

Promise 出现之前往往使用回调函数管理一些异步程序的状态。

# 常见的异步 Ajax 请求格式
ajax(url, successCallback, errorCallback)

Promise 出现后使用 then() 接收事件的状态,且只会接收一次。

案例:插件初始化。

使用回调函数:

# 插件代码
let ppInitStatus = false
let ppInitCallback = null
PP.init = callback => {
if (ppInitStatus) {
callback && callback(/* 数据 */)
} else {
ppInitCallback = callback
}
}
// ...
// ...
// 经历了一系列同步异步程序后初始化完成
ppInitCallback && ppInitCallback(/* 数据 */)
ppInitStatus = true # 第三方调用
PP.init(callback)

使用 Promise:

# 插件代码
let initOk = null
const ppInitStatus = new Promise(resolve => initOk = resolve)
PP.init = callback => {
ppInitStatus.then(callback).catch(console.error)
}
// ...
// ...
// 经历了一系列同步异步程序后初始化完成
initOk(/* 数据 */) # 第三方调用
PP.init(callback)

相对于使用回调函数,逻辑更清晰,什么时候初始化完成和触发回调一目了然,不再需要重复判断状态和回调函数。当然更好的做法是只给第三方输出状态数据,至于如何使用由第三方决定。

# 插件代码
let initOk = null
PP.init = new Promise(resolve => initOk = resolve)
// ...
// ...
// 经历了一系列同步异步程序后初始化完成
initOk(/* 数据 */) # 第三方调用
PP.init.then(callback).catch(console.error)

2.2 链式调用

then() 必然返回一个 Promise 对象,Promise 对象又拥有一个 then() 方法,这正是 Promise 能够链式调用的原因。

const p = new Promise(r => r(1))
.then(res => {
console.log(res) // 1
return Promise.resolve(2)
.then(res => res + 10) // === new Promise(r => r(1))
.then(res => res + 10) // 由此可见,每次返回的是实例后面跟的最后一个 then
})
.then(res => {
console.log(res) // 22
return 3 // === Promise.resolve(3)
})
.then(res => {
console.log(res) // 3
})
.then(res => {
console.log(res) // undefined
return '最强王者'
}) p.then(console.log.bind(null, '是谁活到了最后:')) // 是谁活到了最后: 最强王者

由于返回一个 Promise 结构体永远返回的是链式调用的最后一个 then(),所以在处理封装好的 Promise 接口时没必要在外面再包一层 Promise

# 包一层 Promise
function api () {
return new Promise((resolve, reject) => {
axios.get(/* 链接 */).then(data => {
// ...
// 经历了一系列数据处理
resolve(data.xxx)
})
})
} # 更好的做法:利用链式调用
function api () {
return axios.get(/* 链接 */).then(data => {
// ...
// 经历了一系列数据处理
return data.xxx
})
}

2.3 管理多个 Promise 实例

Promise.all() / Promise.race() 可以将多个 Promise 实例包装成一个 Promise 实例,在处理并行的、没有依赖关系的请求时,能够节约大量的时间。

function wait (ms) {
return new Promise(resolve => setTimeout(resolve.bind(null, ms), ms))
} # Promise.all
Promise.all([wait(2000), wait(4000), wait(3000)])
.then(console.log)
// 4 秒后 [ 2000, 4000, 3000 ] # Promise.race
Promise.race([wait(2000), wait(4000), wait(3000)])
.then(console.log)
// 2 秒后 2000

2.4 Promiseasync / await

async / await 实际上只是建立在 Promise 之上的语法糖,让异步代码看上去更像同步代码,所以 async / await 在 JavaScript 线程中是非阻塞的,但在当前函数作用域内具备阻塞性质。

let ok = null
async function foo () {
console.log(1)
console.log(await new Promise(resolve => ok = resolve))
console.log(3)
}
foo() // 1
ok(2) // 2 3

使用 async / await 的优势:

  1. 简洁干净

    写更少的代码,不需要特地创建一个匿名函数,放入 then() 方法中等待一个响应。

    # Promise
    function getUserInfo () {
    return getData().then(
    data => {
    return data
    }
    )
    } # async / await
    async function getUserInfo () {
    return await getData()
    }
  2. 条件语句

    当一个异步返回值是另一段逻辑的判断条件,链式调用将随着层级的叠加变得更加复杂,让人很容易在代码中迷失自我。使用 async / await 将使代码可读性变得更好。

    # Promise
    function getGameInfo () {
    getUserAbValue().then(
    abValue => {
    if (abValue === 1) {
    return getAInfo().then(
    data => {
    // ...
    }
    )
    } else {
    return getBInfo().then(
    data => {
    // ...
    }
    )
    }
    }
    )
    } # async / await
    async function getGameInfo () {
    const abValue = await getUserAbValue()
    if (abValue === 1) {
    const data = await getAInfo()
    // ...
    } else {
    // ...
    }
    }
  3. 中间值

    异步函数常常存在一些异步返回值,作用仅限于成为下一段逻辑的入场券,如果经历层层链式调用,很容易成为另一种形式的“回调地狱”。

    # Promise
    function getGameInfo () {
    getToken().then(
    token => {
    getLevel(token).then(
    level => {
    getInfo(token, level).then(
    data => {
    // ...
    }
    )
    }
    )
    }
    )
    } # async / await
    async function getGameInfo() {
    const token = await getToken()
    const level = await getLevel(token)
    const data = await getInfo(token, level)
    // ...
    }
  4. 靠谱的 await

    await 'qtt' 等于 await Promise.resolve('qtt')await 会把任何不是 Promise 的值包装成 Promise,看起来貌似没有什么用,但是在处理第三方接口的时候可以 “Hold” 住同步和异步返回值,否则对一个非 Promise 返回值使用 then() 链式调用则会报错。

使用 async / await 的缺点:

  1. async 永远返回 Promise 对象,不够灵活,很多时候我只想单纯返回一个基本类型值。

  2. await 阻塞 async 函数中的代码执行,在上下文关联性不强的代码中略显累赘。

    # async / await
    async function initGame () {
    render(await getGame()) // 等待获取游戏执行完毕再去获取用户信息
    report(await getUserInfo())
    } # Promise
    function initGame () {
    getGame()
    .then(render)
    .catch(console.error)
    getUserInfo() // 获取用户信息和获取游戏同步进行
    .then(report)
    .catch(console.error)
    }

2.5 错误处理

  1. 链式调用中尽量结尾跟 catch 捕获错误,而不是第二个匿名函数。因为标准里注明了若 then() 方法里面的参数不是函数则什么都不错,所以 catch(rejectionFn) 其实就是 then(null, rejectionFn) 的别名。

    anAsyncFn().then(
    resolveSuccess,
    rejectError
    )

    在以上代码中,anAsyncFn() 抛出来的错误 rejectError 会正常接住,但是 resolveSuccess 抛出来的错误将无法捕获,所以更好的做法是永远使用 catch

    anAsyncFn()
    .then(resolveSuccess)
    .catch(rejectError)

    倘若讲究一点,也可以通过 resolveSuccess 来捕获 anAsyncFn() 的错误,catch 捕获 resolveSuccess 的错误。

    anAsyncFn()
    .then(
    resolveSuccess,
    rejectError
    )
    .catch(handleError)
  2. 通过全局属性监听未被处理的 Promise 错误。

    浏览器环境(window)的拒绝状态监听事件:

    • unhandledrejection 当 Promise 被拒绝,并且没有提供拒绝处理程序时,触发该事件。
    • rejectionhandled 当 Promise 被拒绝时,若拒绝处理程序被调用,触发该事件。
    // 初始化列表
    const unhandledRejections = new Map()
    // 监听未处理拒绝状态
    window.addEventListener('unhandledrejection', e => {
    unhandledRejections.set(e.promise, e.reason)
    })
    // 监听已处理拒绝状态
    window.addEventListener('rejectionhandled', e => {
    unhandledRejections.delete(e.promise)
    })
    // 循环处理拒绝状态
    setInterval(() => {
    unhandledRejections.forEach((reason, promise) => {
    console.log('handle: ', reason.message)
    promise.catch(e => {
    console.log(`I catch u!`, e.message)
    })
    })
    unhandledRejections.clear()
    }, 5000)

注意:Promise.reject()new Promise((resolve, reject) => reject()) 这种方式不能直接触发 unhandledrejection 事件,必须是满足已经进行了 then() 链式调用的 Promise 对象才行。

2.6 取消一个 Promise

当执行一个超级久的异步请求时,若超过了能够忍受的最大时长,往往需要取消此次请求,但是 Promise 并没有类似于 cancel() 的取消方法,想结束一个 Promise 只能通过 resolvereject 来改变其状态,社区已经有了满足此需求的开源库 Speculation

或者利用 Promise.race() 的机制来同时注入一个会超时的异步函数,但是 Promise.race() 结束后主程序其实还在 pending 中,占用的资源并没有释放。

Promise.race([anAsyncFn(), timeout(5000)])

2.7 迭代器的应用

若想按顺序执行一堆异步程序,可使用 reduce。每次遍历返回一个 Promise 对象,在下一轮 await 住从而依次执行。

function wasteTime (ms) {
return new Promise(resolve => setTimeout(() => {
resolve(ms)
console.log('waste', ms)
}, ms))
} // 依次浪费 3 4 5 3 秒 === 15 秒
const arr = [3000, 4000, 5000, 3000]
arr.reduce(async (last, curr) => {
await last
return wasteTime(curr)
}, undefined)

三、总结

  1. 每当要使用异步代码时,请考虑使用 Promise
  2. Promise 中所有方法的返回类型都是 Promise
  3. Promise 中的状态改变是一次性的,建议在 reject() 方法中传递 Error 对象。
  4. 确保为所有的 Promise 添加 then()catch() 方法。
  5. 使用 Promise.all() 行运行多个 Promise
  6. 倘若想在 then()catch() 后都做点什么,可使用 finally()
  7. 可以将多个 then() 挂载在同一个 Promise 上。
  8. async (异步)函数返回一个 Promise,所有返回 Promise 的函数也可以被视作一个异步函数。
  9. await 用于调用异步函数,直到其状态改变(fulfilled or rejected)。
  10. 使用 async / await 时要考虑上下文的依赖性,避免造成不必要的阻塞。

更多文章访问我的博客

讲讲 Promise的更多相关文章

  1. promise源码解析

    前言 大部分同学对promise,可能还停留在会使用es6的promise,还没有深入学习.我们都知道promise内部通过reslove.reject来判断执行哪个函数,原型上面的then同样的,也 ...

  2. 模拟实现 Promise(小白版)

    模拟实现 Promise(小白版) 本篇来讲讲如何模拟实现一个 Promise 的基本功能,网上这类文章已经很多,本篇笔墨会比较多,因为想用自己的理解,用白话文来讲讲 Promise 的基本规范,参考 ...

  3. async 函数-----------------解决异步操作隧道的亮光

    之前也学过,只是没有学好,公司现在用的都是async函数 , 所以决定把它弄懂.最近看了看阮一峰的博客,做下记录. 异步I/O不就是读取一个文件吗,干嘛要搞得这么复杂?异步编程的最高境界,就是根本不用 ...

  4. [书籍翻译] 《JavaScript并发编程》第三章 使用Promises实现同步

    本文是我翻译<JavaScript Concurrency>书籍的第三章 使用Promises实现同步,该书主要以Promises.Generator.Web workers等技术来讲解J ...

  5. Javascript - Promise学习笔记

    最近工作轻松了点,想起了以前总是看到的一个单词promise,于是耐心下来学习了一下.   一:Promise是什么?为什么会有这个东西? 首先说明,Promise是为了解决javascript异步编 ...

  6. 大白话讲解Promise(三)搞懂jquery中的Promise

    前两篇我们讲了ES6中的Promise以及Promise/A+规范,在Promise的知识体系中,jquery当然是必不可少的一环,所以本篇就来讲讲jquery中的Promise,也就是我们所知道的D ...

  7. JS - Promise使用详解--摘抄笔记

    第一部分: JS - Promise使用详解1(基本概念.使用优点) 一.promises相关概念 promises 的概念是由 CommonJS 小组的成员在 Promises/A 规范中提出来的. ...

  8. JS引擎是如何工作的?从调用堆栈到Promise

    摘要: 理解 JS 引擎运行原理. 作者:前端小智 原文:JS引擎:它们是如何工作的?从调用堆栈到Promise,需要知道的所有内容 Fundebug经授权转载,版权归原作者所有. 为了保证可读性,本 ...

  9. Promise、Generator,Async/await

    我们知道JavaScript是单线程语言,如果没有异步编程非得卡死. 以前,异步编程的方法有下面四种 回调函数 事件监听 发布/订阅 Promise对象 现在据说异步编程终极解决方案是——async/ ...

随机推荐

  1. Java实现 LeetCode 173 二叉搜索树迭代器

    173. 二叉搜索树迭代器 实现一个二叉搜索树迭代器.你将使用二叉搜索树的根节点初始化迭代器. 调用 next() 将返回二叉搜索树中的下一个最小的数. 示例: BSTIterator iterato ...

  2. java代码(13) ---Predicate详解

    Predicate详解 遇到Predicate是自己在自定义Mybatis拦截器的时候,在拦截器中我们是通过反射机制获取对象的所有属性,在查看这些属性上是否有我们自定义的UUID注解 如果有该注解,那 ...

  3. IOS App如何调用python后端服务

    本篇文章旨在通过一个小的Demo形式来了解ios app是如何调用python后端服务的,以便我们在今后的工作中可以清晰的明白ios app与后端服务之间是如何实现交互的,今天的示例是拿登录功能做一个 ...

  4. CentOS 6.X 双网卡绑定配置

    相关环境 主机:Dell PowerEdge R720服务器(背板有4个GE网口) 操作系统:CentOS(RHEL)6.X 网线连接 使用6类网线 将Dell R720 GE网口 0,与交换机A相连 ...

  5. zabbix 监控cpu 温度

    Zabbix监控Linux主机CPU温度   一.客户端 1.客户Linux主机安装Im_sensors [root@localhost ~]# yum install -y lm-sensors-y ...

  6. AntD框架的upload组件上传图片时使用customRequest方法自定义上传行为

    本次做后台管理系统,采用的是 AntD 框架.涉及到图片的上传,用的是AntD的 upload 组件. 我在上一篇文章<AntD框架的upload组件上传图片时使用customRequest方法 ...

  7. ThreadLocal的使用场景分析

    目录 一.ThreadLocal介绍 二.使用场景1——数据库事务问题 2.1 问题背景 2.2 方案1-修改接口传参 2.3 方案2-使用ThreadLocal 三.使用场景2——日志追踪问题 四. ...

  8. 解决mysql不是内部或外部命令(win10)

    1.原因:cmd当前所在路径为c盘下的system32,由于mysql安装位置不在该目录下,所以会报错. 2.解决方法:配置环境变量 step1:右击此电脑->属性 step2:选择高级系统设置 ...

  9. 在VisualStudio中为GUI程序添加console

    1.使用WINDOWS提供的一系列ReadConsole,WriteConsole等API来完成这个功能,具体参见MSDN HANDLE hOut = GetStdHandle(STD_OUTPUT_ ...

  10. 【JMeter_07】JMeter逻辑控制器__循环控制器<Loop Controller>

    循环控制器<Loop Controller> 业务逻辑: 根据所填写的循环次数,对当前节点下的取样器进行响应次数的循环请求,这里可以填写变量.整数.小数.字母.负数.各种符号等: 当填写整 ...