我的开源项目 — Vchat 正式上线了,欢迎各位小哥哥小姐姐体验。若是以为还行的话,记得给个star哟 ^_^。javascript
众所周知,js是单线程异步机制的。这样就会致使不少异步处理会嵌套不少的回调函数,最为常见的就是ajax请求,咱们须要等请求结果返回后再进行某些操做。如:html
function success(data, status) {
console.log(data)
}
function fail(err, status) {
console.log(err)
}
ajax({
url: myUrl,
type: 'get',
dataType: 'json',
timeout: 1000,
success: success(data, status),
fail: fail(err, status)
})
复制代码
乍一看还行啊,不够绝望啊,让绝望来的更猛烈一些吧!那么试想一下,若是还有多个请求依赖于上一个请求的返回值呢?五个?六个?代码就会变得很是冗余和不易维护。这种现象,咱们通常亲切地称它为‘回调地狱’。如今解决回调地狱的手段有不少,好比很是方便的async/await、Promise等。前端
咱们如今要讲的是Promise。在现在的前端面试中,Promise简直是考点般的存在啊,十个有九个会问。那么咱们如何真正的弄懂Promise呢?俗话说的好,‘想要了解它,先要接近它,再慢慢地实现它’。本身实现一个Promise,不就什么都懂了。java
其实网络上关于Promise的文章有不少,我也查阅了一些相关文章,文末有给出相关原文连接。因此本文侧重点是我在实现Promise过程当中的思路以及我的的一些理解,有感兴趣的小伙伴能够一块儿交流。ios
若是用promise实现上面的ajax,大概是这个效果:git
ajax().success().fail();
复制代码
那么什么是Promise呢?es6
基本用法:github
let getInfo = new Promise((resolve, reject) => {
setTimeout(_ => {
let ran = Math.random();
console.log(ran);
if (ran > 0.5) {
resolve('success');
} else {
reject('fail');
}
}, 200);
});
getInfo.then(r => {
return r + ' ----> Vchat';
}).then(r => {
console.log(r);
}).catch(err => {
console.log(err);
})
// ran > 0.5输出 success ----> Vchat
// ran <= 0.5输出 fail
复制代码
先定个小目标,而后一步步实现它。面试
基础构造ajax
首先须要了解一下基本原理。我第一次接触Promise的时候,还很懵懂(捂脸)。那会只知道这么写,不知道究竟是个什么流程走向。下面,咱们来看看最基本的实现:
function Promise(Fn){
let resolveCall = function() {console.log('我是默认的');}; // 定义为函数是为了防止没有then方法时报错
this.then = (onFulfilled) => {
resolveCall = onFulfilled;
};
function resolve(v){ // 将resolve的参数传给then中的回调
resolveCall(v);
}
Fn(resolve);
}
new Promise((resolve, reject) => {
setTimeout(_ => {
resolve('success');
}, 200)
}).then(r => {
console.log(r);
});
// success
复制代码
这里须要注意的是,当咱们new Promise 的时候Promise里的函数会直接执行。因此若是你想定义一个Promise以待后用,好比axios封装,须要用函数包装。好比这样:
function myPromise() {
return new Promise((resolve, reject) => {
setTimeout(_ => {
resolve('success');
}, 200)
})
}
// myPromise().then()
复制代码
再回到上面,在new Promise 的时候会当即执行fn,遇到异步方法,因而先执行then中的方法,将 onFulfilled 存储到 resolveCall 中。异步时间到了后,执行 resolve,从而执行 resolveCall即储存的then方法。这是输出的是咱们传入的‘success’
这里会有一个问题,若是 Promise 接受的方法不是异步的,则会致使 resolve 比 then 方法先执行。而此时 resolveCall 尚未被赋值,得不到咱们想要的结果。因此要给resolve加上异步操做,从而保证then方法先执行。
// 直接resolve
new Promise((resolve, reject) => {
resolve('success');
}).then(r => {
console.log(r); // 输出为 ‘我是默认的’,由于此时then方法尚未,then方法的回调没有赋值给resolveCall,执行的是默认定义的function() {}。
});
// 加上异步处理,保证then方法先执行
function resolve(v){
setTimeout(_ => {
resolveCall(v);
})
}
复制代码
增长链式调用
链式调用是Promise很是重要的一个特征,可是上面写的那个函数显然是不支持链式调用的,因此咱们须要进行处理,在每个then方法中return一下this。
function Promise(Fn){
this.resolves = []; // 方便存储onFulfilled
this.then = (onFulfilled) => {
this.resolves.push(onFulfilled);
return this;
};
let resolve = (value) =>{ // 改用箭头函数,这样不用担忧this指针问题
setTimeout(_ => {
this.resolves.forEach(fn => fn(value));
});
};
Fn(resolve);
}
复制代码
能够看到,这里将接收then回调的方法改成了Promise的属性resolves,并且是数组。这是由于若是有多个then,依次push到数组的方式才能存储,不然后面的then会将以前保存的覆盖掉。这样等到resolve被调用的时候,依次执行resolves中的函数就能够了。这样能够进行简单的链式调用。
new Promise((resolve, reject) => {
resolve('success');
}).then(r => {
console.log(r); // success
}).then(r => {
console.log(r); // success
});
复制代码
可是咱们会有这样的需求, 某一个then链想本身return一个参数供后面的then使用,如:
then(r => {
console.log(r);
return r + ' ---> Vchat';
}).then();
复制代码
要作到这一步,须要再加一个处理。
let resolve = (value) =>{
setTimeout(_ => {
// 每次执行then的回调时判断一下是否有返回值,有的话更新value
this.resolves.forEach(fn => value = fn(value) || value);
});
};
复制代码
增长状态
咱们在文章开始说了Promise的三种状态以及成功和失败的参数,如今咱们须要体如今本身写的实例里面。
function Promise(Fn){
this.resolves = [];
this.status = 'PENDING'; // 初始为'PENDING'状态
this.value;
this.then = (onFulfilled) => {
if (this.status === 'PENDING') { // 若是是'PENDING',则储存到数组中
this.resolves.push(onFulfilled);
} else if (this.status === 'FULFILLED') { // 若是是'FULFILLED',则当即执行回调
console.log('isFULFILLED');
onFulfilled(this.value);
}
return this;
};
let resolve = (value) =>{
if (this.status === 'PENDING') { // 'PENDING' 状态才执行resolve操做
setTimeout(_ => {
//状态转换为FULFILLED
//执行then时保存到resolves里的回调
//若是回调有返回值,更新当前value
this.status = 'FULFILLED';
this.resolves.forEach(fn => value = fn(value) || value);
// 这里有一个问题 实际上Promise 并无判断是否有fanhui返回值
// fn => value = fn(value),没有返回值就是 undefined
this.value = value;
});
}
};
Fn(resolve);
}
复制代码
这里可能会有同窗以为困惑,咱们经过一个例子来讲明增长的这些处理到底有什么用。
let getInfo = new Promise((resolve, reject) => {
resolve('success');
}).then(_ => {
console.log('hahah');
});
setTimeout(_ => {
getInfo.then(r => {
console.log(r); // success
})
}, 200);
复制代码
在resolve函数中,判断了'PENDING' 状态才执行setTimeout方法,而且在执行时更改了状态为'FULFILLED'。这时,若是运行这个例子,只会输出一个‘hahah’,由于接下来的异步方法调用时状态已经被改成‘FULFILLED’,因此不会再次执行。
这种状况要想它能够执行,就须要用到then方法里的判断,若是状态是'FULFILLED',则当即执行回调。此时的传参是在resolve执行时保存的this.value。这样就符合Promise的状态原则,PENDING不可逆,FULFILLED和REJECTED不能相互转化。
增长失败处理
可能有同窗发现我一直没有处理reject,不用着急。reject和resolve流程是同样的,须要一个reason作为失败的信息返回。在链式调用中,只要有一处出现了reject,后续的resolve都不该该执行,而是直接返回reject。
this.reason;
this.rejects = [];
// 接收失败的onRejected函数
if (this.status === 'PENDING') {
this.rejects.push(onRejected);
}
// 若是状态是'REJECTED',则当即执行onRejected。
if (this.status === 'REJECTED') {
onRejected(this.reason);
}
// reject方法
let reject = (reason) =>{
if (this.status === 'PENDING') {
setTimeout(_ => {
//状态转换为REJECTED
//执行then时保存到rejects里的回调
//若是回调有返回值,更新当前reason
this.status = 'REJECTED';
this.rejects.forEach(fn => reason = fn(reason) || reason);
this.reason = reason;
});
}
};
// 执行Fn出错直接reject
try {
Fn(resolve, reject);
}
catch(err) {
reject(err);
}
复制代码
在执行储存then中的回调函数那一步有一个细节一直没有处理,那就是判断是否有onFulfilled或者onRejected方法,由于是容许不要其中一个的。如今若是then中缺乏某个回调,会直接push进undefined,若是执行的话就会出错,因此要先判断一下是不是函数。
this.then = (onFulfilled, onRejected) => {
// 判断是不是函数,是函数则执行
function success (value) {
return typeof onFulfilled === 'function' && onFulfilled(value) || value;
}
function erro (reason) {
return typeof onRejected === 'function' && onRejected(reason) || reason;
}
// 下面的处理也要换成新定义的函数
if (this.status === 'PENDING') {
this.resolves.push(success);
this.rejects.push(erro);
} else if (this.status === 'FULFILLED') {
success(this.value);
} else if (this.status === 'REJECTED') {
erro(this.reason);
}
return this;
};
复制代码
由于reject回调执行时和resolve基本同样,因此稍微优化一下部分代码。
if(this.status === 'PENDING') {
let transition = (status, val) => {
setTimeout(_ => {
this.status = status;
let f = status === 'FULFILLED',
queue = this[f ? 'resolves' : 'rejects'];
queue.forEach(fn => val = fn(val) || val);
this[f ? 'value' : 'reason'] = val;
});
};
function resolve(value) {
transition('FULFILLED', value);
}
function reject(reason) {
transition('REJECTED', reason);
}
}
复制代码
串行 Promise
假设有多个ajax请求串联调用,即下一个须要上一个的返回值做为参数,而且要return一个新的Promise捕捉错误。这样咱们如今的写法就不能实现了。
个人理解是以前的then返回的一直是this,可是若是某一个then方法出错了,就没法跳出循环、抛出异常。并且原则上一个Promise,只要状态改变成‘FULFILLED’或者‘REJECTED’就不容许再次改变。
以前的例子能够执行是由于没有在then中作异常的处理,即没有reject,只是传递了数据。因此若是要作到每一步均可以独立的抛出异常,从而终止后面的方法执行,还须要再次改造,咱们须要每一个then方法中return一个新的Promise。
// 把then方法放到原型上,这样在new一个新的Promise时会去引用prototype的then方法,而不是再复制一份。
Promise.prototype.then = function(onFulfilled, onRejected) {
let promise = this;
return new Promise((resolve, reject) => {
function success (value) {
let val = typeof onFulfilled === 'function' && onFulfilled(value) || value;
resolve(val); // 执行完这个then方法的onFulfilled之后,resolve下一个then方法
}
function erro (reason) {
let rea = typeof onRejected === 'function' && onRejected(reason) || reason;
reject(rea); // 同resolve
}
if (promise.status === 'PENDING') {
promise.resolves.push(success);
promise.rejects.push(erro);
} else if (promise.status === 'FULFILLED') {
success(promise.value);
} else if (promise.status === 'REJECTED') {
erro(promise.reason);
}
});
};
复制代码
在成功的函数中还须要作一个处理,用以支持在then的回调函数(onFulfilled)中return的Promise。若是onFulfilled方法return的是一个Promise,则直接执行它的then方法。若是成功了,就继续执行后面的then链,失败了直接调用reject。
function success(value) {
let val = typeof onFulfilled === 'function' && onFulfilled(value) || value;
if(val && typeof val['then'] === 'function'){ // 判断是否有then方法
val.then(function(value){ // 若是返回的是Promise 则直接执行获得结果后再调用后面的then方法
resolve(value);
},function(reason){
reject(reason);
});
}else{
resolve(val);
}
}
复制代码
找个例子测试一下
function getInfo(success, fail) {
return new Promise((resolve, reject) => {
setTimeout(_ => {
let ran = Math.random();
console.log(success, ran);
if (ran > 0.5) {
resolve(success);
} else {
reject(fail);
}
}, 200);
})
}
getInfo('Vchat', 'fail').then(res => {
console.log(res);
return getInfo('能够线上预览了', 'erro');
}, rej => {
console.log(rej);
}).then(res => {
console.log(res);
}, rej => {
console.log(rej);
});
// 输出
// Vchat 0.8914818954810422
// Vchat
// 能够线上预览了 0.03702367800412443
// erro
复制代码
到这里,Promise的主要功能基本上都实现了。还有不少实用的扩展,咱们也能够添加。 好比 catch能够看作then的一个语法糖,只有onRejected回调的then方法。其它Promise的方法,好比.all、.race 等等,感兴趣的小伙伴能够本身实现一下。另外,文中若有不对之处,还请指出。
Promise.prototype.catch = function(onRejected){
return this.then(null, onRejected);
}
复制代码
qq前端交流群:960807765,欢迎各类技术交流,期待你的加入
欢迎关注公众号 前端发动机,江三疯的前端二三事,专一技术,也会时常迷糊。但愿在将来的前端路上,与你一同成长。