会员登录 - 用户注册 - 设为首页 - 加入收藏 - 网站地图 30分钟,让你彻底明白Promise原理!

30分钟,让你彻底明白Promise原理

时间:2025-11-05 10:48:54 来源:益强数据堂 作者:人工智能 阅读:773次

前言

前一阵子记录了promise0分钟一些常规用法,这篇文章再深入一个层次,让彻来分析分析promise的底明这种规则机制是如何实现的。ps:本文适合已经对promise的原理用法有所了解的人阅读,如果对其用法还不是太了解,可以移步我0分钟上一篇博文。

本文的让彻promise源码是按照Promise/A+规范来编写的(不想看英文版的移步Promise/A+规范中文翻译)

引子

为了让大家更容易理解,我们从一个场景开始讲解,底明让大家一步一步跟着思路思考,原理相信你一定会更容易看懂0分钟

考虑下面一种获取用户id的让彻请求处理

//例1 function getUserId() {     return new Promise(function(resolve) {         //异步请求         http.get(url, function(results) {             resolve(results.id)         })     }) } getUserId().then(function(id) {     //一些处理 })  

getUserId方法返回一个promise,可以通过它的底明then方法注册(注意注册这个词)在promise异步操作成功时执行的回调。这种执行方式,原理使得异步调用变得十分顺手0分钟

原理剖析

那么类似这种功能的让彻Promise怎么实现呢?其实按照上面一句话,实现一个最基础的底明雏形还是很easy的。

极简promise雏形

