为什么使用Promise
起因
- 大家都知道做前端开发的时候最让人头痛的就是处理异步请求的情况,在请求到的成功回调函数里继续写函数,长此以往形成了回调地狱
let fs = require("fs");
fs.readFile("1.txt","utf-8",function(err,data){ // 读取文件1.txt
console.log(data); // 2.txt
fs.readFile(data,"utf-8",function(err,data){ // 读取文件2.txt
console.log(data); // 3.txt
fs.readFile(data,"utf-8",function(err,data){ // 读取文件3.txt
console.log(data); // 一层一层又一层
})
})
})
复制代码
从实际开发中,promise解决了回调地狱的问题,不会导致难以维护,then可以按照顺序执行
let fs = require("fs");
// 封装了一个read方法返回一个Promise的实例
function read(filePath,encoding){
return new Promise((resolve,reject)=>{
fs.readFile(filePath,encoding,(err,data)=>{
if(err) reject(err);
resolve(data);
})
})
}
fs.read("1.txt","utf-8").then(data=>{
fs.read(data,"utf-8"); // data相当于于callback的成功回调
}).then(data=>{
fs.read(data,"utf-8");
});
复制代码
- 我想同时获取多个文件的内容,我用fs去读取,然后异步的问题就来了,我怎么知道哪个先读完,哪个后读完 产生了多个回调同步结果的问题
// 把两个异步的结果在同一个时间同步了
let fs = require("fs");
function after(times,callback){
// (闭包)开辟一个空间(私有作用域)存放数据集合
let arr = [];
return function(data){
arr.push(data);
// 执行了times次才执行回调函数
if(--times === 0){
callback(arr);
}
}
}
let fn = after(2,function(data){ // 次数是死的
console.log(data);
})
fs.readFile("1.txt","utf-8",function(err,data){
fn(data);
})
fs.readFile("2.txt","utf-8",function(err,data){
fn(data);
})
复制代码
虽然我使用了预置函数after解决了这个问题,但是我们不能每次处理异步请求的时候,都手写一遍after函数,这不科学,不合理
使用Promise.all方法就轻松的解决了上面的问题
let fs = require("fs");
// 封装了一个read方法返回一个Promise的实例
function read(filePath,encoding){
return new Promise((resolve,reject)=>{
fs.readFile(filePath,encoding,(err,data)=>{
if(err) reject(err);
resolve(data);
})
})
}
// all执行后会返回一个新的promise,所以promise执行成功后才算成功,返回值是数组类型,有一个失败就失败了(&&)
Promise.all([read("1.txt","utf-8"),read("2.txt","utf-8")]).then(data=>{
console.log(data);
})
复制代码
了解了Promise的好处之后。那么我们回到今天的主角来
先介绍一下Promise的三种状态:
- Pending 等待态:创建Promise对象时的初始状态
- Resolved 成功态: 成功时的状态
- Rejected 失败态: 失败时的状态
状态转换 pending可以转换成成功态or失败态 成功态和失败态不能互相转换
let fs = require("fs");
// 封装了一个read方法返回一个Promise的实例
function read(filePath,encoding){
return new Promise((resolve,reject)=>{
fs.readFile(filePath,encoding,(err,data)=>{
if(err) reject(err); // pending转换为Rejected
resolve(data); // pending转换为Resolved
})
})
}
}
复制代码
Promise的原理
通过上面的了解之后,我们自己动手来写一个自己的Promise,没有考虑链式结构和all,rece等等
首先我们看一下ES6的Promise的例子
let promise = new Promise((resolve,reject)=>{ // 1
console.log("111");
setTimeout(()=>{ // 支持异步行为=> 6
console.log("222");
resolve("成功");
reject("失败");
})
})
// 有then方法=> 2 有data和err两个参数=> 3
promise.then(data=>{ // 调用成功的方法=> 5
console.log(data);
throw new Error("错误");
},err=>{ // 调用失败的方法=> 5
console.log("err1",err);
});
promise.then(data=>{ // then多次=> 5
console.log(data);
},err=>{
console.log("err2",err); // 如果发现错误就会走失败态 => 7
});
console.log("333");
// 控制台打印结果
// 111 // execute 默认new的时候就自动执行 => 1
// 333
// 222
// 成功 // 通过打印结果知道=> 4 8
// err2 Error: 错误
复制代码
通过上面的例子我们总结出实现简单的Promise的8个步骤
1. execute 默认new的时候就自动执行
2. 每个promise的实例 都有then方法 可通过判断有没有then来区分是不是promise对象
3. then方法中有两个参数 分别是成功和失败的回调
4. then方法是异步的(微任务)
5. 同一个promise的实例可以then多次,成功时会调用所有的成功的方法,失败时会调用所有的失败方法
6. new Promise中可以支持异步行为
7. 如果发现错误就会走失败态
8. resolved <=> rejected 状态只会更改一次
复制代码
动手写一个简单的Promise
// Promise.js
function Promise(execute){
let self = this;
self.value = undefined;
self.reason = undefined;
self.status = "pending"; // 状态默认是等待状态
self.onResolvedCallbacks = []; // 存放then中成功的回调
self.onRejectedCallbacks = []; // 存放then中失败的回调
function resolve(value){ // 成功的回调方法
// 步骤8 状态只能更改一次
if(self.status === "pending"){
self.value = value;
self.status = "resolved";
self.onResolvedCallbacks.forEach(fn=>fn());
}
}
function reject(reason){ // 失败的回调方法
// 步骤8 状态只能更改一次
if(self.status === "pending"){
self.reason = reason;
self.status = "rejected";
self.onRejectedCallbacks.forEach(fn=>fn());
}
}
// 步骤1 初始化执行execute方法
try{
execute(resolve,reject);
}catch(e){
// 步骤7 如果发现错误就会走失败态
reject(e);
}
}
// 步骤2 Promise有个then方法 步骤3 参数onFulfilled,onRejected
Promise.prototype.then = function(onFulfilled,onRejected){
// 步骤4 then方法是异步的(微任务)
setTimeout(()=>{
let self = this;
// 步骤5 成功时会调用所有的成功的方法,失败时会调用所有的失败方法
if(self.status === "resolved"){
onFulfilled(self.value);
}
if(self.status === "rejected"){
onRejected(self.reason);
}
// 步骤6 new Promise可以支持异步行为
// 因为异步延迟了Promise状态的改变,then先执行了 所以使用发布订阅等状态改变之后再统一执行
if(self.status === "pending"){
// 这样写的好处就是不用再传参数了
self.onResolvedCallbacks.push(()=>{
onFulfilled(self.value);
})
self.onRejectedCallbacks.push(()=>{
onRejected(self.reason);
})
}
},0)
}
module.exports = Promise;
复制代码
现在我们来测试一下自己写的Promise
let Promise = require("promise.js"); // 引入自己写的Promise
// 成功和失败的时候可以传递参数(成功和失败是你自己定义的)
let promise = new Promise((resolve,reject) => {
console.log(111);
// setTimeout(()=>{
// reject("abc");
// },1000)
//resolve("abc");
reject("abc");
//throw new Error("错误");
})
promise.then((data)=>{
console.log(data);
},(err)=>{
console.log("err",err);
})
promise.then((data)=>{
console.log(data);
},(err)=>{
console.log("err",err);
})
console.log(222);
复制代码
通过简单的测试,我们发现实现简单的操作没有问题。
我们使用自己写的Promise测试下面的代码就行不通,通过使用ES6原生的Promise测试我们发现Promise的then返回有可能还是一个Promise,当前Promise和下一个Promise有依赖关系 链式调用
具体步骤可以参考 PromiseA+规范 promisesaplus.com/
let fs = require("fs");
function read(filePath,encoding){
return new Promise((resolve,reject)=>{
fs.readFile(filePath,encoding,(err,data)=>{
if(err) reject(err);
resolve(data);
})
})
}
// 下一次的输入需要上一次的输出 (有依赖关系)
read("1.txt","utf-8").then(data=>{
console.log(data);
return read(data,'utf-8'); // 1
}).then(data=>{
console.log(data);
return read(data,'utf-8'); // 1
}).then(data=>{
console.log(data); // 2
return data.split('').reverse().join('');
}).then(data=>{
console.log(data);
throw new Error("失败");
}).then(null,err=>{
console.log("err",err);
}).then(data=>{
console.log(data);
}).catch(err=>{
console.log("catch");
}).then(data=>{
throw new Error("出错");
}).then(data=>{
}).then(null,err=>{
console.log("error"+err);
});
// jquery的链式调用 return this
// return this 在promise这里行不通
let promise1 = new Promise((resolve,reject)=>{
resolve();
});
// 返回的必须是一个新的promise 因为promise成功后不能再走失败
// 只能创建一个新的promise 执行逻辑
let promise2 = promise1.then(data=>{
// promise成功了 如果返回this 那不能走向失败
throw new Error();
});
promise2.then(null,err=>{
console.log(err);
});
复制代码
现在我们来写一个带有链式结构的Promise来实现上面的功能。在原基础上加一个resolvePromise方法来实现then返回Promise实例的链式结构
resolvePromise方法有如下规范:
1. 如果一个promise执行完后 返回的还是一个promise, 会把这个promise 的执行结果,传递给下一次then中
2. 如果then中返回的不是promise 是一个普通值,会将这个普通值作为下一次then的成功的结果
3. 如果then中失败了,会走下一个then的失败
4. 如果返回的是undefined,不管当前是成功还是失败,都会走下一次的成功
5. 错误没有处理的情况下才会走
6. then可以不写东西,相当于没写 (值的穿透)
7. 错误必须被捕获,不然会报错
8. then返回的必须是一个新的promise 因为promise成功后不能再走失败,只能创建一个新的promise 执行逻辑
复制代码
// 文件名 Promise.js
// 带链式结构的promise实现原理
function Promise(execute){
let self = this;
self.value = undefined; // 成功的值
self.reason = undefined; // 失败的原因
self.status = "pending"; // 值是pending状态
self.onResolvedCallbacks = []; // 可能new Promise的时候会有异步操作,保存成功和失败的回调
self.onRejectedCallbacks = [];
function resolve(value){ // 把状态改成成功态
if(self.status === "pending"){ // 只有等待态 可以改变状态
self.value = value;
self.status = "resolved";
self.onResolvedCallbacks.forEach(fn=>fn());
}
}
function reject(reason){ // 把状态改成失败态
if(self.status === "pending"){
self.reason = reason;
self.status = "rejected";
self.onRejectedCallbacks.forEach(fn=>fn());
}
}
try{
// 默认new Promise时 应该执行对应的执行器(同步执行)
execute(resolve,reject);
}catch(e){ // 如果执行exectuor时 发生错误 就会让当前的promise变成失败态
reject(e);
}
}
/**
* then的返回值和promise中then中参数(成功或者失败回调)返回的值进行对比
* 来决定执行的是promise2的成功还是失败回调
* @param {*} promise2 then执行完成之后返回新的promise
* @param {*} x then中成功或者失败函数返回的结果
* @param {*} resolve promise2的成功
* @param {*} reject promise2的失败
*/
// 所有的promise都遵循这个规范 (所有的promise可以通用)
function resolvePromise(promise2,x,resolve,reject){
// 如果promise2和x返回的是同一个对象就会产生死循环
if(promise2 === x){
return reject(new TypeError("Chaining cycle detected for promise #<Promise>"));
}
let called; // 成功或失败只能调用一次 默认false没有调用
// x可能是一个promise 或者是一个普通值 步骤1*
if(x !==null && (typeof x === "object" || typeof x === "function")){
try{
let then = x.then; // 取对象上的属性 怎么能报异常呢?(这个promise不一定是自己写的 可能是别人写的 有的人会乱写)
// 这里的逻辑不单单是自己的 还有别人的 别人的promise 可能既会调用成功 也会调用失败
// {then:{}}
if(typeof then === "function"){
// x可能还是一个promise 那么就让这个promise执行即可
then.call(x,data=>{ // 返回promise后的成功结果
if(called) return; // 防止多次调用
called = true;
// 递归 可能成功后的结果是一个promise 那就要循环的去解析 直到解析为普通值为止
resolvePromise(promise2,data,resolve,reject);
},err=>{ // promise的失败结果
if(called) return;
called = true;
reject(err); // 步骤3*
})
}else{
resolve(x);
}
}catch(e){
if(called) return;
called = true;
reject(e);
}
}else{ // x是常量,就把x做为下一次promise成功的参数 步骤2*
resolve(x);
}
}
// then调用的时候 都是异步调用 (原生的then的成功或者失败 是一个微任务)
// 为什么加setTimeout (规范要求的) 为了保持一致性
Promise.prototype.then = function(onFulfilled,onRejected){
// 成功和失败的回调 是可选参数 值的穿透 步骤6
onFulfilled = typeof onFulfilled === 'function'?onFulfilled:val=>val;
onRejected = typeof onRejected === "function"?onRejected:err=>{throw err};
let self = this;
// promise的链式结构的原理 then执行完之后返回一个新的promise
let promise2;
// 需要每次调用then时都返回一个新的promise 解决状态转换问题(不对的resolve<=>reject)
promise2 = new Promise((resolve,reject)=>{
if(self.status === "resolved"){
setTimeout(()=>{
try{
// 当执行成功或者失败回调的时候 可能会出现异常,那就用这个异常作为promise2的错误的结果
let x = onFulfilled(self.value); // x是then中成功或者失败函数返回的结果
//执行完当前成功或者失败的回调后返回结果可能是promise
resolvePromise(promise2,x,resolve,reject);
}catch(e){
reject(e);
}
},0)
}
if(self.status === "rejected"){
setTimeout(()=>{
try{
let x = onRejected(self.reason);
resolvePromise(promise2,x,resolve,reject);
}catch(e){
reject(e);
}
},0)
}
if(self.status === "pending"){
self.onResolvedCallbacks.push(()=>{
setTimeout(()=>{
try{
let x = onFulfilled(self.value);
resolvePromise(promise2,x,resolve,reject);
}catch(e){
reject(e);
}
},0)
})
self.onRejectedCallbacks.push(()=>{
setTimeout(()=>{
try{
let x = onRejected(self.reason);
resolvePromise(promise2,x,resolve,reject);
}catch(e){
reject(e);
}
},0)
})
}
})
return promise2;
}
// 用来做测试用的 使用语法糖的原理
// 语法糖 (甜) 目的是解决promise嵌套问题的 Q.derfer()
// 通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会
Promise.defer = Promise.deferred = function(){
let dfd = {};
dfd.promise = new Promise((resolve,reject)=>{
dfd.resolve = resolve;
dfd.reject = reject;
})
return dfd;
}
// 安装一个插件 npm install promises-aplus-tests -g 用来测试我们当前代码是否符不符合我们PromiseA+规范
/*
步骤1 cd promise 切换到当前文件的父级文件夹
步骤2 npm install promises-aplus-tests -g 安装插件
步骤3 promises-aplus-tests promise.js(当前文件名) 当前代码是否符不符合我们PromiseA+规范
*/
module.exports = Promise;
复制代码
写到这里,Promise的原理已经完成一大半了
前面不是讲了使用Promise.all方法就可以解决多个回调同步结果的问题,在原先的基础上我们动手写一个all方法
// all执行后会返回一个新的promise,所以promise执行成功后才算成功,返回值是数组类型,有一个失败就失败了(&&)
Promise.all = function(promises){
return new Promise((resolve,reject)=>{
let arr = [],
len = promises.length,
i = 0;
function processData(i,data){
arr[i] = data;
if(++i === len){ // 全部成功才执行成功的回调
resolve(data);
}
}
// 执行顺序是按数组的顺序
for(let i = 0; i < len; i++){
promises[i].then(data=>{ // data是成功的值
processData(i,data);
},reject); // 只要一个失败就执行失败的回调
}
})
}
复制代码
如果你还想测试多个异步同时发送请求,哪个请求的速度最快可以写一个Promise.race方法
// 以请求最快的为准
Promise.race = function(promises){
return new Promise((resolve,reject)=>{
for(let i = 0;i<promises.length;i++){
promises[i].then(resolve,reject);
}
})
}
复制代码
最后补充几点
1.resolve和reject可以通过类名直接调用
2.promise有一个catch方法用来捕获最近的then的异常,如果异常在then中已经处理,
那catch就失效了。catch方法的实现就是基于then的 可以不传成功 返回一个Promise的实例
复制代码
// 直接通过类名来调用resolve和reject方法,返回一个Promise的实例
Promise.resolve = function(value){
return new Promise((resolve,reject)=>{
resolve(value);
});
}
Promise.reject = function(reason){
return new Promise((resolve,reject)=>{
reject(reason);
})
}
//原型上的方法
Promise.prototype.catch = function(onRejected){
// 调then方法里面不写成功的方法
return this.then(null,onRejected);
}
复制代码
希望对大家有帮助,请多多指教。