剖析 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+ 规范明确要求回调需要通过异步方式执行,用以保证一致可靠的执行顺序。为解决这两个问题,可以通过 setTimeoutresolve 中执行回调的逻辑放置到 JS 任务队列末尾:

function resolve(value) {setTimeout(function () {deferreds.forEach(function (deferred) {deferred(value);});}, 0);
}

引入状态

Hmm,好像存在一点问题:如果 Promise 异步操作已经成功,之后调用 then 注册的回调再也不会执行了,而这是不符合我们预期的。

解决这个问题,需要引入规范中所说的 States,即每个 Promise 存在三个互斥状态:pending、fulfilled、rejected,它们之间的关系是:

states flow

经过改进后的代码:

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 series flow

这就是所谓的串行 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 为例,执行步骤如下:

  1. getUserId 生成的 promise (简称 getUserId promise)异步操作成功,执行其内部方法 resolve,传入的参数正是异步操作的结果 userid
  2. 调用 handle 方法处理 deferreds 队列中的回调:getUserMobileById 方法,生成新的 promise(简称 getUserMobileById promise);
  3. 执行之前由 getUserId promise 的 then 方法生成的 bridge promise 的 resolve 方法,传入参数为 getUserMobileById promise。这种情况下,会将该 resolve 方法传入 getUserMobileById promise 的 then 方法中,并直接返回;
  4. getUserMobileById promise 异步操作成功时,执行其 deferreds 中的回调:getUserId bridge promise 的 resolve 方法;
  5. 最后,执行 getUserId bridge promise 的后邻 promise 的 deferreds 中的回调

上述步骤实在有些复杂,主要原因是 bridge promise 的引入。不过正是得益于此,注册一个返回值也是 promise 的回调,从而实现异步操作串行的机制才得以实现。

一图胜千言,下图描述了例 4 的 Promise 执行流:

promise series flow

失败处理

本节处理之前遗留的 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 方法,供异步操作失败时调用,同时抽出了 resolvereject 共用的部分,形成 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、基于 Promise 的异步操作流封装、语法糖等。敬请期待。

参考

  • Introduction to Promises
  • JavaScript Promises … In Wicked Detail
  • A Gentle Introduction to Monads in JavaScript

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/477868.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LeetCode 318. 最大单词长度乘积(位运算)

1. 题目 给定一个字符串数组 words,找到 length(word[i]) * length(word[j]) 的最大值,并且这两个单词不含有公共字母。你可以认为每个单词只包含小写字母。如果不存在这样的两个单词,返回 0。 示例 1: 输入: ["abcw","baz&…

百度研究院商业智能实验室招聘研究实习生!

致力于连接最靠谱的算法岗与最强的求职者招聘贴投放请联系微信xixiaoyao-1岗位职责:同实验室的数据科学家和工程师一起参与研发前沿的机器学习技术,主要内容为对前沿技术进行调研,复现前沿科研成果在顶级会议和期刊上发表论文支持及落地百度飞…

论文浅尝 | KGNLI: 知识图谱增强的自然语言推理模型

笔记整理 | 韩振峰,天津大学硕士链接:https://aclanthology.org/2020.coling-main.571.pdf动机自然语言推理 (NLI) 是自然语言处理中的一项重要任务,它旨在识别两个句子之间的逻辑关系。现有的大多数方法都是基于训练语料库来获得语义知识从而…

block在美团iOS的实践

说到block,相信大部分iOS开发者都会想到retain cycle或是__block修饰的变量。 但是本文将忽略这些老生常谈的讨论,而是将重点放在美团iOS在实践中对block的应用,希望能对同行有所助益。 本文假设读者对block有一定的了解。 从闭包说起 在Lisp…

写Rap,编菜谱,你画我猜……这些 AI demo 我可以玩一天!

文 | ZenMoore编 | 小轶上次写的那篇 《Prompt 综述15篇最新论文梳理]》 有亿点点肝。这次给大家整点轻松好玩的(顺便给这篇推文打个广告,快去看!)。不知道读者朋友们有没有遇到这样的情况:有新的论文发表了&#xff0…

LeetCode 1254. 统计封闭岛屿的数目(图的BFS DFS)

文章目录1. 题目2. 解题2.1 DFS2.2 BFS1. 题目 有一个二维矩阵 grid ,每个位置要么是陆地(记号为 0 )要么是水域(记号为 1 )。 我们从一块陆地出发,每次可以往上下左右 4 个方向相邻区域走,能…

技术动态 | 图对比学习的最新进展

转载公众号 | DataFunSummit 分享嘉宾:朱彦樵 中国科学院自动化研究所编辑整理:吴祺尧 加州大学圣地亚哥分校出品平台:DataFunSummit导读:本文跟大家分享下图自监督学习中最近比较热门的研究方向:图对比学习&#xff0…

如何把Android手机变成一个WIFI下载热点? — 报文转发及DNS报文拦截

