动手实现Promise

前言

之前已经学习过了Promise的相关知识,Promise其实就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。一般的Promise长这个样子:

const promise = new Promise((resolve, reject) => {
    if(/* 一般是异步操作 */) {
        resolve('回调');
    } else {
        reject('error');
    } 
});

promise.then((data) => {
    console.log(data);
}).catch((error) => {
    console.log(error);
});

下面尝试一下自己去实现Promise的功能。

基本功能

// 定一个构造函数
function MyPromise(callBack: Function) {
  let thenFunc: ?Function = null;

  this.then = (call) => {
    thenFunc = call;
  };

  const thenCall = (value) => {
    thenFunc && thenFunc(value);
    thenFunc = null;
  };

  callBack(thenCall);
}

// 使用
const promise = new MyPromise((resolve) => {
    setTimeout(() => {
        resolve('ssss');
    }, 1000);
});

promise.then((data) => {
    console.log(data);
});

定义一个构造函数,将初始化的异步方法传入其中。在函数内部定一个变量来存储promise实例传递进来的then方法,当异步方法开始执行的时候就会在函数内部调用这个变量存储的函数,从将参数回调至promise实例中。

初始化的异步方法中,resolve参数其实是一个函数,对应到自定义的函数中就是thenCall方法,然后当resolve调用的时候,就是函数内部的用来存储then的变量执行的时候。

初始化的方法不是异步的

如果在初始化的时候,传入的不是一个异步的函数,比如下面的

const promise = new MyPromise((resolve) => {
    resolve('ssss');
});

由于执行顺序的问题,并不会回调到then方法中。可以想办法让then方法先执行,之后再执行resolve,利用Js的循环机制以及setTimeout,将resolve放入栈底执行

const thenCall = (value) => {
    setTimeout(() => {    
        thenFunc && thenFunc(value);
        thenFunc = null;
    }, 0);
};

如果对于Js的循环机制和setTimeout不是很了解的话可以参考这篇

这样就可以确保在有then的情况下,resolve在后面执行。

promise调用时间很晚

如果初始化Promise实例之后,并没有立即使用它,而是间隔了一段时间再去调用promise的then方法,那么由于resolve已经执行完毕,所以即使是调用过了then方法也不会正确的回调。

setTimeout(() => {
    promise.then((data) => {
        console.log(data);
    }).catch((err) => {
        console.log('error' + err);
    });  
}, 1000);

之前就已经介绍过了,Promise是有三种状态的:pendingfulfilledrejected。而且只能从pending转换为另外两种状态,而且是不可逆转的。可以借用Promise的这三种状态,让自己的Promise记住自己当前的状态。

添加一个记录Promise状态的变量,添加一个存储参数的变量

function MyPromise(callBack: Function) {
  let thenFunc: ?Function = null;
  let status: string = 'pending';
  let callValue: any = null;

  this.then = (call) => {
    if (status === 'pending') {
      thenFunc = call;
    }
    call(callValue);
  };

  const thenCall = (value) => {
    setTimeout(() => {    
      thenFunc && thenFunc(value);
      callValue = value;
      status = 'fulfilled';
      thenFunc = null;
    }, 0);
  };

  callBack(thenCall);
}

在Promise初始化的时候,Promise的初始状态为pending,执行过thenCall方法后,改变自身状态为fulfilled,并且将传递的参数暂时保存到callValue中。过一段时间后,then执行的时候,由于状态已经改变,则会直接执行then中传递的回调函数,并将callValue作为回调参数。

添加reject状态

上述的方法都是只有resolve一个状态,其实reject与resolve相同,仅仅是执行了不同的操作。