function Promise(fn) {     var value = null,         callbacks = [];  //callbacks为数组,因为可能同时有很多个回调     this.then = function (onFulfilled) {         callbacks.push(onFulfilled);     };     function resolve(value) {         callbacks.forEach(function (callback) {             callback(value);         });     }     fn(resolve); }  

上述代码很简单,大致的逻辑是站群服务器这样的:

调用then方法,将想要在Promise异步操作成功时执行的回调放入callbacks队列,其实也就是注册回调函数,可以向观察者模式方向思考; 创建Promise实例时传入的函数会被赋予一个函数类型的参数,即resolve,它接收一个参数value,代表异步操作返回的结果,当一步操作执行成功后,用户会调用resolve方法,这时候其实真正执行的操作是将callbacks队列中的回调一一执行;

可以结合例1中的代码来看,首先new Promise时,传给promise的函数发送异步请求,接着调用promise对象的then属性,注册请求成功的回调函数,然后当异步请求发送成功时,调用resolve(results.id)方法, 该方法执行then方法注册的回调数组。

相信仔细的人应该可以看出来,then方法应该能够链式调用,亿华云计算但是上面的最基础简单的版本显然无法支持链式调用。想让then方法支持链式调用,其实也是很简单的:

this.then = function (onFulfilled) {     callbacks.push(onFulfilled);     return this; };  

see?只要简单一句话就可以实现类似下面的链式调用:

// 例2 getUserId().then(function (id) {     // 一些处理 }).then(function (id) {     // 一些处理 });  

加入延时机制

细心的同学应该发现,上述代码可能还存在一个问题:如果在then方法注册回调之前,resolve函数就执行了,怎么办?比如promise内部的函数是同步函数:

// 例3 function getUserId() {     return new Promise(function (resolve) {         resolve(9876);     }); } getUserId().then(function (id) {     // 一些处理 });  

这显然是不允许的,Promises/A+规范明确要求回调需要通过异步方式执行,用以保证一致可靠的执行顺序。因此我们要加入一些处理,保证在resolve执行之前,then方法已经注册完所有的回调。我们可以这样改造下resolve函数:

function resolve(value) {     setTimeout(function() {         callbacks.forEach(function (callback) {             callback(value);         });     }, 0) }  

上述代码的思路也很简单,就是通过setTimeout机制,将resolve中执行回调的逻辑放置到JS任务队列末尾,以保证在resolve执行时,then方法的回调函数已经注册完成.

但是网站模板,这样好像还存在一个问题,可以细想一下:如果Promise异步操作已经成功,这时,在异步操作成功之前注册的回调都会执行,但是在Promise异步操作成功这之后调用的then注册的回调就再也不会执行了,这显然不是我们想要的。

加入状态

恩,为了解决上一节抛出的问题,我们必须加入状态机制,也就是大家熟知的pending、fulfilled、rejected。

Promises/A+规范中的2.1Promise States中明确规定了,pending可以转化为fulfilled或rejected并且只能转化一次,也就是说如果pending转化到fulfilled状态,那么就不能再转化到rejected。并且fulfilled和rejected状态只能由pending转化而来,两者之间不能互相转换。一图胜千言:

 

改进后的代码是这样的:

function Promise(fn) {     var state = pending,         value = null,         callbacks = [];     this.then = function (onFulfilled) {         if (state === pending) {             callbacks.push(onFulfilled);             return this;         }         onFulfilled(value);         return this;     };     function resolve(newValue) {         value = newValue;         state = fulfilled;         setTimeout(function () {             callbacks.forEach(function (callback) {                 callback(value);             });         }, 0);     }     fn(resolve); }  

上述代码的思路是这样的:resolve执行时,会将状态设置为fulfilled,在此之后调用then添加的新回调,都会立即执行。

这里没有任何地方将state设为rejected,为了让大家聚焦在核心代码上,这个问题后面会有一小节专门加入。

链式Promise

那么这里问题又来了,如果用户再then函数里面注册的仍然是一个Promise,该如何解决?比如下面的例4:

// 例4 getUserId()     .then(getUserJobById)     .then(function (job) {         // 对job的处理     }); function getUserJobById(id) {     return new Promise(function (resolve) {         http.get(baseUrl + id, function(job) {             resolve(job);         });     }); }  

这种场景相信用过promise的人都知道会有很多,那么类似这种就是所谓的链式Promise。

链式Promise是指在当前promise达到fulfilled状态后,即开始进行下一个promise(后邻promise)。那么我们如何衔接当前promise和后邻promise呢?(这是这里的难点)。

其实也不是辣么难,只要在then方法里面return一个promise就好啦。Promises/A+规范中的2.2.7就是这么说哒(微笑脸)~

下面来看看这段暗藏玄机的then方法和resolve方法改造代码:

function Promise(fn) {     var state = pending,         value = null,         callbacks = [];     this.then = function (onFulfilled) {         return new Promise(function (resolve) {             handle({                 onFulfilled: onFulfilled || null,                 resolve: resolve             });         });     };     function handle(callback) {         if (state === pending) {             callbacks.push(callback);             return;         }         //如果then中没有传递任何东西         if(!callback.onResolved) {             callback.resolve(value);             return;         }         var ret = callback.onFulfilled(value);         callback.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);                 return;             }         }         state = fulfilled;         value = newValue;         setTimeout(function () {             callbacks.forEach(function (callback) {                 handle(callback);             });         }, 0);     }     fn(resolve); }  

我们结合例4的代码,分析下上面的代码逻辑,为了方便阅读,我把例4的代码贴在这里:

// 例4 getUserId()     .then(getUserJobById)     .then(function (job) {         // 对job的处理     }); function getUserJobById(id) {     return new Promise(function (resolve) {         http.get(baseUrl + id, function(job) {             resolve(job);         });     }); }   then方法中,创建并返回了新的Promise实例,这是串行Promise的基础,并且支持链式调用。 handle方法是promise内部的方法。then方法传入的形参onFulfilled以及创建新Promise实例时传入的resolve均被push到当前promise的callbacks队列中,这是衔接当前promise和后邻promise的关键所在(这里一定要好好的分析下handle的作用)。 getUserId生成的promise(简称getUserId promise)异步操作成功,执行其内部方法resolve,传入的参数正是异步操作的结果id 调用handle方法处理callbacks队列中的回调:getUserJobById方法,生成新的promise(getUserJobById promise) 执行之前由getUserId promise的then方法生成的新promise(称为bridge promise)的resolve方法,传入参数为getUserJobById promise。这种情况下,会将该resolve方法传入getUserJobById promise的then方法中,并直接返回。 在getUserJobById promise异步操作成功时,执行其callbacks中的回调:getUserId bridge promise中的resolve方法 ***执行getUserId bridge promise的后邻promise的callbacks中的回调。

更直白的可以看下面的图,一图胜千言(都是根据自己的理解画出来的,如有不对欢迎指正):

 

失败处理

在异步操作失败时,标记其状态为rejected,并执行注册的失败回调:

//例5 function getUserId() {     return new Promise(function(resolve) {         //异步请求         http.get(url, function(error, results) {             if (error) {                 reject(error);             }             resolve(results.id)         })     }) } getUserId().then(function(id) {     //一些处理 }, function(error) {     console.log(error) })  

有了之前处理fulfilled状态的经验,支持错误处理变得很容易,只需要在注册回调、处理状态变更上都要加入新的逻辑:

function Promise(fn) {     var state = pending,         value = null,         callbacks = [];     this.then = function (onFulfilled, onRejected) {         return new Promise(function (resolve, reject) {             handle({                 onFulfilled: onFulfilled || null,                 onRejected: onRejected || null,                 resolve: resolve,                 reject: reject             });         });     };     function handle(callback) {         if (state === pending) {             callbacks.push(callback);             return;         }         var cb = state === fulfilled ? callback.onFulfilled : callback.onRejected,             ret;         if (cb === null) {             cb = state === fulfilled ? callback.resolve : callback.reject;             cb(value);             return;         }         ret = cb(value);         callback.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;         execute();     }     function reject(reason) {         state = rejected;         value = reason;         execute();     }     function execute() {         setTimeout(function () {             callbacks.forEach(function (callback) {                 handle(callback);             });         }, 0);     }     fn(resolve, reject); }  

上述代码增加了新的reject方法,供异步操作失败时调用,同时抽出了resolve和reject共用的部分,形成execute方法。

错误冒泡是上述代码已经支持,且非常实用的一个特性。在handle中发现没有指定异步操作失败的回调时,会直接将bridge promise(then函数返回的promise,后同)设为rejected状态,如此达成执行后续失败回调的效果。这有利于简化串行Promise的失败处理成本,因为一组异步操作往往会对应一个实际功能,失败处理方法通常是一致的:

//例6 getUserId()     .then(getUserJobById)     .then(function (job) {         // 处理job     }, function (error) {         // getUserId或者getUerJobById时出现的错误         console.log(error);     });  

异常处理

细心的同学会想到:如果在执行成功回调、失败回调时代码出错怎么办?对于这类异常,可以使用try-catch捕获错误,并将bridge promise设为rejected状态。handle方法改造如下:

function handle(callback) {     if (state === pending) {         callbacks.push(callback);         return;     }     var cb = state === fulfilled ? callback.onFulfilled : callback.onRejected,         ret;     if (cb === null) {         cb = state === fulfilled ? callback.resolve : callback.reject;         cb(value);         return;     }     try {         ret = cb(value);         callback.resolve(ret);     } catch (e) {         callback.reject(e);     } }  

如果在异步操作中,多次执行resolve或者reject会重复处理后续回调,可以通过内置一个标志位解决。

总结

刚开始看promise源码的时候总不能很好的理解then和resolve函数的运行机理,但是如果你静下心来,反过来根据执行promise时的逻辑来推演,就不难理解了。这里一定要注意的点是:promise里面的then函数仅仅是注册了后续需要执行的代码,真正的执行是在resolve方法里面执行的,理清了这层,再来分析源码会省力的多。

现在回顾下Promise的实现过程,其主要使用了设计模式中的观察者模式:

通过Promise.prototype.then和Promise.prototype.catch方法将观察者方法注册到被观察者Promise对象中,同时返回一个新的Promise对象,以便可以链式调用。 被观察者管理内部pending、fulfilled和rejected的状态转变,同时通过构造函数中传递的resolve和reject方法以主动触发状态转变和通知观察者。

参考文献

深入理解 Promise JavaScript Promises … In Wicked Detail 

(责任编辑:域名)

推荐内容
  • 解锁手机屏幕的方法(快速、安全、有效的解锁手机屏幕密码)
  • 缤特力e500(一台能够满足你所有需求的理想车型)
  • 老毛桃制作工具教程(以老毛桃为材料,手工制作各类工具,让生活更便利)
  • OPPOR9最新版本的性能和功能如何?(探究OPPOR9最新版本的关键特点和用户体验)
  • Ubuntu是一个流行的Linux操作系统,基于Debian发行版和GNOME桌面环境,和其他Linux发行版相比,Ubuntu非常易用,和Windows相容性很好,非常适合Windows用户的迁移,预装了大量常用软件,中文版的功能也较全,支持拼音输入法,预装了Firefox、Open Office、多媒体播放、图像处理等大多数常用软件,一般会自动安装网卡、音效卡等设备的驱动,对于不打游戏不用网银的用户来说,基本上能用的功能都有了,在Windows操作系统下不用分区即可安装使用,就如同安装一个应用软件那么容易,整个Ubuntu操作系统在Windows下就如同一个大文件一样,很容易卸载掉。下面我就介绍一下Ubuntu操作系统安装使用的方法,供Ubuntu新手参考,希望能起到Linux扫盲的作用。     下载Ubuntu   Ubuntu有三个版本,分别是桌面版(Desktop Edition),服务器版(Server Edition),上网本版(Netbook Remix),普通桌面电脑使用桌面版即可,下载地址请点这里,32位CPU请选择32bit version,上网本则可下载Netbook Remix,目前Ubuntu已经占据三分之一的上网本市场,仅次于Windows XP系统。Google的Chrome操作系统强有力的对手就是Ubuntu Netbook Remix。   目前最新的版本是9.04版,下载后的文件名是ubuntu-9.04-desktop-i386.iso,大小是698M,通过迅雷下载非常快,大约半个小时左右可以下载完毕。   安装Ubuntu   在Windows下可以不用重新分区,直接像安装一个应用程序那样安装Ubuntu,安装方法是,先使用一个虚拟光驱(例如微软的Windows虚拟光驱)装载ubuntu-9.04-desktop-i386.iso文件,然后运行根目录下的wubi.exe,运行前要将本地磁盘的名字都修改为英文名,否则会出现错误信息“UnicodeEncodeError: ascii codec cant encode characters in position 0-3: ordinal not in range(128)”而无法运行。       运行之后,会出现如下界面,选择“Install inside Windows”即可在Windows下直接安装而无需分区。    接着出现下面的安装界面,选择一个磁盘,然后将语言选择为“Chinese(Simplified)简体中文”,Installation size为Ubuntu环境的总共磁盘大小,然后是登录用户名和密码,设置好了以后就点安装继续。    后面的安装操作很简单,不需要手动干预就可以直接安装好整个操作系统,大部分的硬件驱动都可以自动安装好。提示安装完毕后,重启系统,就可以使用Ubuntu了。   自动登录Ubuntu   Ubuntu默认是每次登录都是要输入用户名和密码的,这是基于安全方面的考虑,不过对于桌面版,大家都习惯自己的电脑能自动登录,类似Windows XP系统那样,通过一些设置可以实现Ubuntu自动登录。设置的方法是:点击“系统”—“系统管理”—“登录窗口” (需要输入管理员密码),然后在“安全”选项页—勾选(启用自动登录),然后在下拉列表里选择自己的用户名。之后Ubuntu就能够自动登录了。   开机自动运行程序   类似Windows的启动菜单,在Linux也可以实现开机自动运行一些命令,比较简单的方法是修改 /etc/rc.local 文件,将需要执行的命令添加进去。     桌面设置   Ubuntu的桌面,默认有两个任务栏,一个在上面,一个在下面,通常习惯Windows的用户喜欢将上面的移到下面,Ubuntu的面板无法拖动,在上面点右键后,可以让其显示在屏幕下端。   桌面背景设置和Windows很类似,在“桌面”上点右键,点更改桌面背景,就可以进行修改设置。   修改root密码   Ubuntu默认的用户并不是root,我们可以通过操作来使用root这个超级管理员帐号,以获得更大的权限。先打开终端,然后执行下面的语句   sudo passwd root   就可以修改超级管理员root的密码,之后就可以使用su命令切换到root用户来执行某些更高权限的操作。   Hosts修改   在Windows下,我们上Twitter等网站都需要修改hosts文件,在Linux下也有hosts文件,文件位于/etc/hosts,使用root用户可以编辑修改这个文件,主机名和IP的格式与Windows的完全相同,例如:   127.0.0.1 localhost   在Ubuntu下安装软件   Ubuntu下的软件安装有几种方式,常用的是deb包的安装方式,deb是debian系列的Linux包管理方式,ubuntu属于debian的派生,也默认支持这种软件安装方式,当下载到一个deb格式的软件后,直接在界面上就可以安装。   另一种常见的安装方式是源代码编译安装,很多软件会提供了源代码给最终用户,用户需要自行编译安装,先使用tar将源代码解压缩到一个目录下,然后进入这个目录,执行以下三条命令:   ./configure     make   sudo make install   执行完成后,即可完成软件的编译和安装。   还有一种方式是apt-get的安装方法,APT是Debian及其衍生发行版的软件包管理器,APT可以自动下载,配置,安装二进制或者源代码格式的软件包,因此简化了Unix系统上管理软件的过程。常用的安装命令是:   sudo apt-get install 软件名   sudo apt-get remove 软件名   Firefox浏览器的更新   Ubuntu安装完成后会自动安装一个Firefox浏览器,遗憾的是这个Firefox版本通常较低,例如Ubuntu 9.04会安装Firefox 3.0,不过我们可以想办法下载最新的Firefox覆盖掉老版本Firefox,具体方法是,先上Firefox官方网站下载最新的Linux版本Firefox,然后将其解压缩到某一个目录下,例如firefox目录,进入终端,到这个目录的父目录,执行下面的语句:   sudo cp -r firefox /usr/lib/firefox-3.5.2   sudo mv /usr/bin/firefox /usr/bin/firefox.old   sudo ln -s /usr/lib/firefox-3.5.2/firefox /usr/bin/firefox-3.5.2   sudo ln -s /usr/bin/firefox-3.5.2 /usr/bin/firefox   之后就可以将Firefox成功替换为最新的Firefox 3.52版本,未来的Firefox更新也可以使用这种方法。     Firefox的Flash问题   经过我的实际测试,Ubuntu自动安装的Flash插件swfdec存在很多问题,在Firefox中,很多网页的Flash无法显示,包括Google音乐和开心网等,因此建议使用下面两条语句将其卸载。   sudo apt-get remove swfdec-mozilla   sudo apt-get remove swfdec-gnome   之后可安装官方的Adobe Flash Player的Linux版,下载地址是: http://get.adobe.com/flashplayer/   安装完成后,还要解决中文乱码问题,解决方法是执行下面语句:   sudo cp /etc/fonts/conf.d/49-sansserif.conf /etc/fonts/conf.d/49-sansserif.conf.bak   sudo rm /etc/fonts/conf.d/49-sansserif.conf   之后,Firefox的Flash就完全正常了,在Firefox中访问开心网等Flash网站,显示都正常。   安装常用软件   介绍完了安装的方法和Firefox,下面就可以去各个网站下载一些常用的Linux软件来安装了,下面是我整理的一些常用的Linux软件列表:   Linux QQ:访问这个地址,下载deb文件安装,可以在Linux下玩腾讯QQ。   防火墙 firestarter: 使用 sudo apt-get install firestarter 安装。   杀毒软件 AntiVir: 虽然Linux下的病毒很少,但对于新手还是有必要安装一个杀毒软件,访问这个地址可以下载免费版的AntiVir杀毒软件,这个软件我曾经在《五个最佳的防病毒软件》中介绍过。   rpm 转 deb 工具: 使用 sudo apt-get install alien 安装   JAVA环境安装: JRE的安装 sudo apt-get install sun-java6-jre ,JDK的安装 sudo apt-get install sun-java6-jdk       eclipse安装: 先到这个地址下载最新的eclipse,然后使用tar xvfz eclipse-php-galileo-linux-gtk.tar.gz -C /opt 解压缩后就可以使用。    Picasa 3 for Linux安装: 访问这个地址,下载后直接安装。   Google Earth安装: 在这里下载最新版本的Google Earth,下载下来是个BIN文件,在图形界面上右击 GoogleEarthLinux.bin,在“权限”选项卡中勾选“允许以程序执行文件”,如下图。    之后在终端上执行 ./GoogleEarthLinux.bin 即可安装。    安装LAMP环境   Ubuntu的桌面版也可以安装LAMP(Linux + Apache + MySQL + PHP)环境,这里我介绍一个最简单的方法,就是使用XAMPP,这个项目我曾经在《常见的WAMP集成环境》中介绍过,XAMPP不但支持Windows,还支持Linux,在其网站下载之后,运行下面两条命令:   tar xvfz xampp-linux-1.7.2.tar.gz -C /opt   /opt/lampp/lampp start   就可以启动LAMP环境,XAMPP是功能全面的集成环境,软件包中包含Apache、MySQL、SQLite、PHP、Perl、FileZilla FTP Server、Tomcat等等,很适合开发环境使用。   安装程序添加程序菜单和桌面       有些程序是直接解压缩安装的,因此不会添加“应用程序”的菜单项,我们可以手动将其添加菜单项,具体方法是,打开“系统”—“首选项”—“主菜单”,新增即可。   添加桌面快捷方式是,在桌面上点右键,创建启动器。这个“启动器”就是Windows里面的“快捷方式”。   将“应用程序”的菜单项创建到桌面快捷方式的方法是,在“应用程序”的菜单项上单击鼠标右键,选择“将此启动器添加到桌面”或“将此启动器添加到面板”,就可以了。
  • 优盘系统安装教程(从零开始,轻松完成优盘系统的安装和配置)