从一道Promise执行顺序的题目看Promise实现

之前在网上看到一道Promise执行顺序的题目——打印以下程序的输出:

成都网站制作、成都网站建设的开发,更需要了解用户,从用户角度来建设网站,获得较好的用户体验。创新互联公司多年互联网经验,见的多,沟通容易、能帮助客户提出的运营建议。作为成都一家网络公司,打造的就是网站建设产品直销的概念。选择创新互联公司,不只是建站,我们把建站作为产品,不断的更新、完善,让每位来访用户感受到浩方产品的价值服务。

你所需要的网站建设服务,我们均能行业靠前的水平为你提供.标准是产品质量的保证,主要从事成都网站制作、成都网站设计、外贸营销网站建设企业网站建设、手机网站制作设计、网页设计、成都品牌网站建设、网页制作、做网站、建网站。创新互联建站拥有实力坚强的技术研发团队及素养的视觉设计专才。

目前创新互联已为数千家的企业提供了网站建设、域名、网页空间、网站运营、企业网站设计、江华网站维护等服务,公司将坚持客户导向、应用为本的策略,正道将秉承"和谐、参与、激情"的文化,与客户和合作伙伴齐心协力一起成长,共同发展。

创新互联专注于企业全网整合营销推广、网站重做改版、普陀网站定制设计、自适应品牌网站建设、H5高端网站建设商城网站开发、集团公司官网建设、外贸网站建设、高端网站制作、响应式网页设计等建站业务,价格优惠性价比高,为普陀等各大城市提供网站开发制作服务。

超过10多年行业经验,技术领先,服务至上的经营模式,全靠网络和口碑获得客户,为自己降低成本,也就是为客户降低成本。到目前业务范围包括了:成都网站建设、网站建设,成都网站推广,成都网站优化,整体网络托管,重庆小程序开发公司,微信开发,成都App定制开发,同时也可以让客户的网站和网络营销和我们一样获得订单和生意!

成都创新互联是一家专注于成都网站设计、网站制作、外贸营销网站建设与策划设计,马边彝族网站建设哪家好?成都创新互联做网站,专注于网站建设十年,网设计领域的专业建站公司;建站业务涵盖:马边彝族等地区。马边彝族做网站价格咨询:13518219792

成都创新互联成都网站建设按需定制网站,是成都网站设计公司,为成都集装箱提供网站建设服务,有成熟的网站定制合作流程,提供网站定制设计服务:原型图制作、网站创意设计、前端HTML5制作、后台程序开发等。成都网站制作热线:13518219792

创新互联服务项目包括山西网站建设、山西网站制作、山西网页制作以及山西网络营销策划等。多年来,我们专注于互联网行业,利用自身积累的技术优势、行业经验、深度合作伙伴关系等,向广大中小型企业、政府机构等提供互联网行业的解决方案,山西网站推广取得了明显的社会效益与经济效益。目前,我们服务的客户以成都为中心已经辐射到山西省份的部分城市,未来相信会继续扩大服务区域并继续获得客户的支持与信任!

网站建设哪家好,找创新互联建站!专注于网页设计、网站建设、微信开发、重庆小程序开发、集团企业网站建设等服务项目。为回馈新老客户创新互联还提供了梨树免费建站欢迎大家使用!

成都创新互联公司2013年至今,先为杜集等服务建站,杜集等地企业,进行企业商务咨询服务。为杜集企业网站制作PC+手机+微官网三网同步一站式服务解决您的所有建站问题。

赣县ssl适用于网站、小程序/APP、API接口等需要进行数据传输应用场景,ssl证书未来市场广阔!成为创新互联的ssl证书销售渠道,可以享受市场价格4-6折优惠!如果有意向欢迎电话联系或者加微信:18982081108(备注:SSL证书合作)期待与您的合作!

 
 
 
 
  1. new Promise(resolve => { 
  2.     console.log(1); 
  3.     resolve(3); 
  4. }).then(num => { 
  5.     console.log(num) 
  6. }); 
  7. console.log(2) 

这道题的输出是123,为什么不是132呢?因为我一直理解Promise是没有异步功能,它只是帮忙解决异步回调的问题,实质上是和回调是一样的,所以如果按照这个想法,resolve之后应该会立刻then。但实际上并不是。难道用了setTimeout?

