剖析 Promise 之基础篇
来源:互联网 发布:淘宝客服简历工作内容 编辑:程序博客网 时间:2024/06/05 06:47
剖析 Promise 之基础
随着浏览器端异步操作复杂程度的日益增加,以及以 Evented I/O 为核心思想的 NodeJS 的持续火爆,Promise、Async 等异步操作封装由于解决了异步编程上面临的诸多挑战,得到了越来越广泛的应用。本文旨在剖析 Promise 的内部机制,从实现原理层面深入探讨,从而达到“知其然且知其所以然”,在使用 Promise 上更加熟练自如。如果你还不太了解 Promise,推荐阅读下 promisejs.org 的介绍。
是什么
Promise 是一种对异步操作的封装,可以通过独立的接口添加在异步操作执行成功、失败时执行的方法。主流的规范是 Promises/A+。
Promise 较通常的回调、事件/消息,在处理异步操作时具有显著的优势。其中最为重要的一点是:Promise 在语义上代表了异步操作的主体。这种准确、清晰的定位极大推动了它在编程中的普及,因为具有单一职责,而且将份内事做到极致的事物总是具有病毒式的传染力。分离输入输出参数、错误冒泡、串行/并行控制流等特性都成为 Promise 横扫异步操作编程领域的重要砝码,以至于 ES6 都将其收录,并已在 Chrome、Firefox 等现代浏览器中实现。
内部机制
自从看到 Promise 的 API,我对它的实现就充满了深深的好奇,一直有心窥其究竟。接下来,将首先从最简单的基础实现开始,由浅入深的逐步探索,剖析每一个 feature 后面的故事。
为了让语言上更加准确和简练,本文做如下约定:
- Promise:代表由 Promises/A+ 规范所定义的异步操作封装方式;
- promise:代表一个 Promise 实例。
基础实现
为了增加代入感,本文从最为基础的一个应用实例开始探索:通过异步请求获取用户id,然后做一些处理。在平时大家都是习惯用回调或者事件来处理,下面我们看下 Promise 的处理方式:
// 例1function getUserId() { return new Promise(function (resolve) { // 异步请求 Y.io('/userid', { on: { success: function (id, res) { resolve(JSON.parse(res).id); } } }); });}getUserId().then(function (id) { // do sth with id});
JS Bin
getUserId
方法返回一个 promise,可以通过它的 then
方法注册在 promise 异步操作成功时执行的回调。自然、表意的 API,用起来十分顺手。
满足这样一种使用场景的 Promise 是如何构建的呢?其实并不复杂,下面给出最基础的实现:
function Promise(fn) { var value = null, deferreds = []; this.then = function (onFulfilled) { deferreds.push(onFulfilled); }; function resolve(value) { deferreds.forEach(function (deferred) { deferred(value); }); } fn(resolve);}
代码很短,逻辑也非常清晰:
- 调用
then
方法,将想要在 Promise 异步操作成功时执行的回调放入deferreds
队列; - 创建 Promise 实例时传入函数被赋予一个函数类型的参数,即
resolve
,用以在合适的时机触发异步操作成功。真正执行的操作是将deferreds
队列中的回调一一执行; resolve
接收一个参数,即异步操作返回的结果,方便回调使用。
有时需要注册多个回调,如果能够支持 jQuery 那样的链式操作就好了!事实上,这很容易:
this.then = function (onFulfilled) { deferreds.push(onFulfilled); return this;};
这个小改进带来的好处非常明显,当真是一个大收益的小创新呢:
// 例2getUserId().then(function (id) { // do sth with id}).then(function (id) { // do sth else with id});
JS Bin
延时
如果 promise 是同步代码,resolve
会先于 then
执行,这时 deferreds
队列还空无一物,更严重的是,后续注册的回调再也不会被执行了:
// 例3function getUserId() { return new Promise(function (resolve) { resolve(9876); });}getUserId().then(function (id) { // do sth with id});
JS Bin
此外,Promises/A+ 规范明确要求回调需要通过异步方式执行,用以保证一致可靠的执行顺序。为解决这两个问题,可以通过 setTimeout
将 resolve
中执行回调的逻辑放置到 JS 任务队列末尾:
function resolve(value) { setTimeout(function () { deferreds.forEach(function (deferred) { deferred(value); }); }, 0);}
引入状态
Hmm,好像存在一点问题:如果 Promise 异步操作已经成功,之后调用 then
注册的回调再也不会执行了,而这是不符合我们预期的。
解决这个问题,需要引入规范中所说的 States,即每个 Promise 存在三个互斥状态:pending、fulfilled、rejected,它们之间的关系是:
经过改进后的代码:
function Promise(fn) { var state = 'pending', value = null, deferreds = []; this.then = function (onFulfilled) { if (state === 'pending') { deferreds.push(onFulfilled); return this; } onFulfilled(value); return this; }; function resolve(newValue) { value = newValue; state = 'fulfilled'; setTimeout(function () { deferreds.forEach(function (deferred) { deferred(value); }); }, 0); } fn(resolve);}
JS Bin
resolve
执行时,会将状态设置为 fulfilled,在此之后调用 then
添加的新回调,都会立即执行。
似乎少了点什么,哦,是的,没有任何地方将 state 设为 rejected,这个问题稍后会聊,方便聚焦在核心代码上。
串行 Promise
在这一小节,将要探索的是 Promise 的 Killer Feature:串行 Promise,这是最为有趣也最为神秘的一个功能。
串行 Promise 是指在当前 promise 达到 fulfilled 状态后,即开始进行下一个 promise(后邻 promise)。例如获取用户 id 后,再根据用户 id 获取用户手机号等其他信息,这样的场景比比皆是:
// 例4getUserId() .then(getUserMobileById) .then(function (mobile) { // do sth with mobile });function getUserMobileById(id) { return new Promise(function (resolve) { Y.io('/usermobile/' + id, { on: { success: function (i, o) { resolve(JSON.parse(o).mobile); } } }); });}
JS Bin
这个 feature 实现的难点在于:如何衔接当前 promise 和后邻 promise。
首先对 then
方法进行改造:
this.then = function (onFulfilled) { return new Promise(function (resolve) { handle({ onFulfilled: onFulfilled || null, resolve: resolve }); });};function handle(deferred) { if (state === 'pending') { deferreds.push(deferred); return; } var ret = deferred.onFulfilled(value); deferred.resolve(ret);}
then
方法改变很多,这是一段暗藏玄机的代码:
then
方法中,创建了一个新的 Promise 实例,并作为返回值,这类 promise,权且称作 bridge promise。这是串行 Promise 的基础。另外,因为返回类型一致,之前的链式执行仍然被支持;handle
方法是当前 promise 的内部方法。这一点很重要,看不懂的童鞋可以去补充下闭包的知识。then
方法传入的形参onFullfilled
,以及创建新 Promise 实例时传入的resolve
均被压入当前 promise 的deferreds
队列中。所谓“巧妇难为无米之炊”,而这,正是衔接当前 promise 与后邻 promise 的“米”之所在。
新增的 handle
方法,相比改造之前的 then
方法,仅增加了一行代码:
deferred.resolve(ret);
这意味着当前 promise 异步操作成功后执行 handle
方法时,先执行 onFulfilled
方法,然后将其返回值作为实参执行 resolve
方法,而这标志着后邻 promise 异步操作成功,接力工作就这样完成啦!
以例 2 代码为例,串行 Promise 执行流如下:
这就是所谓的串行 Promise?当然不是,这些改造只是为了为最后的冲刺做铺垫,它们在重构底层实现的同时,兼容了本文之前讨论的所有功能。接下来,画龙点睛之笔--最后一个方法 resolve
是这样被改造的:
function resolve(newValue) { if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) { var then = newValue.then; if (typeof then === 'function') { then.call(newValue, resolve); return; } } state = 'fulfilled'; value = newValue; setTimeout(function () { deferreds.forEach(function (deferred) { handle(deferred); }); }, 0);}
啊哈,resolve
方法现在支持传入的参数是一个 Promise 实例了!以例 4 为例,执行步骤如下:
getUserId
生成的 promise (简称getUserId
promise)异步操作成功,执行其内部方法resolve
,传入的参数正是异步操作的结果userid
;- 调用
handle
方法处理deferreds
队列中的回调:getUserMobileById
方法,生成新的 promise(简称getUserMobileById
promise); - 执行之前由
getUserId
promise 的then
方法生成的 bridge promise 的resolve
方法,传入参数为getUserMobileById
promise。这种情况下,会将该resolve
方法传入getUserMobileById
promise 的then
方法中,并直接返回; - 在
getUserMobileById
promise 异步操作成功时,执行其deferreds
中的回调:getUserId
bridge promise 的resolve
方法; - 最后,执行
getUserId
bridge promise 的后邻 promise 的deferreds
中的回调
上述步骤实在有些复杂,主要原因是 bridge promise 的引入。不过正是得益于此,注册一个返回值也是 promise 的回调,从而实现异步操作串行的机制才得以实现。
一图胜千言,下图描述了例 4 的 Promise 执行流:
失败处理
本节处理之前遗留的 rejected 状态问题。在异步操作失败时,标记其状态为 rejected,并执行注册的失败回调:
// 例5function getUserId() { return new Promise(function (resolve, reject) { // 异步请求 Y.io('/userid/1', { on: { success: function (id, res) { var o = JSON.parse(res); if (o.status === 1) { resolve(o.id); } else { // 请求失败,返回错误信息 reject(o.errorMsg); } } } }); });}getUserId().then(function (id) { // do sth with id}, function (error) { console.log(error);});
JS Bin
有了之前处理 fulfilled 状态的经验,支持错误处理变得很容易。毫无疑问的是,这将加倍 code base,在注册回调、处理状态变更上都要加入新的逻辑:
function Promise(fn) { var state = 'pending', value = null, deferreds = []; this.then = function (onFulfilled, onRejected) { return new Promise(function (resolve, reject) { handle({ onFulfilled: onFulfilled || null, onRejected: onRejected || null, resolve: resolve, reject: reject }); }); }; function handle(deferred) { if (state === 'pending') { deferreds.push(deferred); return; } var cb = state === 'fulfilled' ? deferred.onFulfilled : deferred.onRejected, ret; if (cb === null) { cb = state === 'fulfilled' ? deferred.resolve : deferred.reject; cb(value); return; } ret = cb(value); deferred.resolve(ret); } function resolve(newValue) { if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) { var then = newValue.then; if (typeof then === 'function') { then.call(newValue, resolve, reject); return; } } state = 'fulfilled'; value = newValue; finale(); } function reject(reason) { state = 'rejected'; value = reason; finale(); } function finale() { setTimeout(function () { deferreds.forEach(function (deferred) { handle(deferred); }); }, 0); } fn(resolve, reject);}
增加了新的 reject
方法,供异步操作失败时调用,同时抽出了 resolve
和 reject
共用的部分,形成 finale
方法。
错误冒泡是上述代码已经支持,且非常实用的一个特性。在 handle
中发现没有指定异步操作失败的回调时,会直接将 bridge promise 设为 rejected 状态,如此达成执行后续失败回调的效果。这有利于简化串行 Promise 的失败处理成本,因为一组异步操作往往会对应一个实际功能,失败处理方法通常是一致的:
// 例6getUserId() .then(getUserMobileById) .then(function (mobile) { // do sth else with mobile }, function (error) { // getUserId或者getUerMobileById时出现的错误 console.log(error); });
JS Bin
异常处理
如果在执行成功回调、失败回调时代码出错怎么办?对于这类异常,可以使用 try-catch
捕获错误,并将 bridge promise 设为 rejected 状态。handle
方法改造如下:
function handle(deferred) { if (state === 'pending') { deferreds.push(deferred); return; } var cb = state === 'fulfilled' ? deferred.onFulfilled : deferred.onRejected, ret; if (cb === null) { cb = state === 'fulfilled' ? deferred.resolve : deferred.reject; cb(value); return; } try { ret = cb(value); deferred.resolve(ret); } catch (e) { deferred.reject(e); } }
如果在异步操作中,多次执行 resolve
或者 reject
会重复处理后续回调,可以通过内置一个标志位解决。
总结
Promise 作为异步操作的一种 Monad,魔幻一般的 API 让人难以驾驭。本文从简单的基础实现起步,逐步添加内置状态、串行、失败处理/失败冒泡、异常处理等关键特性,最终达到类似由 Forbes Lindesay 所完成的一个简单 Promise 实现的效果。在让我本人更加深刻理解 Promise 魔力之源的同时,希望为各位更加熟练的使用这一实用工具带来一些帮助。
- 剖析 Promise 之基础篇
- Promise 之基础篇
- JavaScript基础之Promise
- ES6基础之详解Promise基本用法
- promise基础
- 剖析SQLServer2005查询通知之基础篇
- Promise浅析(一)——基础篇
- ES6新特性之Promise篇
- es6基础回顾--Promise
- JQuery的Promise详解(一):Promise基础
- JQuery的Promise详解 : Promise基础
- 剖析SQL Server 2005查询通知之基础篇
- 剖析SQL Server 2005查询通知之基础篇
- 剖析SQL Server 2005查询通知之基础篇(1)
- 剖析SQL Server 2005查询通知之基础篇(2)
- 剖析SQL Server 2005查询通知之基础篇(3)
- es6之promise被坑记
- 异步编程之Promise
- [servlet] 02 ServletConfig接口
- PDA-MC3190
- Jedis下的ShardedJedis(分布式)使用方法(二)
- Mac安装maven
- eclipse安装svn服务器
- 剖析 Promise 之基础篇
- 取近似值
- 命运 HDU
- JAVA读取Properties配置文件
- 修改复制后的WEB项目的根路径
- Linux下的消息队列MsgQueue
- MySQL使用笔记
- NDK探究之旅《六》—函数的指针、结构体、枚举、宏定义
- 深度学习之目标跟踪