zl程序教程

您现在的位置是:首页 >  后端

当前栏目

JS魔法堂:剖析源码理解Promises/A规范

JS源码 理解 剖析 规范 魔法 promises
2023-09-14 08:58:00 时间

一、前言

  Promises/A是由CommonJS组织制定的异步模式编程规范,有不少库已根据该规范及后来经改进的Promises/A+规范提供了实现

  如Q, Bluebird, when, rsvp.js, mmDeferred, jQuery.Deffered()等。

  虽然上述实现库均以Promises/A+规范作为实现基准,但由于Promises/A+是对Promises/A规范的改进和增强,因此深入学习Promises/A规范也是不可缺少的。

  本文内容主要根据以下内容进行学习后整理而成,若有纰漏请各位指正,谢谢。

  https://www.promisejs.org/

  http://wiki.commonjs.org/wiki/Promises/A

  由于篇幅较长特设目录一坨

  二、从痛点出发

  三、从感性领悟

  四、Promises/A的API规范

  五、通过示例看特性

  六、官方实例的源码剖析

     1. 基础功能部分(1.1. 构造函数; 1.2. then函数的实现)

     2. 辅助功能部分(2.1. Promise.resolve实现; 2.2. Promise.reject实现; 2.3. Promise.all实现; 2.4. Promise.race实现)

  七、总结

  八、参考

二、从痛点出发

  js中最常见的异步编程方式我想应该非回调函数不可了,优点是简单明了。但如果要实现下面的功能——非连续移动的动画效果,那是否还那么简单明了呢?

