当前位置: 首页 > 科技观察

30分钟,让你全面了解Promise的原理

时间:2023-03-12 07:42:52 科技观察

前言我前段时间记录了promise的一些常用用法。本文更深入地分析和分析promise的规则机制是如何实现的。ps:本文适合已经知道promise用法的朋友。如果不太了解它的用法,可以移步我之前的博文。本文中的promise源码是按照Promise/A+规范编写的(我不想看英文版的Promise/A+规范中文翻译)。为了让大家更容易理解,我们从一个场景开始,让大家循序渐进,相信你会更容易理解。考虑如下获取用户id的请求处理//例1functiongetUserId(){returnnewPromise(function(resolve){//异步请求http.get(url,function(results){resolve(results.id)})})}getUserId().then(function(id){//someprocessing})getUserId方法返回一个promise,可以通过它的then方法注册(注意注册二字),当promise异步操作成功时执行回调。这种执行方式使得异步调用非常方便。原理分析那么如何实现类似功能的Promise呢?其实按照上面这句话,实现最基本的原型还是很容易的。极简promise原型functionPromise(fn){varvalue=null,callbacks=[];//callbacks是一个数组,因为可能同时有很多回调this.then=function(onFulfilled){callbacks.push(onFulfilled);};functionresolve(value){callbacks.forEach(function(callback){callback(value);});}fn(resolve);}上面的代码很简单,大体逻辑是这样的:调用then方法,你将Promise中想要异步操作成功时执行的回调放在callbacks队列中,其实就是注册的回调函数,可以考虑观察者模式的方向;创建Promise实例时传入的函数会被赋予一个函数类型的参数,即resolve,它接收一个参数值,代表异步操作返回的结果。当一步操作执行成功后,用户会调用resolve方法。这时候实际操作就是将回调队列中的回调一个一个执行;结合示例1中的代码可以看出,首先在创建新的Promise时,传递给promise的函数发送一个异步请求,然后调用promise对象的then属性,注册成功的回调函数request,然后在异步请求发送成功后调用resolve(results.id)方法,执行then方法注册的回调数组。相信细心的人应该能看出来then方法应该是可以链式调用的,但是上面这个最基础最简单的版本显然是不能支持链式调用的。想让then方法支持链式调用其实很简单:this.then=function(onFulfilled){callbacks.push(onFulfilled);returnthis;};看到了吗?简单的一句话就可以实现类似于下面的链式调用//Example2getUserId().then(function(id){//Someprocessing}).then(function(id){//Someprocessing});加了延迟机制后,细心的同学应该会发现,上面的代码可能还是有问题:如果在then方法注册回调之前就执行了resolve函数,怎么办?比如promise里面的函数是一个同步函数://Example3functiongetUserId(){returnnewPromise(function(resolve){resolve(9876);});}getUserId().then(function(id){//一些加工});这显然是不允许的。Promises/A+规范明确要求回调需要异步执行,以保证一致可靠的执行顺序。因此,我们需要增加一些处理,确保在执行resolve之前,所有的回调都已经注册到then方法中。我们可以这样修改resolve函数:functionresolve(value){setTimeout(function(){callbacks.forEach(function(callback){callback(value);});},0)}上面代码的思路也很简单,就是通过setTimeout机制,将resolve中执行回调的逻辑放在JS任务队列的尾部,保证resolve执行时then方法的回调函数已经注册。不过,这种方式好像有个问题,大家可以仔细想想:如果Promise异步操作已经成功了。这时候异步操作成功前注册的回调会被执行,但是Promise异步操作成功后调用的then注册的回调就再也不会执行了,这显然不是我们想要的。添加状态那么,为了解决上一节中提出的问题,我们必须添加状态机制,这些机制被称为pending、fulfilled和rejected。2.1Promises/A+规范中的PromiseStates明确规定pending可以转换为fulfilled或rejected,并且只能转换一次,也就是说如果pending转换为fulfilled状态,则不能再次转换为rejected。而fulfilled和rejected状态只能由pending转化而来,两者不能相互转化。一图胜千言:改进后的代码是这样的:'pending'){callbacks.push(onFulfilled);returnthis;}onFulfilled(value);returnthis;};functionresolve(newValue){value=newValue;state='fulfilled';setTimeout(function(){callbacks.forEach(函数(callback){callback(value);});},0);}fn(resolve);}上面代码的思路如下:resolve执行时,状态会设置为fulfilled,并且那么then添加的新回调将被调用,将立即执行。没有地方设置状态为rejected。为了让大家关注核心代码,后面会有专栏专门讨论这个问题。ChainedPromise那么问题又来了,如果用户还在then函数中注册了一个Promise,怎么解决呢?比如下面的例子4://Example4g??etUserId().then(getUserJobById).then(function(job){//processingofjob});functiongetUserJobById(id){returnnewPromise(function(resolve){http.get(baseUrl+id,function(job){resolve(job);});});}相信用过promises的人都知道会有很多场景,所以这就是所谓的chainedpromise。ChainedPromise是指当前promise达到fulfilled状态后,开始下一个promise(next-adjacentpromise)。那么我们如何连接当前的承诺和下一个相邻的承诺呢?(这是这里的难点)。其实也没那么难,只要在then方法中返回一个promise即可。Promises/A+规范中的2.2.7是这么说的(笑脸)~我们来看看then方法和resolve方法中隐藏的玄机,改造代码:functionPromise(fn){varstate='pending',value=null,callbacks=[];this.then=function(onFulfilled){returnnewPromise(function(resolve){handle({onFulfilled:onFulfilled||null,resolve:resolve});});};functionhandle(callback){if(state==='pending'){callbacks.push(callback);return;}//如果没有传入则if(!callback.onResolved){callback.resolve(value);return;}varret=callback.onFulfilled(value);callback.resolve(ret);}functionresolve(newValue){if(newValue&&(typeofnewValue==='object'||typeofnewValue==='function')){varthen=newValue.then;if(typeofthen==='function'){then.call(newValue,resolve);return;}}state='fulfilled';value=newValue;setTimeout(function(){callbacks.forEach(function(callback){handle(callback);});},0);}fn(resolve);}我们结合例4的代码,分析一下上面的代码逻辑。为了阅读方便,我把例4的代码贴在这里://Example4g??etUserId()。then(getUserJobById).then(function(job){//job的处理});functiongetUserJobById(id){returnnewPromise(function(resolve){http.get(baseUrl+id,function(job){resolve(job);});});}then方法中创建并返回一个新的Promise实例,它是串行Promise的基础,支持链式调用handle方法的方式是promise的内部方法。then方法传入的形参onFulfilled和新建Promise实例时传入的resolve被推送到当前promise的回调队列中,这是连接当前promise和后续promise的关键(一定要分析这里小心处理)的作用)。getUserId生成的promise(简称getUserIdpromise)异步操作成功,执行其内部方法resolve。传入的参数是异步操作的结果id。调用handle方法处理回调队列中的回调:getUserJobById方法生成新的promise(getUserJobByIdpromise)执行getUserIdpromise的then方法生成的新promise(称为bridgepromise)的resolve方法,传入参数是getUserJobById承诺。在这种情况下,resolve方法将传递给getUserJobByIdpromise的then方法并直接返回。当getUserJobByIdpromise的异步操作成功时,在其回调中执行回调:getUserIdbridgepromise中的resolve方法***在getUserIdbridgepromise的next-adjacentpromise的回调中执行回调。说的直接一点,大家可以看下图,一张图顶一千字(都是根据自己的理解画的,有不对的地方请指正):失败处理当异步操作失败时,标记其状态为rejected,执行注册失败回调://Example5functiongetUserId(){returnnewPromise(function(resolve){//异步请求http.get(url,function(error,results){if(error){reject(error);}resolve(results.id)})})}getUserId().then(function(id){//someprocessing},function(error){console.log(error)})有之前处理的经验完成状态,对错误处理的支持变得很容易,只需添加新的逻辑来注册回调和处理状态变化:functionPromise(fn){varstate='pending',value=null,callbacks=[];this.then=function(onFulfilled,onRejected){returnnewPromise(function(resolve,reject){handle({onFulfilled:onFulfilled||null,onRejected:onRejected||null,resolve:resolve,reject:reject});});};functionhandle(callback){if(state==='pending'){callbacks.push(callback);return;}varcb=state==='fulfilled'?callback.onFulfilled:回调.onRejected,ret;if(cb===null){cb=state==='fulfilled'?callback.resolve:callback.reject;cb(value);return;}ret=cb(value);callback.resolve(ret);}functionresolve(newValue){if(newValue&&(typeofnewValue==='object'||typeofnewValue==='function')){varthen=newValue.then;if(typeofthen==='function'){then.call(newValue,resolve,reject);return;}}state='fulfilled';value=newValue;execute();}functionreject(reason){state='rejected';value=reason;execute();}functionexecute(){setTimeout(function(){callbacks.forEach(function(callback){handle(callback);});},0);}fn(resolve,reject);}上面的代码新增了一个reject方法,在异步操作失败时调用。同时,将resolve和reject共享的部分提取出来,形成execute方法错误冒泡是上面代码已经支持的一个非常实用的特性。当在handle中发现没有指定异步操作失败的回调时,会直接将bridgepromise(then函数返回的promise,下同)设置为rejected状态,从而达到如下效果执行后续的失败回调。这有助于简化串行Promise的失败处理成本,因为一组异步操作往往对应一个实际的函数,失败处理方式通常是一致的://例6getUserId().then(getUserJobById).then(function(job){//处理作业},function(error){//Errorconsole.log(error)whengetUserIdorgetUerJobById;});对异常处理比较细心的同学会想:如果在执行成功回调或者失败回调的时候代码出错怎么办?对于此类异常,可以使用try-catch来捕获错误,并将bridgepromise设置为rejected状态。handle方法修改如下:onRejected,ret;if(cb===null){cb=state=='fulfilled'?callback.resolve:callback.reject;cb(value);return;}try{ret=cb(value);回调。resolve(ret);}catch(e){callback.reject(e);}}如果在异步操作中多次执行resolve或reject,后面的回调会被重复处理,可以通过内置flag解决.小结刚开始看promise源码的时候,并不能很好的理解then和resolve函数的运行机制,但是如果你在执行promise的时候静下心来,按照逻辑推导一下,就不难理解了.这里必须要注意的是:promise中的then函数只是注册了后面需要执行的代码,真正的执行是在resolve方法中执行的。理清了这一层之后,分析源码就省了很多功夫。现在回头看看Promise的实现过程,在设计模式中主要使用了观察者模式:通过Promise.prototype.then和Promise.prototype.catch方法将观察者方法注册到被观察的Promise对象中,并返回一个新的同时Promise对象,以便它可以被链接。被观察对象管理内部的pending、fulfilled、rejected状态转换,同时主动触发状态转换并通过构造函数中传入的resolve和reject方法通知观察者。深入研究PromiseJavaScriptPromises......在邪恶的细节