本文是一块儿学习造轮子系列的第一篇,本篇咱们将从零开始写一个符合Promises/A+规范的promise,本系列文章将会选取一些前端比较经典的轮子进行源码分析,而且从零开始逐步实现,本系列将会学习Promises/A+,Redux,react-redux,vue,dom-diff,webpack,babel,kao,express,async/await,jquery,Lodash,requirejs,lib-flexible等前端经典轮子的实现方式,每一章源码都托管在github上,欢迎关注~
相关系列文章:
一块儿学习造轮子(一):从零开始写一个符合Promises/A+规范的promise
一块儿学习造轮子(二):从零开始写一个Redux
一块儿学习造轮子(三):从零开始写一个React-Redux
本系列github仓库:
一块儿学习造轮子系列github(欢迎star~)前端
Promise 是异步编程的一种解决方案,比传统的解决方案回调函数和事件更合理更强大。它由社区最先提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。本篇不注重讲解promise的用法,关于用法,能够看阮一峰老师的ECMAScript 6系列里面的Promise部分:
vue
本篇主要讲解如何从零开始一步步的实现promise各项特性及功能,最终使其符合Promises/A+规范,由于讲解较细,因此文章略长。 另外,每一步的项目源码都在github上,能够对照参考,每一步都有对应的项目代码及测试代码,喜欢的话,欢迎给个star~
react
项目地址:本文代码的github仓库jquery
本文promise里用到的异步操做的示例都是使用的node里面的fs.readFile方法,在浏览器端可使用setTimeout方法进行模拟异步操做。
webpack
function MyPromise(fn) {
let self = this; // 缓存当前promise实例
self.value = null; //成功时的值
self.error = null; //失败时的缘由
self.onFulfilled = null; //成功的回调函数
self.onRejected = null; //失败的回调函数
function resolve(value) {
self.value = value;
self.onFulfilled(self.value);//resolve时执行成功回调
}
function reject(error) {
self.error = error;
self.onRejected(self.error)//reject时执行失败回调
}
fn(resolve, reject);
}
MyPromise.prototype.then = function(onFulfilled, onRejected) {
//在这里给promise实例注册成功和失败回调
this.onFulfilled = onFulfilled;
this.onRejected = onRejected;
}
module.exports = MyPromise
复制代码
代码很短,逻辑也很是清晰,在then中注册了这个promise实例的成功回调和失败回调,当promise reslove时,就把异步执行结果赋值给promise实例的value,并把这个值传入成功回调中执行,失败就把异步执行失败缘由赋值给promise实例的error,并把这个值传入失败回调并执行。
git
基础版本代码
es6
咱们知道,咱们在使用es6 的promise时,能够传入一个异步任务,也能够传入一个同步任务,可是咱们的上面基础版代码并不支持同步任务,若是咱们这样写就会报错:github
let promise = new Promise((resolve, reject) => {
resolve("同步任务执行")
});
复制代码
为何呢?由于是同步任务,因此当咱们的promise实例reslove时,它的then方法还没执行到,因此回调函数还没注册上,这时reslove中调用成功回调确定会报错的。
web
使promise支持同步方法
function resolve(value) {
//利用setTimeout特性将具体执行放到then以后
setTimeout(() => {
self.value = value;
self.onFulfilled(self.value)
})
}
function reject(error) {
setTimeout(() => {
self.error = error;
self.onRejected(self.error)
})
}
复制代码
实现很简单,就是在reslove和reject里面用setTimeout进行包裹,使其到then方法执行以后再去执行,这样咱们就让promise支持传入同步方法,另外,关于这一点,Promise/A+规范里也明确要求了这一点。
2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code.
咱们知道在使用promise时,promise有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操做的结果,能够决定当前是哪种状态,任何其余操做都没法改变这个状态。另外,promise一旦状态改变,就不会再变,任什么时候候均可以获得这个结果promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种状况发生,状态就凝固了,不会再变了,会一直保持这个结果,若是改变已经发生了,你再对promise对象添加回调函数,也会当即获得这个结果。
//定义三种状态
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
function MyPromise(fn) {
let self = this;
self.value = null;
self.error = null;
self.status = PENDING;
self.onFulfilled = null;
self.onRejected = null;
function resolve(value) {
//若是状态是pending才去修改状态为fulfilled并执行成功逻辑
if (self.status === PENDING) {
setTimeout(function() {
self.status = FULFILLED;
self.value = value;
self.onFulfilled(self.value);
})
}
}
function reject(error) {
//若是状态是pending才去修改状态为rejected并执行失败逻辑
if (self.status === PENDING) {
setTimeout(function() {
self.status = REJECTED;
self.error = error;
self.onRejected(self.error);
})
}
}
fn(resolve, reject);
}
MyPromise.prototype.then = function(onFulfilled, onRejected) {
if (this.status === PENDING) {
this.onFulfilled = onFulfilled;
this.onRejected = onRejected;
} else if (this.status === FULFILLED) {
//若是状态是fulfilled,直接执行成功回调,并将成功值传入
onFulfilled(this.value)
} else {
//若是状态是rejected,直接执行失败回调,并将失败缘由传入
onRejected(this.error)
}
return this;
}
module.exports = MyPromise
复制代码
首先,咱们创建了三种状态"pending","fulfilled","rejected",而后咱们在reslove和reject中作判断,只有状态是pending时,才去改变promise的状态,并执行相应操做,另外,咱们在then中判断,若是这个promise已经变为"fulfilled"或"rejected"就马上执行它的回调,并把结果传入。
咱们平时写promise通常都是对应的一组流程化的操做,如这样:
promise.then(f1).then(f2).then(f3)
可是咱们以前的版本最多只能注册一个回调,这一节咱们就来实现链式操做。
使promise支持链式操做
想支持链式操做,其实很简单,首先存储回调时要改成使用数组
self.onFulfilledCallbacks = [];
self.onRejectedCallbacks = [];
复制代码
固然执行回调时,也要改为遍历回调数组执行回调函数
self.onFulfilledCallbacks.forEach((callback) => callback(self.value));
复制代码
最后,then方法也要改一下,只须要在最后一行加一个return this便可,这其实和jQuery链式操做的原理一致,每次调用完方法都返回自身实例,后面的方法也是实例的方法,因此能够继续执行。
MyPromise.prototype.then = function(onFulfilled, onRejected) {
if (this.status === PENDING) {
this.onFulfilledCallbacks.push(onFulfilled);
this.onRejectedCallbacks.push(onRejected);
} else if (this.status === FULFILLED) {
onFulfilled(this.value)
} else {
onRejected(this.error)
}
return this;
}
复制代码
咱们上一节实现了链式调用,可是目前then方法里只能传入同步任务,可是咱们日常用promise,then方法里通常是异步任务,由于咱们用promise主要用来解决一组流程化的异步操做,以下面这样的调取接口获取用户id后,再根据用户id调取接口获取用户余额,获取用户id和获取用户余额都须要调用接口,因此都是异步任务,如何使promise支持串行异步操做呢?
getUserId()
.then(getUserBalanceById)
.then(function (balance) {
// do sth
}, function (error) {
console.log(error);
});
复制代码
使promise支持串行异步操做
这里为方便讲解咱们引入一个常见场景:用promise顺序读取文件内容,场景代码以下:
let p = new Promise((resolve, reject) => {
fs.readFile('../file/1.txt', "utf8", function(err, data) {
err ? reject(err) : resolve(data)
});
});
let f1 = function(data) {
console.log(data)
return new Promise((resolve, reject) => {
fs.readFile('../file/2.txt', "utf8", function(err, data) {
err ? reject(err) : resolve(data)
});
});
}
let f2 = function(data) {
console.log(data)
return new Promise((resolve, reject) => {
fs.readFile('../file/3.txt', "utf8", function(err, data) {
err ? reject(err) : resolve(data)
});
});
}
let f3 = function(data) {
console.log(data);
}
let errorLog = function(error) {
console.log(error)
}
p.then(f1).then(f2).then(f3).catch(errorLog)
//会依次输出
//this is 1.txt
//this is 2.txt
//this is 3.txt
复制代码
上面场景,咱们读取完1.txt后并打印1.txt内容,再去读取2.txt并打印2.txt内容,再去读取3.txt并打印3.txt内容,而读取文件都是异步操做,因此都是返回一个promise,咱们上一节实现的promise能够实现执行完异步操做后执行后续回调,可是本节的回调读取文件内容操做并非同步的,而是异步的,因此当读取完1.txt后,执行它回调onFulfilledCallbacks里面的f1,f2,f3时,异步操做尚未完成,因此咱们本想获得这样的输出:
this is 1.txt
this is 2.txt
this is 3.txt
复制代码
可是实际上却会输出
this is 1.txt
this is 1.txt
this is 1.txt
复制代码
因此要想实现异步操做串行,咱们不能将回调函数都注册在初始promise的onFulfilledCallbacks里面,而要将每一个回调函数注册在对应的异步操做promise的onFulfilledCallbacks里面,用读取文件的场景来举例,f1要在p的onFulfilledCallbacks里面,而f2应该在f1里面return的那个Promise的onFulfilledCallbacks里面,由于只有这样才能实现读取完2.txt后才去打印2.txt的结果。
可是,咱们日常写promise通常都是这样写的: promise.then(f1).then(f2).then(f3)
,一开始全部流程咱们就指定好了,而不是在f1里面才去注册f1的回调,f2里面才去注册f2的回调。
如何既能保持这种链式写法的同时又能使异步操做衔接执行呢?咱们其实让then方法最后再也不返回自身实例,而是返回一个新的promise便可,咱们能够叫它bridgePromise,它最大的做用就是衔接后续操做,咱们看下具体实现代码:
MyPromise.prototype.then = function(onFulfilled, onRejected) {
const self = this;
let bridgePromise;
//防止使用者不传成功或失败回调函数,因此成功失败回调都给了默认回调函数
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value;
onRejected = typeof onRejected === "function" ? onRejected : error => { throw error };
if (self.status === FULFILLED) {
return bridgePromise = new MyPromise((resolve, reject) => {
setTimeout(() => {
try {
let x = onFulfilled(self.value);
resolvePromise(bridgePromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
})
}
if (self.status === REJECTED) {
return bridgePromise = new MyPromise((resolve, reject) => {
setTimeout(() => {
try {
let x = onRejected(self.error);
resolvePromise(bridgePromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});
}
if (self.status === PENDING) {
return bridgePromise = new MyPromise((resolve, reject) => {
self.onFulfilledCallbacks.push((value) => {
try {
let x = onFulfilled(value);
resolvePromise(bridgePromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
self.onRejectedCallbacks.push((error) => {
try {
let x = onRejected(error);
resolvePromise(bridgePromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});
}
}
//catch方法实际上是个语法糖,就是只传onRejected不传onFulfilled的then方法
MyPromise.prototype.catch = function(onRejected) {
return this.then(null, onRejected);
}
//用来解析回调函数的返回值x,x多是普通值也多是个promise对象
function resolvePromise(bridgePromise, x, resolve, reject) {
//若是x是一个promise
if (x instanceof MyPromise) {
//若是这个promise是pending状态,就在它的then方法里继续执行resolvePromise解析它的结果,直到返回值不是一个pending状态的promise为止
if (x.status === PENDING) {
x.then(y => {
resolvePromise(bridgePromise, y, resolve, reject);
}, error => {
reject(error);
});
} else {
x.then(resolve, reject);
}
//若是x是一个普通值,就让bridgePromise的状态fulfilled,并把这个值传递下去
} else {
resolve(x);
}
}
复制代码
首先,为防止使用者不传成功回调函数或不失败回调函数,咱们给了默认回调函数,而后不管当前promise是什么状态,咱们都返回一个bridgePromise用来衔接后续操做。
另外执行回调函数时,由于回调函数既可能会返回一个异步的promise也可能会返回一个同步结果,因此咱们把直接把回调函数的结果托管给bridgePromise,使用resolvePromise方法来解析回调函数的结果,若是回调函数返回一个promise而且状态仍是pending,就在这个promise的then方法中继续解析这个promise reslove传过来的值,若是值仍是pending状态的promise就继续解析,直到不是一个异步promise,而是一个正常值就使用bridgePromise的reslove方法将bridgePromise的状态改成fulfilled,并调用onFulfilledCallbacks回调数组中的方法,将该值传入,到此异步操做就衔接上了。
这里很抽象,咱们仍是以文件顺序读取的场景画一张图解释一下流程:
当执行p.then(f1).then(f2).then(f3)
时:
到此,reslove这一条线已经咱们已经走通,让咱们看看reject这一条线,reject其实处理起来很简单:
MyPromise.prototype.catch = function(onRejected) {
return this.then(null, onRejected);
}
复制代码
到此,咱们已经能够愉快的使用promise.then(f1).then(f2).then(f3).catch(errorLog)
来顺序读取文件内容了。
其实,到支持串行异步任务这一节,咱们写的promise在功能上已经基本齐全了,可是还不太规范,好比说一些其余状况的判断等等,这一节咱们就比着Promises/A+的规范打磨一下咱们写的promise。若是只是想学习promise的核心实现的,这一节看不懂也不要紧,由于这一节并无增长promise的功能,只是使promise更加规范,更加健壮。
使promise达到Promises/A+规范,经过promises-aplus-tests的完整测试
首先来能够了解一下Promises/A+规范:
Promises/A+规范原版
Promises/A+规范中文版
相比上一节代码,本节代码除了在resolvePromise函数里增长了几个其余状况的判断外,其余函数都没有修改。完整promise代码以下:
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
function MyPromise(fn) {
const self = this;
self.value = null;
self.error = null;
self.status = PENDING;
self.onFulfilledCallbacks = [];
self.onRejectedCallbacks = [];
function resolve(value) {
if (value instanceof MyPromise) {
return value.then(resolve, reject);
}
if (self.status === PENDING) {
setTimeout(() => {
self.status = FULFILLED;
self.value = value;
self.onFulfilledCallbacks.forEach((callback) => callback(self.value));
}, 0)
}
}
function reject(error) {
if (self.status === PENDING) {
setTimeout(function() {
self.status = REJECTED;
self.error = error;
self.onRejectedCallbacks.forEach((callback) => callback(self.error));
}, 0)
}
}
try {
fn(resolve, reject);
} catch (e) {
reject(e);
}
}
function resolvePromise(bridgepromise, x, resolve, reject) {
//2.3.1规范,避免循环引用
if (bridgepromise === x) {
return reject(new TypeError('Circular reference'));
}
let called = false;
//这个判断分支其实已经能够删除,用下面那个分支代替,由于promise也是一个thenable对象
if (x instanceof MyPromise) {
if (x.status === PENDING) {
x.then(y => {
resolvePromise(bridgepromise, y, resolve, reject);
}, error => {
reject(error);
});
} else {
x.then(resolve, reject);
}
// 2.3.3规范,若是 x 为对象或者函数
} else if (x != null && ((typeof x === 'object') || (typeof x === 'function'))) {
try {
// 是不是thenable对象(具备then方法的对象/函数)
//2.3.3.1 将 then 赋为 x.then
let then = x.then;
if (typeof then === 'function') {
//2.3.3.3 若是 then 是一个函数,以x为this调用then函数,且第一个参数是resolvePromise,第二个参数是rejectPromise
then.call(x, y => {
if (called) return;
called = true;
resolvePromise(bridgepromise, y, resolve, reject);
}, error => {
if (called) return;
called = true;
reject(error);
})
} else {
//2.3.3.4 若是 then不是一个函数,则 以x为值fulfill promise。
resolve(x);
}
} catch (e) {
//2.3.3.2 若是在取x.then值时抛出了异常,则以这个异常作为缘由将promise拒绝。
if (called) return;
called = true;
reject(e);
}
} else {
resolve(x);
}
}
MyPromise.prototype.then = function(onFulfilled, onRejected) {
const self = this;
let bridgePromise;
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value;
onRejected = typeof onRejected === "function" ? onRejected : error => { throw error };
if (self.status === FULFILLED) {
return bridgePromise = new MyPromise((resolve, reject) => {
setTimeout(() => {
try {
let x = onFulfilled(self.value);
resolvePromise(bridgePromise, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
})
}
if (self.status === REJECTED) {
return bridgePromise = new MyPromise((resolve, reject) => {
setTimeout(() => {
try {
let x = onRejected(self.error);
resolvePromise(bridgePromise, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
}
if (self.status === PENDING) {
return bridgePromise = new MyPromise((resolve, reject) => {
self.onFulfilledCallbacks.push((value) => {
try {
let x = onFulfilled(value);
resolvePromise(bridgePromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
self.onRejectedCallbacks.push((error) => {
try {
let x = onRejected(error);
resolvePromise(bridgePromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});
}
}
MyPromise.prototype.catch = function(onRejected) {
return this.then(null, onRejected);
}
// 执行测试用例须要用到的代码
MyPromise.deferred = function() {
let defer = {};
defer.promise = new MyPromise((resolve, reject) => {
defer.resolve = resolve;
defer.reject = reject;
});
return defer;
}
try {
module.exports = MyPromise
} catch (e) {}
复制代码
咱们能够先跑一下测试,须要安装一下测试插件,而后执行测试,测试时注意在加上上面最后的那几行代码才能执行测试用例。
1.npm i -g promises-aplus-tests
2.promises-aplus-tests mypromise.js
复制代码
运行测试用例能够看到,咱们上面写的promise代码经过了完整的Promises/A+规范测试。
而后开始分析咱们这一节的代码,咱们主要在resolvePromise里加了额外的两个判断,第一个是x和bridgePromise是指向相同值时,报出循环引用的错误,使promise符合2.3.1规范,而后咱们增长了一个x 为对象或者函数的判断,这一条判断主要对应2.3.3规范,中文规范如图:
else if (x != null && ((typeof x === 'object') || (typeof x === 'function'))) {
try {
// 是不是thenable对象(具备then方法的对象/函数)
//2.3.3.1 将 then 赋为 x.then
let then = x.then;
if (typeof then === 'function') {
//2.3.3.3 若是 then 是一个函数,以x为this调用then函数,且第一个参数是resolvePromise,第二个参数是rejectPromise
then.call(x, y => {
if (called) return;
called = true;
resolvePromise(bridgepromise, y, resolve, reject);
}, error => {
if (called) return;
called = true;
reject(error);
})
} else {
//2.3.3.4 若是 then不是一个函数,则以x为值fulfill promise。
resolve(x);
}
} catch (e) {
//2.3.3.2 若是在取x.then值时抛出了异常,则以这个异常作为缘由将promise拒绝。
if (called) return;
called = true;
reject(e);
}
}
复制代码
再写完这个分支的代码后,其实咱们已经能够删除if (x instanceof MyPromise) {}
这个分支的代码,由于promise也是一个thenable对象,彻底可使用上述代码兼容代替。另外,本节代码不少重复代码能够封装优化一下,可是为了看得清晰,并无进行抽象封装,你们若是以为重复代码太多的话,能够自行抽象封装。
上一节咱们已经实现了一个符合Promises/A+规范的promise,本节咱们把一些es6 promise里的经常使用方法实现一下。
实现es6 promise的all,race,resolve,reject方法
咱们仍是在以前的基础上继续往下写:
MyPromise.all = function(promises) {
return new MyPromise(function(resolve, reject) {
let result = [];
let count = 0;
for (let i = 0; i < promises.length; i++) {
promises[i].then(function(data) {
result[i] = data;
if (++count == promises.length) {
resolve(result);
}
}, function(error) {
reject(error);
});
}
});
}
MyPromise.race = function(promises) {
return new MyPromise(function(resolve, reject) {
for (let i = 0; i < promises.length; i++) {
promises[i].then(function(data) {
resolve(data);
}, function(error) {
reject(error);
});
}
});
}
MyPromise.resolve = function(value) {
return new MyPromise(resolve => {
resolve(value);
});
}
MyPromise.reject = function(error) {
return new MyPromise((resolve, reject) => {
reject(error);
});
}
复制代码
其实前几节把promise的主线逻辑实现后,这些方法都不难实现,all的原理就是返回一个promise,在这个promise中给全部传入的promise的then方法中都注册上回调,回调成功了就把值放到结果数组中,全部回调都成功了就让返回的这个promise去reslove,把结果数组返回出去,race和all大同小异,只不过它不会等全部promise都成功,而是谁快就把谁返回出去,resolve和reject的逻辑也很简单,看一下就明白了。
其实到上一节为止,promise的方法已经都讲完了,这一节讲一个著名promise库bluebird里面的方法promiseify,由于这个方法很经常使用并且之前面试还被问过。promiseify有什么做用呢?它的做用就是将异步回调函数api转换为promise形式,好比下面这个,对fs.readFile 执行promiseify后,就能够直接用promise的方式去调用读取文件的方法了,是否是很强大。
let Promise = require('./bluebird');
let fs = require("fs");
var readFile = Promise.promisify(fs.readFile);
readFile("1.txt", "utf8").then(function(data) {
console.log(data);
})
复制代码
实现bluebird的promiseify方法
MyPromise.promisify = function(fn) {
return function() {
var args = Array.from(arguments);
return new MyPromise(function(resolve, reject) {
fn.apply(null, args.concat(function(err) {
err ? reject(err) : resolve(arguments[1])
}));
})
}
}
复制代码
虽然方法很强大,可是实现起来并无很难,想在外边直接调用promise的方法那就返回一个promise呗,内部将原来参数后面拼接一个回调函数参数,在回调函数里执行这个promise的reslove方法把结果传出去,promiseify就实现了。
不知不觉写了这么多了,你们若是以为还能够就给个赞呗,另外每一节的代码都托管到了github上,你们能够对照看那一节的promise实现代码及测试代码,也顺便求个star~
项目地址:本文代码的github仓库
另外,实现一个符合Promises/A+规范的promise不止本文一种实现方式,本文只是选取了一种比较通俗易懂的实现方式做为讲解,你们也能够用本身的方式去实现一个符合Promises/A+规范的promise。