function MyPromise(callBack: Function) {
  let thenFunc: ?Function = null;
  let catchFunc: ?Function = null;
  let status: string = 'pending';
  let callValue: any = null;

  this.then = (call) => {
    if (status === 'pending') {
      thenFunc = call;
    } else if (status === 'fulfilled') {
      call(callValue);
    }
    return this;
  };

  this.catch = (call) => {
    if (status === 'pending') {
      catchFunc = call;
    } else if (status === 'rejected') {
      call(callValue);
    }
    return this;
  };

  const thenCall = (value) => {
    setTimeout(() => {    
      thenFunc && thenFunc(value);
      callValue = value;
      status = 'fulfilled';
      thenFunc = null;
    }, 0);
  };

  const catchCall = (value) => {
    setTimeout(() => {    
      catchFunc && catchFunc(value);
      callValue = value;
      status = 'rejected';
      catchFunc = null;
    }, 0);
  };

  callBack(thenCall, catchCall);
}


const promise = new MyPromise((resolve, reject) => {
    setTimeout(() => {
        reject('ssss');
    }, 1000);
});

promise.then((data) => {
    console.log(data);
}).catch((err) => {
    console.log('error' + err);
});  

关于return this,其实是将对象本身返回以供后续的catch或者其他操作,如果没有那么会报错说没有then方法或者没有catch方法。

链式调用

自身调用

Promise可以通过不停地返回Promise对象从而实现链式调用

promise.then().then().then().....

由于MyPromise内部是由一个变量来保存的回调函数,只能保存最新的then方法,要是想实现自身的链式调用,那么就需要将其修改为数组,把每一个then传入的回调函数保存起来。

function MyPromise(callBack: Function) {
  let thenFunc: Array<Function> = [];
  let catchFunc: ?Function = null;
  let status: string = 'pending';
  let callValue: any = null;

  this.then = (call) => {
    if (status === 'pending') {
      thenFunc.push(call);
    } else if (status === 'fulfilled') {
      call(callValue);
    }
    return this;
  };

  const thenCall = (value) => {
    setTimeout(() => {    
      callValue = value;
      status = 'fulfilled';
      thenFunc.forEach((fun) => {
        fun && fun(value);
      });
    }, 0);
  };

    ....
    ....
    
  callBack(thenCall, catchCall);
}

串行promise

更加常见的需求是不同的Promise类型需要在同一个方法中链式调用

const promise = new MyPromise((resolve, reject) => {
    setTimeout(() => {
        resolve('ssss');
    }, 100);
});

promise.then((data) => {
    console.log('11' + data);
    return new MyPromise((resolve) => {
        setTimeout(() => {
            resolve('dddd');
        }, 100);  
    });
}).then((data) => {
    console.log('22' + data);
}).catch((err) => {
    console.log('error' + err);
});  

// 控制台输入的结果为
// 11ssss
// 22ssss
// 并不是预期的
// 11ssss
// 22dddd

那么如何让后续的then方法,返回的是最新初始化的promise呢?首先考虑的位置就是MyPromise中的then方法。因为返回了一个新的promise,而不是像之前的返回this,所以就没有办法处理新的promise里面的内容。那么就应该区分出来,then方法里是否返回了一个新的promise。

this.then = (call) => {
    const ret = typeof call === 'function' && call(callValue);
    if (ret && ret.then && typeof ret.then === 'function') {
        // 返回了一个新的promise
    } else {
        // 正常处理
    }
};

在then方法中,首先让传入的回调方法执行,观察它的返回值是否是一个对象并且里面包含着then方法。如果有的话,该回调函数就返回了一个新的pormise。

上述的方法确实可以判断出来是否传入了新的promise,但是要明确的一点是,then方法中并不是回调函数执行的时候,回调函数是否执行是由thenCall控制。以上的逻辑判断,需要传入thenFunc数组中存储起来,在thenCall中执行。

const handlePromise = (call: Function, value) => {
    const ret = typeof call === 'function' && call(value);
    if (ret && ret.then && typeof ret.then === 'function') {
        ret.then((data) => {
            call(data);
        })
    }
};

this.then = (call) => {
    if (status === 'pending') {
        thenFunc.push((value) => { handlePromise(call, value); });
    } else if (status === 'fulfilled') {
        call(callValue);
    }
    return this;
};
`将handlePromise传入thenFunc数组中,当其执行的时候,会判断then传入回调函数的返回值。如果是一个promise对象,那么就执行该promise的then函数`
// 控制台输出结果为
// 11ssss
// 22ssss
// 11dddd