var left = function(cb){

 el.style.left = (el.offsetLeft + 200) + px;

 cb cb();

var el = document.getElementById(test);

// 臭名远播的“回调地狱(callback hell)”

setTimeout(function(){

 left(function(){

 setTimeout(function(){

 left(function(){

 setTimeout(function(){

 left();

 },2000);

 }, 2000);

}, 2000);
  傻眼了吧!下面我们看一下使用Promises/A规范异步模式的编码方式吧!
var el = document.getElementById(test);

// 定义thenable对象

var thenable = {then: function(resolve, reject){

 setTimeout(function(){

 el.style.left = (el.offsetLeft + 200) + px;

 // 触发promise对象链中的下一个状态为pending的promise对象的状态变为fulfilled

 resolve resolve(); 

 }, 2000);

// 将thenable对象封装为promise对象

var promise = Promise.resolve(thenable);

// 订阅promise对象状态变化时所触发的事件

promise

 .then(function(){

 return thenable;

 .then(function(){

 return thenable;

 });
 也许你有着“我不入地狱谁入地狱”的豪情,但如果现在需求改为循环10次执行非连续移动呢?20次呢?这时我想连地狱的门在哪也难以找到了,但Promises/A的方式让我们轻松应对。
var el = document.getElementById(test);

var thenable = {then: function(resolve, reject){

 setTimeout(function(){

 el.style.left = (el.offsetLeft + 200) + px;

 resolve resolve();

 }, 2000);

var promise = Promise.resolve(thenable);

var i = 0, count = 10;

while (++i 10){

 promise = promise.then(function(){

 return thenable;

}

三、 从感性领悟

  也许通过上述代码我们已经了解到Promise可以让我们以不同的方式编写异步代码,但也仅仅停留在按形式套代码的层面上而已,只有从感性出发理解Promise的设计思想我想才能更好地掌握。以下内容是读张鑫旭的《es6-javascript-promise-感性认知》和《js算法与思维方式》后的所思所想。

  首先举一个生活的例子,看看实际生活的处理逻辑和通过代的处理逻辑有什么出入吧!

  例子:下班搭车去接小孩回家。


  直觉思维分解上述句子会得出以下任务及顺序:下班- 搭车- 到幼儿园(小学等)接小孩- (走路)回家。可以看到这种思维方式是任务+执行顺序的,丝毫没有带任务间的时间距离。于是同步代码可以写成:

var 下班 = function(){};

var 搭车 = function(){};

var 接小孩 = function(){};

var 回家 = function(){};

下班();

搭车();

接小孩();

回家();
   但实际执行时各任务均有可能因某些原因出现不同程度的延时,如下班时老板突然安排一项新任务(推迟10分钟下班),错过班车(下一班要等10分钟),小孩 正在搞卫生(20分钟后搞完),回家。真实生活中我们能做的就是干等或做点其他事等到点后再继续之前的流程!但程序中尤其是像JS这样单线程程序是“等” 不起的,于是出现异步模式:
var 下班 = function(nextTask){};

var 搭车 = function(nextTask){};

var 接小孩 = function(nextTask){};

var 回家 = function(nextTask){};

下班(function(){

 setTimeout(function(){

 搭车(function(){

 setTimeout(function(){

 接小孩(function(){

 setTimeout(function(){

 回家(); 

 }, 20*60*1000);

 }, 10*60*1000);

 }, 10*60*1000);

});

   当回头再看上面这段代码时,会发现整个流程被任务间的时间距离拉得很远“下班------------搭车------------接小孩 -------------------回家”。回想一下真实生活中我们即使执行每个任务时均需要等待,但整个流程的抽象也只是“下班,等,搭车,等,接 小孩,等,回家”。因此回调函数的异步模式与我们的思维模式相距甚远,那么如何做到即告诉程序任务间的时间距离,又从代码结构上淡化这种时间距离感呢?而 Promise就是其中一种方式了!

  从开发者角度(第三人称)来看Promise作为任务间的纽带存在,流程被抽象为“下班,promise,搭车,promise,接小 孩,promise,回家”,而任务间的时间距离则归并到任务本身而已。从程序执行角度(第一人称)来看Promise为一个待定变量,但结果仅有两种 ——成功和失败,于是仅对待定变量设定两种结果的处理方式即可。

// 任务定义部分

var 下班 = function(){};

下班.then = function(resovle){ 

 setTimeout(function(){

 resovle();

 }, 10*60*1000);

var 搭车 = function(){};

搭车.then = function(resovle){ 

 setTimeout(function(){

 resovle();

 }, 10*60*1000);

var 接小孩 = function(){};

接小孩.then = function(resovle){ 

 setTimeout(function(){

 resovle();

 }, 20*60*1000);

var 回家 = function(){};

// 流程部分

var p = new Promise(function(resolve, reject){

 resolve();

p.then(function(){

 下班();

 return 下班;

 .then(function(){

 搭车();

 return 搭车;

 .then(function(){

 接小孩();

 return 接小孩;

 .then(function(){

 回家();

 });

 看代码结构被拉平了,但代码结构的变化是表象,最根本的是任务间的时间距离被淡化了,当我们想了解工作流程时不会被时间距离分散注意力,当我们想知道各个任务的延时时只需查看任务定义本身即可,这就是关注点分离的一种表现哦!

 

四、Promises/A的API规范

  经过上述示例我想大家已经尝到了甜头,并希望掌握这一武器从而逃离回调地狱的折磨了。下面就一起了解Promise及其API规范吧!

  1. 有限状态机

    Promise(中文:承诺)其实为一个有限状态机,共有三种状态:pending(执行中)、fulfilled(执行成功)和rejected(执行失败)。

    其中pending为初始状态,fulfilled和rejected为结束状态(结束状态表示promise的生命周期已结束)。

    状态转换关系为:pending- fulfilled,pending- rejected。

    随着状态的转换将触发各种事件(如执行成功事件、执行失败事件等)。  

  2. 构造函数

    Promise({Function} factory/*({Function} resolve, {Function} reject)*/) ,构造函数存在一个Function类型的入参factory,作为唯一一个修改promise对象状态的地方,其中factory函数的入参resolve的作用是将promise对象的状态从pending转换为fulfilled,而reject的作用是将promise对象的状态从pending转换为rejected。

    入参 void resolve({Any} val)  , 当val为非thenable对象和promise对象时则会将val作为执行成功事件处理函数的入参,若val为thenable对象时则会执行 thenable.then方法,若val为Promise对象时则会将该Promise对象添加到Promise对象单向链表中。

    入参 void reject({Any} reason) ,reason不管是哪种内容均直接作为执行失败事件处理函数的入参。

    注意:关于抛异常的做法,同步模式为 throw new Error("Im synchronous way!") ,而Promise规范的做法是 reject(new Error("Im asynchronous way!")); 

  3. 实例方法

     Promise then([{Function} onFulfilled[, {Function} onRejected]]) , 用于订阅Promise对象状态转换事件,入参onFulfilled为执行成功的事件处理函数,入参onRejected为执行失败的事件处理函数。两 者的返回值均作为Promise对象单向链表中下一个Promise对象的状态转换事件处理函数的入参。而then方法的返回值是一个新的Promise 对象并且已添加到Promise对象单向链表的末尾。

     Promise catch({Function} onRejected) ,相当于 then(null, onRejected) 。

  4. 类方法

     Promise Promise.resolve({Any} obj) ,用于将非Promise类型的入参封装为Promise对象,若obj为非thenable对象则返回状态为fulfilled的Promise对象,对于非若入参为Promise对象则直接返回。

     Promise Promise.reject({Any} obj) ,用于将非Promise类型的入参封装为状态为rejected的Promise对象。

     Promise Promise.all({Array} array) , 当array中所有Promise实例的状态均为fulfilled时,该方法返回的Promise对象的状态也转为fulfilled(执行成功事件处 理函数的入参为array数组中所有Promise实例执行成功事件处理函数的返回值),否则转换为rejected。

     Promise Promise.race({Array} array) , 当array中所有Promise实例的状态出现fulfilled时,该方法返回的Promise对象的状态也转为fulfilled(执行成功事件处 理函数的入参为状态为fulfilled的Promise实例执行成功事件处理函数的返回值),否则转换为rejected。

  5. thenable对象

    拥有 then方法 的对象均称为thenable对象,并且thenable对象将作为Promise对象被处理。

 

五、通过示例看特性  

  单看接口API是无法掌握Promise/A的特性的,下面通过示例说明:


// 订阅p2的执行成功事件处理函数,并创建Promise实例p3 // 该处理函数返回一个Promise实例,并1秒后该Promise实例的状态转换为rejected var p3 = p2.then(function(val){ console.log(val); var tmp = new Promise(function(resolve, reject){ setTimout(function(){ reject(new Error(my error!)); }, 1000); return tmp; // 订阅p3的执行成功事件处理函数,并创建Promise实例p4 // 由于p2的处理函数所返回的Promise实例状态为rejected,因此p3的执行成功事件处理函数将不被执行,并且p3没有执行失败事件处理函数,因此会将控制权往下传递给p4的执行失败事件处理函数。 var p4 = p3.then(function(val){ console.log(skip); // 订阅p4的执行成功事件处理函数,并创建Promise实例p5 var p5 = p4.catch(function(reason){ console.log(reason); });、

该示例的结果为:hello1     hello1    hello2    hello2    error:my error!。

示例2——事件处理函数晚绑定,同样可被触发



var p1 = new Promise(function(resolve, reject){

 resolve(hello);

// Promise实例p1状态转换为fulfilled一秒后才绑定事件处理函数

setTimeout(function(){

 p1.then(function(val){

 console.log(val);

}, 1000);


该示例的结果为: hello

 

六、官方实现的源码剖析

由于Promises/A规范实际仅提供接口定义,并没有规定具体实现细节,因此我们可以先自行作实现方式的猜想。

上述的示例1表明Promise是具有链式操作,因此Promise的内部结构应该是一个单向链表结构,每个节点除了自身数据外,还有一个字段用于指向下一个Promise实例。


构造函数的具体实现可以是这样的

var Promise = exports.Promise = function(fn){

 if (!(this instanceof iPromise))

 return new iPromise(fn);

 var _ = this._ = {};

 _.state = 0; // 0:pending, 1:fulfilled, 2:rejected

 _.thenables = []; // 单向链表

 fn fn(this.resolve.bind(this), this.reject.bind(this));

};
而then函数的具体实现为
Promise.prototype.then = function(fulfilledHandler, rejectedHandler){

 var _ = this._;

 var promise = new Promise();

 // 单向链表的逻辑结构

 var thenable = {

 onFulfilled: fulfilledHandler, // 执行成功的事件处理函数

 onRejected: rejectedHandler, // 执行失败的事件处理函数

 promise: promise // 下一个Promise实例

 _.thenables.push(thenable);

 if (_.state !== 0){

 window[window.setImmediate ? setImmediate : setTimeout].call(window, function(){

 handle(_);

 }, 0);

 return promise;

};

 但官方提供的实现方式却比上述思路晦涩得多(源码含适配nodejs和浏览器端的额外代码干扰视线,因此我提取可直接在浏览器上使用的主逻辑部分出来,具体代码请浏览:https://github.com/fsjohnhuang/iPromise/blob/master/theory/PromisesA/promise-6.0.0-browser.js)

 1. 基础功能部分

    基础功能部分主要分为 构造函数 和 then函数的实现 两部分,而 then函数的实现是理解的难点


var Promise = exports.Promise = function (fn) {

 if (typeof this !== "object") throw new TypeError("Promises must be constructed via new");

 if (typeof fn !== "function") throw new TypeError("not a function");

 var state = null; // 状态,null:pending,true:fulfilled,false:rejected

 var value = null; // 当前promise的状态事件处理函数(onFulfilled或onRejected)的入参

 var deferreds = []; // 当前promise的状态事件处理函数和promise链表中下一个promise的状态转换发起函数

 var self = this;

 // 唯一的公开方法

 this.then = function(onFulfilled, onRejected) {

 return new self.constructor(function(resolve, reject) {

 handle(new Handler(onFulfilled, onRejected, resolve, reject));

 // 保存和执行deferreds数组中的元素

 function handle(deferred) {

 if (state === null) {

 deferreds.push(deferred);

 return;

 // asap的作用为将入参的操作压入event loop队列中

 asap(function() {

 var cb = state ? deferred.onFulfilled : deferred.onRejected;

 if (cb === null) {

 (state ? deferred.resolve : deferred.reject)(value);

 return;

 var ret;

 try {

 // 执行当前promise的状态转换事件处理函数

 ret = cb(value);

 } catch (e) {

 // 修改promise链表中下一个promise对象的状态为rejected

 deferred.reject(e);

 return;

 // 修改promise链表中下一个promise对象的状态为fulfilled

 deferred.resolve(ret);

 // promise的状态转换发起函数,触发promise的状态从pending- fulfilled

 function resolve(newValue) {

 try {

 if (newValue === self) throw new TypeError("A promise cannot be resolved with itself.");

 if (newValue (typeof newValue === "object" || typeof newValue === "function")) {

 var then = newValue.then;

 if (typeof then === "function") {

 // 将控制权移交thenable和promise对象,由它们来设置当前pormise的状态和状态转换事件处理函数的实参

 doResolve(then.bind(newValue), resolve, reject);

 return;

 state = true;

 value = newValue;

 finale();

 } catch (e) {

 reject(e);

 // promise的状态转换发起函数,触发promise的状态从pending- rejected

 function reject(newValue) {

 state = false;

 value = newValue;

 finale();

 // 向链表的下一个promise移动

 function finale() {

 for (var i = 0, len = deferreds.length; i len; i++) handle(deferreds[i]);

 deferreds = null;

 // 执行构造函数的工厂方法,由工厂方法触发promise的状态转换

 doResolve(fn, resolve, reject);

 }
     我们可以通过 new Promise(function(resolve, reject){ resolve(hello); }); 来跟踪一下执行过程,发现重点在 doResolve(fn, resolve, reject) 方法调用中,该方法定义如下:
// 对状态转换事件处理函数进行封装后,再传给执行函数

 function doResolve(fn, onFulfilled, onRejected) {

 // done作为开关以防止fn内同时调用resolve和reject方法

 var done = false;

 try {

 fn(function(value) {

 if (done) return;

 done = true;

 onFulfilled(value);

 }, function(reason) {

 if (done) return;

 done = true;

 onRejected(reason);

 } catch (ex) {

 if (done) return;

 done = true;

 onRejected(ex);

 }

     doResovle仅仅是对resolve和reject方法进行封装以防止同时被调用的情况而已,这时控制权到达 resolve方法 。由于resovle的入参为字符串类型,因此直接修改当前promise的状态和保存状态转换事件处理函数的实参即可(若resolve的入参为thenable对象或Promise对象,则将控制权交给该对象,由该对象来设置当前promise的状态和状态转换事件处理函数的实参),然后将控制权移交 finale方法 。finale方法内部会遍历deffereds数组并根据状态调用对应的处理函数和修改promise链表中下一个promise对象的状态。

     那么deffereds数组具体是什么呢?其实它就跟我之前猜想的thenables数组功能一致,用于保存状态转换事件处理函数和维护promise 单向链表(不直接存放下一个promise对象的指针,而是存放下一个promise的resovle和reject方法)的。具体数据结构如下:

// 构造promise的链表逻辑结构

function Handler(onFulfilled, onRejected, resolve, reject) {

 this.onFulfilled = typeof onFulfilled === "function" ? onFulfilled : null; // 当前promise的状态转换事件处理函数

 this.onRejected = typeof onRejected === "function" ? onRejected : null; // 当前promise的状态转换事件处理函数

 this.resolve = resolve; // 设置链表中下一个promise的状态为fulfilled

 this.reject = reject; // 设置链表中下一个promise的状态为rejected

}
    若当前promise有deffered实例,那么则会执行handle函数中asap函数的函数入参
function() {

 var cb = state ? deferred.onFulfilled : deferred.onRejected;

 if (cb === null) {

 (state ? deferred.resolve : deferred.reject)(value);

 return;

 var ret;

 try {

 // 执行当前promise的状态转换事件处理函数

 ret = cb(value);

 } catch (e) {

 // 修改promise链表中下一个promise对象的状态为rejected

 deferred.reject(e);

 return;

 // 修改promise链表中下一个promise对象的状态为fulfilled

 deferred.resolve(ret);

}
     我觉得原实现方式不够直白,于是改成这样:
function(){

 var cb = deferred[state ? onFulfilled : onRejected];

 var deferredAction = resolve, ret;

 try{

 ret = cb ? cb(value) : value;

 catch (e){

 ret = e;

 deferredAction = reject;

 deferred[deferredAction].call(deferred, ret);

}

 文字太多了,还是看图更清楚哦!

 

   接下来的问题就是deffereds数组的元素是从何而来呢?那就要看看then函数了。

    1.2. then函数的实现


this.then = function(onFulfilled, onRejected){ return new self.constructor(function(resolve, reject) { handle(new Handler(onFulfilled, onRejected, resolve, reject));       为了好看些,我修改了一下格式:
this.then = function(onFulfilled, onRejected) {

 // 构造新的promise实例并返回,从而形成链式操作

 return new Promise(function(resolve, reject) {

 var handler = new Handler(onFulfilled, onRejected, resolve, reject);

 注意:这里利用了闭包特性,此处的handle并不是新Promise的handle函数,而是this.then所属promise的handle函数。

 因此handler将被添加到this.then所属promise的deffereds数组中。

 而onFulfilled和onRejected自然成为了this.then所属promise的状态转换事件处理函数,

 而resolve和reject依旧是新promise实例的状态转换触发函数。

 handle(handler);

};

    源码读后感:

      通过闭包特性来让链表后一个对象调用前一个对象的方法和变量,从而实现私有成员方法和属性实在是过瘾。比起我猜想的实现方式通过下划线(_)提示API调用者该属性下的均为私有成员的做法封装性更完整。

 2. 辅助功能部分

    辅助功能部分主要就是Promise.resolve、Promise.reject、Promise.all、Promsie.race的实现,它们均由基础功能扩展而来。

    2.1. Promise.resolve实现

      作用:将非Promise对象转换为Promise对象,而非Promise对象则被细分为两种:thenable对象和非thenable对象。

         thenable对象的then将作为Promise构造函数的工厂方法被调用

         非thenable对象(Number、DOMString、Boolean、null、undefined等)将作为pending- fulfilled的事件处理函数的入参。


     由于源码中加入性能优化的代码,因此我提出核心逻辑以便分析:

// 将非thenable对象构造为thenable对象

// 其then方法则返回一个真正的Promise对象

function ValuePromise(value) {

 this.then = function(onFulfilled) {

 if (typeof onFulfilled !== "function") return this;

 return new Promise(function(resolve, reject) {

 asap(function() {

 try {

 resolve(onFulfilled(value));

 } catch (ex) {

 reject(ex);

 也可以将非thenable对象构造为Promise对象

 function ValuePromise(value){

 return new Promise(function(resolve){

 resolve(value);

 Promise.resolve = function(value) {

 if (value instanceof Promise) return value;

 if (typeof value === "object" || typeof value === "function") {

 try {

 var then = value.then;

 if (typeof then === "function") {

 return new Promise(then.bind(value));

 } catch (ex) {

 return new Promise(function(resolve, reject) {

 reject(ex);

 return new ValuePromise(value);

 };

    2.2. Promise.reject实现

         作用:创建一个状态为rejected的promise对象,且入参将作为onRejected函数的入参。

 Promise.reject = function(value) {

 return new Promise(function(resolve, reject) {

 reject(value);

 };

    2.3. Promise.all实现

         作用:返回的一个promise实例,且该实例当且仅当Promise.all入参数组中所有Promise元素状态均为fulfilled时该返回的 promise实例的状态转换为fulfilled(onFulfilled事件处理函数的入参为处理结果数组),否则转换为rejected。

Promise.all = function(arr) {

 var args = Array.prototype.slice.call(arr);

 return new Promise(function(resolve, reject) {

 if (args.length === 0) return resolve([]);

 var remaining = args.length;

 function res(i, val) {

 try {

 if (val (typeof val === "object" || typeof val === "function")) {

 var then = val.then;

 if (typeof then === "function") {

 then.call(val, function(val) {

 // 对于thenable和promise对象则订阅onFulfilled事件获取处理结果值

 res(i, val);

 }, reject);

 return;

 args[i] = val;

 // 检测是否所有入参都已返回值

 if (--remaining === 0) {

 resolve(args);

 } catch (ex) {

 reject(ex);

 for (var i = 0; i args.length; i++) {

 res(i, args[i]);

 };

    2.4. Promise.race实现

       作用:返回一个promise对象,且入参数组中一旦某个promise对象状态转换为fulfilled,则该promise对象的状态也转换为fulfilled。

Promise.race = function(values) {

 return new Promise(function(resolve, reject) {

 values.forEach(function(value) {

 // 将数组元素转换为promise对象

 Promise.resolve(value).then(resolve, reject);

};
    源 码实现的方式是即使第一个数组元素的状态已经为fulfilled,但仍然会订阅其他元素的onFulfilled和onRejected事件,依赖 resolve函数中的标识位done来保证返回的promise对象的onFulfilled函数仅执行一次。我修改为如下形式:



Promise.race = function(values){

 return new Promise(function(resolve, reject){

 var over = 0;

 for (var i = 0, len = values.length; i len !over; ++i){

 var val = values[i];

 if (val typeof val.then === function){

 val.then(function(res){

 !over++ resolve(res);

 }, reject);

 else{

 !over++ resolve(val);

 };


七、总结  

  虽然通过Promises/A规范进行异步编程已经舒坦不少,但该规范仍然不够给力,于是出现了Promises/A+规范。后面我们继续探讨Promises/A+规范吧!



JS的精髓,事件详解 鼠标单击 单击事件onclick,如点击某个按钮弹出一个提示框。这里要特别注意一点,单击事件不只是按钮才有,任何元素我们都可以为它添加单击事件!
JS查漏补缺——ECMAScript相关 JS查漏补缺系列是我在学习JS高级语法时做的笔记,通过实践费曼学习法进一步加深自己对其的理解,也希望别人能通过我的笔记能学习到相关的知识点。这一次我们来了解ECMAScript相关语法中的大小写和严格模式
js之JavaScript开发者应懂的33个概念 这个项目是为了帮助开发者掌握 JavaScript 概念而创立的。它不是必备,但在未来学习(JavaScript)中,可以作为一篇指南