前言
我们都知道javascript是一门单线程的脚本语言。不能创建线程,不能开展并行任务,不能对线程操作。在页面加载时会阻塞ui渲染。但是,虽然js是单线程语言,我们的浏览器却不是单线程的。我们可以利用异步线程来解决这个问题。这也正是为什么异步编程对于js来说是非常重要的原因。
什么是promise
首先,我们要知道,什么是promise。promise是一个为了解决异步编程而被提出来的解决方案,他最早是由社区提出来的,在ES6中,被写进了语言标准,统一了它的用法,原生提供了Promise对象。
Promsie为什么会被提出?
传统的解决异步编程的方案是什么呢?
- setTimeout、setInterval、setImmediate ( ps:html5标准规定 setTimeout的最小执行间隔是4ms,而setInterval的最小执行间隔是10ms) 这个方案的问题在于,它并非那么精确,以setTimeout为例
setTimeout(()=>{console.log('我执行了')})
for(let i=0;i<10000000000;i++){};
复制代码
- 控制台会在4ms后将 '我执行了' 打出吗?显然不会,他要等到当前执行栈中的代码执行完之后才会执行,也就是,他要等到for循环结束之后才能执行异步的代码。
- 事件监听
整个程序都要变成事件驱动型,程序的运行机制将会变得很不清晰。 - 回调函数
js中对于异步编程的实现,基本上就是回调函数,但是层层嵌套的回调函数可读性非常差,错误处理也很不清晰。想起被回调地狱控制的恐惧吧。
$.ajax('a.json',function(data){
$.ajax(data,function(data){
$.ajax(data,function(data){
$.ajax(data,function(data){
$.ajax(data,function(data){
console.log(data)
})
})
})
})
})
复制代码
- 解决方案
为了解决这个问题es6、7把Promise、Generator以及两者的进阶async函数写入了语言标准, Promise的写法是回调函数的改进,使用Promise实例的then方法,可以将异步回调的嵌套关系转变为链式调用。
let promise = new Promise((resolve,reject)=>{
setTimeout(resolve,3000)
})
promise.then(data=>{
console.log(data)
},err=>{
console.error(err)
}).then()
复制代码
Promsie写起来
废话不多说了,相信点进这篇文章的coder应该对于Promise本身已经很熟悉了(对这一块存有疑虑的,可以先去开一下阮老师的es6入门),那么我们赶紧把来实现一下我们自己的Promise库吧.
class Promise {
constructor(executor) {
// 一个Promise实例创建出来时,他的默认状态一定是等待态 pending
this.status = 'pending';
// 成功时的不可变值
this.value = undefined;
// 失败的原因
this.reason = undefined;
/**
* 两个数组 onResolvedCakkbacks和onRejectedCallbacks
* 用来存放当前promise实例还处于pending状态时添加的成功回调函数
* 以及失败的回调函数
* */
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
let resolve = data => {
/**
* 确认只有当状态为pending时,改变当前promise实例的状态为fullfilled
* 并且执行存放在成功回调数组里面的回调函数
* */
if (this.status === "pending") {
// 确定不可变值
this.value = data;
// 固定当前实例的状态
this.status = 'fulfilled';
this.onResolvedCallbacks.forEach(fn => fn())
}
}
let reject = err => {
/**
* 确认只有当状态为pending时,改变当前promise实例的状态为rejected
* 并且执行存放在成功回调数组里面的回调函数
* */
if (this.status === 'pending') {
// 获取失败原因
this.reason = err;
// 固定当前实例的状态
this.status = 'rejected';
this.onRejectedCallbacks.forEach(fn => fn())
}
}
/**
* 创建Promise实例时,将会直接运行他的执行函数,
* 并且,如果在执行过程中出现了异常,
* 将会导致该promise实例的状态变为rejected
* */
try {
executor(resolve, reject);
} catch (e) {
reject(e); // 将错误原因传递出去
}
}
/**
* Promise实例上的原型上有一个then方法,then方法上有两个参数,分别是
* 当该实例状态为fulfilled时或者rejected时的回调
* */
then(onFulFilled, onRejected) {
/**
* 由于onFulFilled和onRejected可能存在不是函数的问题
* 所以要对这两个参数进行判断
* */
onFulFilled = typeof onFulFilled === 'function' ? onFulFilled : y => y;
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err }; // 没有传递错误回调时,将错误原因向外抛出
/**
* 申明一个变量promise2,为什么要叫promise2? (黑人问号????)
* 嗯,这是个好问题,因为这是Promise A+规范里面规定的,
* 同时,规范还规定了,每个Promise的实例的then方法都会返回一个
* 新的Promise实例,注意,这个新的实例Promsie并不是我们原来的那个Promise实例
* */
let promise2;
// 当前状态为pending时
if (this.status === 'pending') {
promise2 = new Promise((resolve, reject) => {
// 存放成功的回调
this.onResolvedCallbacks.push(() => {
/**
* 在原生es6中,异步运行回调时,Promise.then属于微任务,
* 但是我们现在只能使用setTimeout来模拟这个异步回调,
* 导致这里成为了一个宏任务,虽然和原声的Promise对象有点区别,
* 但是我们的这个Promise仍然是符合 Promise A+规范的
* 如果我们是在node环境下,也许我们可以使用process.nexttick来进行模拟
* 这样将会和原声Promise对象表现的更加一致
* 关于宏任务和微任务的区别,这又要和js事件环联系起来,由于篇幅有限,
* 大家可以去了解下js事件循环相关的知识
* */
setTimeout(() => {
/**
* 获取onFulfilled(this.value)的返回值x,
* 我们需要一个方法来检测,这个返回值x到底是什么东西,
* 他是不是一个promise对象?如果他不是promise对象,
* 那么我们就可以直接把x作为promise2的fulfilled回调函数的参数传过去
* */
try {
let x = onFulFilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e) // 如果运行异常,那么直接让promise2的状态固定为rejected,并把错误原因传递出去
}
});
});
// 存放失败的回调
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});
})
}
// 当状态为成功时
if (this.status === 'fulfilled') {
promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
try {
let x = onFulFilled(this.value);
// 解析x和promise2的关系
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
// 异常时失败
reject(e);
}
})
})
}
if (this.status === 'rejected') {
promise2 == new Promise((resolve, reject) => {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e);
}
});
});
}
return promise2; // Promise实例的then方法,总是返回一个新的Promise
}
}
// 定义检测x与promise2的方法
function resolvePromise(promise2, x, resolve, reject) {
/**
* 首先我们要判断x是不是Promise2在规范中,规定了一段代码,这个代
* 码可以实现我们的promise和别人的promise可以进行交互,但是我们不知道
* 别人的promise的实现会不会出现 promise2 === x 这种情况的出现,如果
* 出现这种情况会出现循环引用的问题, promise2 === x,这样,promise2要等待x
* 的状态改变,而x就是它本身,这样循环引用promise2的状态就会在这里停滞,一直
* 处于pending状态
* */
if (promise2 === x) {
return reject(new TypeError('循环引用'))
}
// 对x的数据类型进行判断,当x为对象或者函数时,我们需要对它进行判断
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
let called; //防止成功后调用失败方法
/**
* 规范要求声明一个变量then,并将x的then属性赋给该变量
* 这个时候我们需要使用try catch来运行这段代码,因为,在这个过程中如果发生了异常,
* 我们需要把异常给传递出去
* 一般来说,这样一个赋值操作是不会出现异常这种问题的,但是我们的Promise类库可能会和其他人
* 的Promise类库混用,这个时候,如果其他人的类库中x的then属性是通过Object.defineProperty定义的
* 将可能导致出现异常(虽然正常人不会这么写代码,但保不齐刚好遇到个不正常的呢?)
* 例: Object.defineProperty(x,'then',{
* set(){
* throw (new Error());
* }
* })
* */
try {
let then = x.then;
if (typeof then === 'function') {
/**
* 按照规范,如果then是函数,我们就认为x是promise
* 这时,我们通过then.call运行then方法,将this指向x
* ,后面的是成功的回调和失败的回调
* */
then.call(x, y => { // 如果y是promise ,那么我们就继续递归解析
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
}, err => { //如果失败了,那么就直接将状态修改为失败
if (called) return;
called = true;
reject(r);
})
} else { // 当then只是一个普通对象时,那么我们直接将这个对象作为promise2的不可变值传出去
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e)
}
} else { //当x既不是对象,也不是函数时,直接将他作为promise2的不可变值传递出去
resolve(x)
}
}
//测试一下
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(3333)
}, 3000)
})
p1.then(data => {
console.log(1, data)
}, err => {
console.error(2, err)
})
//导出Promise
module.exports = Promise;
复制代码
最后,我们再使用npm安装一个插件,这个插件能够帮助我们检测,我们的promise类库是否符合 Promise A+规范
npm install promises-aplus-tests -g
promises-aplus-test 文件名
复制代码
附上Promise A+规范地址,大家可以看看