控制台输出并不是预期的结果,但是好消息是出现第二个promise的数据。那么问题在哪里呢,为什么会输出三个结果?

// 当第一个then执行的时候,执行到
const ret = typeof call === 'function' && call(value);
// 因为调用了call(value);
// 在控制台打印了11ssss
// 之后判断出返回了一个promise对象,执行到
ret.then((data) => {
    call(data);
})
// 这里会执行第二个promise的异步函数
setTimeout(() => {
    resolve('dddd');
}, 100);  
// 即在100ms后,回调dddd,注意这里面仍然是在第一个then中回调!
// 执行到这里的时候控制台并没有输出11dddd,而是先输出了22ssss
// 因为是一个100ms延迟的异步函数,在调用的时候会直接执行第二个then
// 此时MyPromise中的value值仍然是ssss,所以会直接输出22ssss
// 100ms后输出11dddd

到了第二次then方法的时候,仍然是第一个值,原因就是return this。仍然返回了本身的promise,而不是第二个promise中的内容。那么可以考虑在then中每次都返回一个新的promise,用来接收新的值。

const handlePromise = (call: Function, resolve: Function) => {
    const ret = typeof call === 'function' && call(callValue);
    if (ret && ret.then && typeof ret.then === 'function') {
        ret.then((value) => {
            resolve(value);
        });
    } else {
        resolve(ret);
    }
};

this.then = (call) => {
    return new MyPromise((resolve) => {
        if (status === 'pending') {
            thenFunc.push(() => { handlePromise(call, resolve); });
        } else if (status === 'fulfilled') 
            call(callValue);
    }    
  });
};

由于在then中构建了一个新的promise,在每次then中都会调用resolve来改变MyPromise中value的值,确保在下一次的then中获取到新的值。

以上即可在promise中实现链式调用。

相关API

promise.race()

Promise的race方法其实是将几个promise一起执行,首先回调的promise会做为race方法的回调值。

MyPromise.race = (raceList: Array<MyPromise>) => {
  return new MyPromise((resolve) => {
    let count = 0;
    for (let i = 0; i < raceList.length; i++) {
      raceList[i].then((value) => {
        if (count === 0) {
          count++;
          resolve(value);
        }
      });
    }  
  });
};

promise.all()

Promise的all方法是将几个promise一起执行,当每个promise返回成功的时候,才会将所有的结果组合成一个数组返回到结果中。

MyPromise.all = (allList: Array<MyPromise>) => {
  return new MyPromise((resolve) => {
    let dataList = [];
    for (let i = 0; i < allList.length; i++) {
      allList[i].then((value) => {
        dataList.push(value);
        if (dataList.length == allList.length) {
          resolve(dataList);
        }
      });
    }  
  });
};

小结

以上是全部关于自己实现promise的内容,仅仅实现了promise的一部分内容。实现起来还是有一点吃力的,尤其是关于链式调用的实现,更加能理解关于函数编程的思想,总体来说实现的比较乱。网上其他大神都是用es5写的,和es6写法还是有一点出入的。使用es6实现还是比较简洁的,如果有任何问题请不吝赐教。

参考链接

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354

推荐阅读更多精彩内容

  • Promise 对象 Promise 的含义 Promise 是异步编程的一种解决方案,比传统的解决方案——回调函...
    neromous阅读 8,706评论 1 56
  • 你不知道JS:异步 第三章:Promises 在第二章,我们指出了采用回调来表达异步和管理并发时的两种主要不足:缺...
    purple_force阅读 2,066评论 0 4
  • 前言 本文旨在简单讲解一下javascript中的Promise对象的概念,特性与简单的使用方法。并在文末会附上一...
    _暮雨清秋_阅读 2,197评论 0 3
  • //本文内容起初摘抄于 阮一峰 作者的译文,用于记录和学习,建议观者移步于原文 概念: 所谓的Promise,...
    曾经过往阅读 1,238评论 0 7
  • 特别说明,为便于查阅,文章转自https://github.com/getify/You-Dont-Know-JS...
    杀破狼real阅读 885评论 0 2