如果在promise里面再加一个promise:

 
 
 
 
  1. new Promise(resolve => { 
  2.     console.log(1); 
  3.     resolve(3); 
  4.     Promise.resolve().then(()=> console.log(4)) 
  5. }).then(num => { 
  6.     console.log(num) 
  7. }); 
  8. console.log(2) 

执行顺序是1243,第二个Promise的顺序会比***个的早,所以直观来看也是比较奇怪,这是为什么呢?

Promise的实现有很多库,有jQuery的deferred,还有很多提供polyfill的,如es6-promise,lie等,它们的实现都基于Promise/A+标准,这也是ES6的Promise采用的。

为了回答上面题目的执行顺序问题,必须得理解Promise是怎么实现的,所以得看那些库是怎么实现的,特别是我错误地认为不存在的Promise的异步是怎么实现的,因为***一行的console.log(2)它并不是***执行的,那么必定有某些类似于setTimeout的异步机制让上面同步的代码在异步执行,所以它才能在代码执行完了之后才执行。

当然我们不只是为了解答一道题,主要还是借此了解Promise的内部机制。读者如果有时间有兴趣可以自行分析,然后再回过头来比较一下本文的分析。或者你可以跟着下面的思路,操起鼠标和键盘和我一起干。

这里使用lie的库,相对于es6-promise来说代码更容易看懂,先npm install一下:

 
 
 
 
  1. npm install lie 

让代码在浏览器端运行,准备以下html:

 
 
 
 
  1.  
  2.  
  3.  
  4.      
  5.  
  6.  
  7.      
  8.      
  9.  
  10.  

其中index.js的内容为:

 
 
 
 
  1. console.log(Promise); 
  2. new Promise(resolve => { 
  3.     console.log(1); 
  4.     resolve(3); 
  5.     Promise.resolve().then(()=> console.log(4)) 
  6. }).then(num => { 
  7.     console.log(num) 
  8. }); 
  9. console.log(2); 

把Promise打印一下,确认已经把原生的那个覆盖了,对比如下:

因为原生的Promise我们是打不了断点的,所以才需要借助一个第三方的库。

我们在第4行的resolve(3)那里打个断点进去看一下resolve是怎么执行的,层层进去,***的函数是这个:

我们发现,这个函数好像没干啥,它就是设置了下self的state状态为FULFILLED(完成),并且把结果outcome设置为调resolve传进来的值,这里是3,如果resolve传来是一个Promise的话就会进入到上图187行的Promise链处理,这里我们不考虑这种情况。这里的self是指向一个Promise对象:

它主要有3个属性——outcome、queue、state,其中outcome是resolve传进来的结果,state是Promise的状态,在第83行的代码可以查到Promise的状态总共有3种:

 
 
 
 
  1. var REJECTED = ['REJECTED']; 
  2. var FULFILLED = ['FULFILLED']; 
  3. var PENDING = ['PENDING']; 

