前言
此篇文章主要是用来加深自己对promise原理的理解。
什么是Promise
Promise是JS解决异步编程的一种方式,是ES6推广的一个新概念。所谓的Promise相当于一个状态机,里面承载着未来某一个时刻所发生的状态。
Promise的状态
Promise的有三种状态:
- pending: 一个promise在resolve或者reject前就处于这个状态。
- fulfilled: 一个promise被resolve后就处于fulfilled状态,这个状态不能再改变,而且必须拥有一个不可变的值(value)。
- rejected: 一个promise被reject后就处于rejected状态,这个状态也不能再改变,而且必须拥有一个不可变的拒绝原因(reason)。
Promise的特点是什么
- 对象的状态不受外界的影响,只有异步结果才能决定对象的状态
- 对象的状态一旦发生改变,就不会再改变了。任何时刻都可以拿到此结果
Promise解决了什么问题
Promise和回调函数是2个概念,Promise是新的概念而不是回调函数的扩展。
-
使用Promise解决了回调地狱的问题
Promise通过链式调用解决了回调函数层层嵌套的问题,
-
使用Promise处理异步编程,不会使得代码逻辑跳跃
最原始的回调函数处理异步编程使得代码跳跃,因为当异步有了结果才会去执行回调函数,那么callback的执行和定义可能并不在同一个地方。使得代码的可读性没那么清晰。而Promsie通过.then函数调用就可以解决这样的逻辑跳跃问题。
-
使用Promsie可以捕获处理错误信息
最原始的回调函数处理异步编程无法实现错误信息的捕获,那么通过使用Promise的.catch方法或者在.then的第二个参数(回调函数)来处理错误信息。
Promsie实现思路分析
在实现Promsie的过程当中,需要先了解Promises/A+规范,这里可查看Promises/A+规范翻译
Promise的基本结构
let p=new Promise((resolve, reject) => {
setTimeout(() => {
resolve('FULFILLED');
}, 1000)
}).then(res=>{
console.log(res);
}).catch(err=>{
console.log(err);
})
如上是使用ES6中Promise的基本结构,可见Promise是一个构造函数或者是一个类,传入一个回调函数,回调函数接收resolve和reject两个方法;并且Promise有.then和.catch基本这些基本的方法。
then方法
promise.then(onFulfilled, onRejected)
一个Promise必须拥有then方法,而且then方法接收2个可选参数onFulfilled和onRejected。
- onFulfilled 用来当状态值是Fulfilled接受的处理函数。
- onRejected 用来处理状态值是Rejected拒绝的处理函数。
- then 方法可以被同一个 promise 调用多次
- 当 promise 成功执行时,所有 onFulfilled 需按照其注册顺序依次回调
- 当 promise 被拒绝执行时,所有的 onRejected 需按照其注册顺序依次回调
- then 方法返回一个 promise 对象。
catch方法
一个Promise必须拥有catch方法,用来处理拒绝的情况。
promise.catch(onRejected)
Promise的状态值分析
Promise有三个状态值,所以先定义三个状态常量。一个Promise的当前状态必须是等待状态(Pending), 接受状态(Fulfilled) 和 拒绝状态(Rejected)当中的一种。
// 先定义三个常量表示状态
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
- 当Promsie的状态值是pending时,那么它有可能变成Fulfilled或者是Rejected。
- 当Promise的状态是Fulfilled或者是Rejected,那么就无法改变它的值了。
- 当状态值为rejected时,有一个不可变的拒绝原因。
Promise封装
MyPromise接收一个fn(函数),执行这个fn,并且将resolve和reject传递出去。
- 当调用resolve时将状态值PENDING变成FULFILLED,并且接收value值(成功的值)
- 当调用reject时将状态值PENDING变成REJECTED,并且接收reason值(失败拒绝的值)
MyPromise通过使用try--catch来捕获错误信息。
function MyPromise(fn) {
this.status = PENDING; // 初始状态为pending
this.value = null; // 初始化value
this.reason = null; // 初始化reason
// 存一下this,以便resolve和reject里面访问
var that = this;
// resolve方法参数是value
function resolve(value) {
if (that.status === PENDING) {
that.status = FULFILLED;
that.value = value;
}
}
// reject方法参数是reason
function reject(reason) {
if (that.status === PENDING) {
that.status = REJECTED;
that.reason = reason;
}
}
try {
fn(resolve, reject);
} catch (error) {
reject(error);
}
}
接下来实现一个then函数:
根据上面Promises/A+规范的分析可见,then方法返回一个 promise ,并且then方法可以处理不同状态值的情况。想想当我们使用promise时,如果成功之后就会调用then的onFulfilled方法,当失败之后就会调用then的onRejected方法。所以,在大体上能确定的是有条件分支是当状态值为FULFILLED和REJECTED这两种情况,在想想状态值不同的时候,是如何调用onFulfilled方法和onRejected方法。
在调用onFulfilled方法和onRejected方法之前会去判断onFulfilled方法和onRejected方法是不是一个函数,如果是才调用这个函数,不是则直接返回值。(值是promise1的值是根据成功或者失败决定返回reason还是value)。其实就是相当于将promise1的值穿透到promise2。
那如果onFulfilled方法和onRejected方法是函数并且有自己的返回值,那么我们就要根据Promises/A+规范进行编码处理。
在将Promises/A+规范转换成代码之前,我们先看看如下的代码:
new Promise(fn).then(onFulfilled, onRejected);
当我们以如上代码使用promise时,Promise的状态值还是 PENDING (then方法和Promise构造函数一起执行,根本不知道什么时候状态值发生改变)。这时候肯定不能立即调onFulfilled或者onRejected的,因为fn到底成功还是失败还不知道。那什么时候知道fn成功还是失败呢?答案是fn里面主动调resolve或者reject的时候。所以要先收集onFulfilled和onRejected方法(肯定在then方法里面去收集);如果状态值还是PENDING,应该将onFulfilled和onRejected两个回调存起来,等到fn有了结论,resolve或者reject的时候再来调用对应的代码(肯定在状态值发生变化的地方去调用函数)。因为后面then还有链式调用,会有多个onFulfilled和onRejected,这里用两个数组将他们存起来,等resolve或者reject的时候将数组里面的全部方法拿出来执行一遍。
这个过程像极了订阅发布模式。那这样的话then方法里面肯定需要多一条状态值为PENDING的条件分支。
所以新增MyPromise函数的代码如下:
function MyPromise(fn) {
...
// 构造函数里面添加两个数组存储成功和失败的回调
+ this.onFulfilledCallbacks = [];
+ this.onRejectedCallbacks = [];
// 存一下this,以便resolve和reject里面访问
var that = this;
// resolve方法参数是value
function resolve(value) {
if (that.status === PENDING) {
...
// resolve里面将所有成功的回调拿出来执行
+ that.onFulfilledCallbacks.forEach((callback) => {
+ callback(that.value);
+ });
}
}
// reject方法参数是reason
function reject(reason) {
if (that.status === PENDING) {
...
// resolve里面将所有失败的回调拿出来执行
+ that.onRejectedCallbacks.forEach((callback) => {
+ callback(that.reason);
+ });
}
}
...
}
回到当onFulfilled方法和onRejected方法是函数并且有自己的返回值时的情况,这时候就需要关注Promises/A+规范的Promise 解决过程,将 Promise 解决过程转成代码如下:
function resolvePromise(promise, x, resolve, reject) {
// 如果 promise 和 x 指向同一对象,以 TypeError 为据因拒绝执行 promise
// 这是为了防止死循环
if (promise === x) {
return reject(
new TypeError("The promise and the return value are the same")
);
}
if (x instanceof MyPromise) {
// 如果 x 为 Promise ,则使 promise 接受 x 的状态
// 也就是继续执行x,如果执行的时候拿到一个y,还要继续解析y
// 这个if跟下面判断then然后拿到执行其实重复了,可有可无
x.then(function(y) {
resolvePromise(promise, y, resolve, reject);
}, reject);
}
// 如果 x 为对象或者函数
else if (typeof x === "object" || typeof x === "function") {
// 这个坑是跑测试的时候发现的,如果x是null,应该直接resolve
if (x === null) {
return resolve(x);
}
try {
// 把 x.then 赋值给 then
var then = x.then;
} catch (error) {
// 如果取 x.then 的值时抛出错误 e ,则以 e 为据因拒绝 promise
return reject(error);
}
// 如果 then 是函数
if (typeof then === "function") {
var called = false;
// 将 x 作为函数的作用域 this 调用之
// 传递两个回调函数作为参数,第一个参数叫做 resolvePromise ,第二个参数叫做 rejectPromise
// 名字重名了,我直接用匿名函数了
try {
then.call(
x,
// 如果 resolvePromise 以值 y 为参数被调用,则运行 [[Resolve]](promise, y)
function(y) {
// 如果 resolvePromise 和 rejectPromise 均被调用,
// 或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用
// 实现这条需要前面加一个变量called
if (called) return;
called = true;
resolvePromise(promise, y, resolve, reject);
},
// 如果 rejectPromise 以据因 r 为参数被调用,则以据因 r 拒绝 promise
function(r) {
if (called) return;
called = true;
reject(r);
}
);
} catch (error) {
// 如果调用 then 方法抛出了异常 e:
// 如果 resolvePromise 或 rejectPromise 已经被调用,则忽略之
if (called) return;
// 否则以 e 为据因拒绝 promise
reject(error);
}
} else {
// 如果 then 不是函数,以 x 为参数执行 promise
resolve(x);
}
} else {
// 如果 x 不为对象或者函数,以 x 为参数执行 promise
resolve(x);
}
}
在完成Promise 解决过程之后,我们还需讨论onFulfilled 和 onRejected 方法在如何执行?是同步执行还是异步执行呢?
在规范中讲到:实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。
所以在我们执行onFulfilled 和 onRejected的时候都应该包到setTimeout里面去。那么then 方法的实现代码如下:
MyPromise.prototype.then = function(onFulfilled, onRejected) {
// 如果onFulfilled不是函数,给一个默认函数,返回value
var realOnFulfilled = onFulfilled;
// 如果onRejected不是函数,给一个默认函数,返回reason的Error
var realOnRejected = onRejected;
var that = this; // 保存一下this
if (this.status === FULFILLED) {
var promise2 = new MyPromise(function(resolve, reject) {
setTimeout(function() {
try {
if (typeof onFulfilled !== "function") {
resolve(that.value);
} else {
var x = realOnFulfilled(that.value);
resolvePromise(promise2, x, resolve, reject);
}
} catch (error) {
reject(error);
}
}, 0);
});
return promise2;
}
if (this.status === REJECTED) {
var promise2 = new MyPromise(function(resolve, reject) {
setTimeout(function() {
try {
if (typeof onRejected !== "function") {
reject(that.reason);
} else {
var x = realOnRejected(that.reason);
resolvePromise(promise2, x, resolve, reject);
}
} catch (error) {
reject(error);
}
}, 0);
});
return promise2;
}
// 如果还是PENDING状态,将回调保存下来
if (this.status === PENDING) {
var promise2 = new MyPromise(function(resolve, reject) {
that.onFulfilledCallbacks.push(function() {
setTimeout(function() {
try {
if (typeof onFulfilled !== "function") {
resolve(that.value);
} else {
var x = realOnFulfilled(that.value);
resolvePromise(promise2, x, resolve, reject);
}
} catch (error) {
reject(error);
}
}, 0);
});
that.onRejectedCallbacks.push(function() {
setTimeout(function() {
try {
if (typeof onRejected !== "function") {
reject(that.reason);
} else {
var x = realOnRejected(that.reason);
resolvePromise(promise2, x, resolve, reject);
}
} catch (error) {
reject(error);
}
}, 0);
});
});
return promise2;
}
};
其他Promise方法
根据ES6的文档Promise 对象的用法手写Promise的其他方法,这里先拎出来各方法的特点再编写代码。
1.MyPromise.resolve特点
- 参数是一个 Promise 实例则返回该实例
- 参数不是Promise 实例则包装成Promise对象并且直接resolve参数
MyPromise.resolve = function(parameter) {
if (parameter instanceof MyPromise) {
return parameter;
}
return new MyPromise(function(resolve) {
resolve(parameter);
});
};
- MyPromise.reject特点
- 返回一个新的 Promise 实例,该实例的状态为reject,并且据因为传入的参数
MyPromise.reject = function(reason) {
return new MyPromise(function(resolve, reject) {
reject(reason);
});
};
3.MyPromise.all特点
- 传入一个数组作为参数,数组的每一个元素都是一个promise实例
- 返回一个新的promise,并且promise的结果值是一个数组,数组中的值为参数中每一个promise的返回值并且一一对应的关系
- 只要参数中的promise实例一个发生错误了,那么这个新的promise状态值就是拒绝状态
MyPromise.all = function(args=[]) {
let count=0;
let callbackArr=[];
return new Promise((resolve,reject)=>{
for(let i=0;i<args.length;i++){
// (function(i){
args[i].then(res=>{
callbackArr[i]=res;
count++;
if(count==args.length){
return resolve(callbackArr);
}
}).catch(err=>{
return reject(err);
})
//})(i)
}
})
};
MyPromise.prototype.catch 特点
- .catch()方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数。
MyPromise.prototype.catch = function(onRejected) {
this.then(null, onRejected);
};
其他的方法小伙伴自行实现,抓住特点,我觉得实现其他就不会很难了。
总结
知原理知天下,当会使用一个语法的时候多考虑背后的实现思想原理,会加深对此语法的理解。