随着WiFi的普及,移动运营商的热点也越来越多了,如中国移动的CMCC、中国电信的ChinaNet、中国联通的ChinaUnicom等,一般来说,连上此类的热点,打开浏览器上网时都会自动跳转到一个验证页面,最近有个项目也有类…

OpenKG 祝大家新春快乐

OpenKG 祝各位读者新春快乐,虎虎生威!OpenKGOpenKG(中文开放知识图谱)旨在推动以中文为核心的知识图谱数据的开放、互联及众包,并促进知识图谱算法、工具及平台的开源开放。点击阅读原文,进入 OpenKG 网站。…

推荐中使用FNN/PNN/ONN/NFM优化特征交叉

文 | 水哥源 | 知乎sayingDNN时代来临的时候,LR没打过,也很难加入。FM打不过,但他可以加入FM的精髓,最上在于latent embedding,有了它才能把交互拆解到基底上;居中在于element-wise乘,能让两个特…

LeetCode 310. 最小高度树(图 聪明的BFS,从外向内包围)

文章目录1. 题目2. 解题2.1 暴力BFS2.2 聪明的BFS1. 题目 对于一个具有树特征的无向图,我们可选择任何一个节点作为根。图因此可以成为树,在所有可能的树中,具有最小高度的树被称为最小高度树。给出这样的一个图,写出一个函数找到…

Emma使用与分析

什么是Emma EMMA 是一个开源、面向 Java 程序测试覆盖率收集和报告工具。 它通过对编译后的 Java 字节码文件进行插装,在测试执行过程中收集覆盖率信息,并通过支持多种报表格式对覆盖率结果进行展示。 EMMA 所使用的字节码插装不仅保证 EMMA 不会给源代码…

论文浅尝 | WWW2022 - “知识提示”之知识约束预训练微调

本文作者 | 陈想(浙江大学)、张宁豫(浙江大学)、谢辛(陈想)、邓淑敏(浙江大学)姚云志(浙江大学)、谭传奇(阿里巴巴),黄非&…

吐血整理:论文写作中注意这些细节,能显著提升成稿质量

文 | python编 | 小轶前言不知诸位在科研的起步阶段,是否曾有过如下的感受:总感觉自己写的论文就是和自己读过的论文长得不太一样,也不知道为啥。投稿的时候,审稿人也总是 get 不到论文的核心,只揪着论文的次要细节不放…

LeetCode 1237. 找出给定方程的正整数解

1. 题目 给出一个函数 f(x, y) 和一个目标结果 z&#xff0c;请你计算方程 f(x,y) z 所有可能的正整数 数对 x 和 y。 给定函数是严格单调的&#xff0c;也就是说&#xff1a; f(x, y) < f(x 1, y) f(x, y) < f(x, y 1)函数接口定义如下&#xff1a; interface Cu…

基于Wi-Fi的室内定位在美团总部的实践和应用(上)

室内定位技术的商业化必将带来一波创新高潮&#xff0c;尤其是在O2O领域&#xff0c;各种基于此技术的应用将出现在我们的面前。我们可以想象一些比较常见的应用场景&#xff0c;比如在大型商场里面借助室内导航快速找到目标商铺&#xff0c;商店根据用户的具体位置向用户推送更…

LeetCode 1266. 访问所有点的最小时间(数学)

1. 题目 平面上有 n 个点&#xff0c;点的位置用整数坐标表示 points[i] [xi, yi]。请你计算访问所有这些点需要的最小时间&#xff08;以秒为单位&#xff09;。 你可以按照下面的规则在平面上移动&#xff1a; 每一秒沿水平或者竖直方向移动一个单位长度&#xff0c;或者…

Coding Party 邀你出战!飞桨黑客马拉松线下场来啦

12月4-5日&#xff0c;这48H&#xff0c;与100开发者HACK Together“黑客”很酷&#xff1f;和“黑客”一起“战斗”是种什么样的体验&#xff1f;你参与这场“战斗”了吗&#xff1f;PaddlePaddle Hackathon 2021飞桨黑客马拉松&#xff0c;由百度飞桨联合深度学习技术及应用国…

技术动态 | 「知识图谱嵌入技术研究」最新2022综述

转载公众号 | 专知知识图谱(KG)是一种用图模型来描述知识和建模事物之间关联关系的技术. 知识图谱嵌入(KGE)作为一 种被广泛采用的知识表示方法,其主要思想是将知识图谱中的实体和关系嵌入到连续的向量空间中,用来简化操作, 同时保留 KG 的固有结构.它可以使得多种下游任务受益…

全球仅3000人通过的TensorFlow开发人员认证到底有多香!

现在&#xff0c;我们每个人都已经习惯了社交网络&#xff0c;随时随地都能进行微信、QQ语音和视频通话。那么&#xff0c;你是否发现在不同场景下&#xff0c;这些社交软件音频、视频都是那么的流畅&#xff0c;这背后是什么科技在支撑这“声”与“话”的美好呢&#xff1f;这…