Rejected失败,fulfilled成功,pending还在处理中,在紧接着89行的Promise的构造函数可以看到,state初始化的状态为pending:

 
 
 
 
  1. function Promise(resolver) { 
  2.   if (typeof resolver !== 'function') { 
  3.     throw new TypeError('resolver must be a function'); 
  4.   } 
  5.   this.state = PENDING; 
  6.   this.queue = []; 
  7.   this.outcome = void 0; 
  8.   if (resolver !== INTERNAL) { 
  9.     safelyResolveThenable(this, resolver); 
  10.   } 

并且在右边的调用栈可以看到,resolver是由Promise的构造函数触发执行的,即当你new Promise的时候就会执行传参的函数,如下图所示:

传进来的函数支持两个参数,分别是resolve和reject回调:

传进来的函数支持两个参数,分别是resolve和reject回调:

 
 
 
 
  1. let resolver = function(resolve, reject) { 
  2.     if (success) resolve(); 
  3.     else reject(); 
  4. }; 
  5.   
  6. new Promise(resolver); 

这两个函数是Promise内部定义,但是要在你的函数里调一下它的函数,告诉它什么时候成功了,什么时候失败了,这样它才能继续下一步的操作。所以这两个函数参数是传进来的,它们是Promise的回调函数。Promise是怎么定义和传递这两个函数的呢?还是在刚刚那个断点的位置,但是我们改变一下右边调用栈显示的位置:

上图执行的thenable函数就是我们传给它的resolver,然后传递onSuccess和onError,分别是我们在resolver里面写的resolve和reject这两个参数。如果我们调了它的resolve即onSuccess函数,它就会调236行的handlers.resolve就到了我们***次打断点的那张图,这里再放一次:

然后去设置当前Promise对象的state,outcome等属性。这里没有进入到193行的while循环里,因为queue是空的。这个地方下文会继续提到。

接着,我们在then那里打个断点进去看一下:

then又做了些什么工作呢?如下图所示:

then可以传两个参数,分别为成功回调和失败回调。我们给它传了一个成功回调,即上图划线的地方。并且由于在resolver里面已经把state置成fulfilled完成态了,所以它会执行unwrap函数,并传递成功回调、以及resolve给的结果outcome(还有一个参数promise,主要是用于返回,形成then链)。

unwrap函数是这样实现的:

在167行执行then里传给Promise的成功回调,并传递结果outcome。

这段代码是包在一个immediate函数里的,这里就是解决Promise异步问题的关键了。并且我们在node_modules目录里面,也发现了lie使用了immediate库,它可以实现一个nextTick的功能,即在当前代码逻辑单元同步执行完了之后立刻执行,相当于setTimeout 0,但是它又不是直接用setTimeout 0实现的。

我们重点来看一下它是怎么实现一个nextTick的功能的。immediate里面会调一个scheduleDrain(drain是排水的意思):

 
 
 
 
  1. function immediate(task) { 
  2.   // 这个判断先忽略 
  3.   if (queue.push(task) === 1 && !draining) { 
  4.     scheduleDrain(); 
  5.   } 

实现逻辑在这个scheduleDrain,它是这么实现的:

 
 
 
 
  1. var Mutation = global.MutationObserver || global.WebKitMutationObserver; 
  2. var scheduleDrain = null; 
  3.   // 浏览器环境,IE11以上支持 
  4.   if (Mutation) { 
  5.       // ... 
  6.   }  
  7.   // Node.js环境 
  8.   else if (!global.setImmediate && typeof global.MessageChannel !== 'undefined') 
  9.   
  10.   } 
  11.   // 低浏览器版本解决方案 
  12.   else if ('document' in global && 'onreadystatechange' in global.document.createElement('script')) { 
  13.   
  14.   } 
  15.   // ***实在没办法了,用最次的setTimeout 
  16.   else { 
  17.     scheduleDrain = function () { 
  18.       setTimeout(nextTick, 0); 
  19.     }; 
  20.   } 

它会有一个兼容性判断,优先使用MutationObserver,然后是使用script标签的方式,这种到IE6都支持,***啥都不行就用setTimeout 0.

我们主要看一下Mutation的方式是怎么实现的,MDN上有介绍这个MutationObserver的用法,可以用它来监听DOM结点的变化,如增删、属性变化等。Immediate是这么实现的:

 
 
 
 
  1. if (Mutation) { 
  2.     var called = 0; 
  3.     var observer = new Mutation(nextTick); 
  4.     var element = global.document.createTextNode(''); 
  5.     // 监听节点的data属性的变化 
  6.     observer.observe(element, { 
  7.       characterData: true 
  8.     }); 
  9.     scheduleDrain = function () { 
  10.       // 让data属性发生变化,在0/1之间不断切换, 
  11.       // 进而触发observer执行nextTick函数 
  12.       element.data = (called = ++called % 2); 
  13.     }; 
  14.   } 

使用nextTick回调注册一个observer观察者,然后创建一个DOM节点element,成为observer的观察对象,观察它的data属性。当需要执行nextTick函数的时候,就调一下scheduleDrain改变data属性,就会触发观察者的回调nextTick。它是异步执行的,在当前代码单元执行完之后立刻之行,但又是在setTimeout 0之前执行的,也就是说,以下代码,***行的5是***输出的:

 
 
 
 
  1. setTimeout(()=> console.log(5), 0); 
  2. new Promise(resolve => { 
  3.     console.log(1); 
  4.     resolve(3); 
  5.     // Promise.resolve().then(()=> console.log(4)) 
  6. }).then(num => { 
  7.     console.log(num) 
  8. }); 
  9. console.log(2); 

这个时候,我们就可以回答为什么上面代码的输出顺序是123,而不是132了。***点可以肯定的是1是***输出的,因为new一个Promise之后,传给它的resolver同步执行,所以1***打印。执行了resolve(3)之后,就会把当前Promiser对象的state改成完成态,并记录结果outcome。然后跳出来执行then,把传给then的成功回调给immediate在nextTick执行,而nextTick是使用Mutation异步执行的,所以3会在2之后输出。

如果在promise里面再写一个promsie的话,由于里面的promise的then要比外面的promise的then先执行,也就是说它的nextTick更先注册,所以4是在3之前输出。

这样基本上就解释了Promise的执行顺序的问题。但是我们还没说它的nextTick是怎么实现的,上面代码在执行immediate的时候把成功回调push到一个全局的数组queue里面,而nextTick是把这些回调按顺序执行,如下代码所示:

 
 
 
 
  1. function nextTick() { 
  2.   draining = true; 
  3.   var i, oldQueue; 
  4.   var len = queue.length; 
  5.   while (len) { 
  6.     oldQueue = queue; 
  7.     // 把queue清空 
  8.     queue = []; 
  9.     i = -1; 
  10.     // 执行当前所有回调 
  11.     while (++i < len) { 
  12.       oldQueue[i](); 
  13.     } 
  14.     len = queue.length; 
  15.   } 
  16.   draining = false; 

它会先把排水的变量draining设置成true,然后处理完成之后再设置成false,我们再回顾一下刚刚执行immediate的判断:

 
 
 
 
  1. function immediate(task) { 
  2.   if (queue.push(task) === 1 && !draining) { 
  3.     scheduleDrain(); 
  4.   } 

由于JS是单线程的,所以我觉得这个draining的变量判断好像没有太大的必要。另外一个判断,当queue为空时,push一个变量进来,这个时候queue只有1个元素,返回值就为1。所以如果之前已经push过了,那么这里就不用再触发nextTick,因为***次的push会把所有queue回调元素都执行的,只要保证后面的操作有被push到这个queue里面就好了。所以这个判断是一个优化。

另外,es6-promise的核心代码是一样的,只是它把immediate函数改成asap(as soon as possible),它也是优先使用Mutation.

还有一个问题,上面说的resolver的代码是同步,但是我们经常用Promise是用在异步的情况,resolve是异步调的,不是像上面同步调的,如:

 
 
 
 
  1. let resolver = function(resolve) { 
  2.     setTimeout(() => { 
  3.         // 异步调用resolve 
  4.         resolve(); 
  5.     }, 2000); 
  6.     // resolver执行完了还没执行resolve 
  7. }; 
  8. new Promise(resolver).then(num => console.log(num)); 

这个时候,同步执行完resolver,但还没执行resolve,所以在执行then的时候这个Promise的state还是pending的,就会走到134的代码(刚刚执行的是132行的unwrap):

它会创建一个QueueItem然后放到当前Promise对象的queue属性里面(注意这里的queue和上面说的immediate里全局的queue是两个不同的变量)。然后异步执行结束调用resolve,这个时候queue不为空了:

就会执行queue队列里面的成功回调。因为then是可以then多次的,所以成功回调可能会有多个。它也是调用immediate,在nextTick的时候执行的。

也就是说如果是同步resolve的,是通过MutationObserver/Setimeout 0之类的方式在当前的代码单元执行完之后立刻执行成功回调;而如果是异步resolve的,是先把成功回调放到当前Promise对象的一个队列里面,等到异步结束了执行resolve的时候再用同样的方式在nextTick调用成功回调。

我们还没说失败的回调,但大体是相似的。

【本文是专栏作者“人人网FED”的原创稿件,转载请通过联系原作者获取授权】

戳这里,看该作者更多好文

当前名称:从一道Promise执行顺序的题目看Promise实现
标题来源:http://www.hantingmc.com/qtweb/news17/492317.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联