一,为什么要学Promise?
我们知道js中代码执行的顺序是从上到下同步执行,那么要想在同步函数中拿异步函数中的结果,我们会怎么做呢?
细想之下除了回调函数目前还没有其他的办法,而Promise的原理就是回调函数,你可能要问了,那干脆直接用回调函数不就行了,当然可以,不过不过优雅,而Promise就很优雅。
1.Promise的语法
// Promise 是es6新增的一个类, 可以解决异步操作上的弊端和问题
var fs = require("fs")
// 需求: 使用fs模块异步读取一个文件, 在异步回调函数外部获取并使用读取结果
// 方案1: 使用全局变量记录异步结果并在需要的地方打印
var result = null;
fs.readFile("./1, es6简介.txt", function(err,data){
console.log(1, err, data)
result = data.toString()
})
console.log(2, result) // 空数据
// 方案2: 使用函数返回值把异步结果返回,外部调用函数得到返回值
function method(){
fs.readFile("./1, es6简介.txt", function(err,data){
return data.toString();
})
}
console.log(3, method()); // undefined
setTimeout(() => {
console.log(4, method()); // undefind
}, 1000);
// 方案3: 使用回调函数的结构,把异步结果放入回调函数中, 在回调函数中获取异步结果
function method1(callback){
fs.readFile("./1, es6简介.txt", function(err,data){
callback(data)
})
}
method1(function(result){
console.log(5, result)
})
// 总结: 以上三种方案, 1,2是传统方案, 拿不到异步结果, 第三种使用回调函数的写法可以拿到结果, 第三种方案就是一个最简单的promise结构 (只是promise的雏形)
// 以下是es6中promise基本语法
// 新建一个promise对象, 参数是回调函数, 回调函数有两个参数
var promise = new Promise(function(resolve, reject){
// resolve 和 reject 也是两个函数,
// 在promise对象中开始一个异步操作
fs.readFile("./1, es6简介.txt", function(err,data){
// 如果异步任务出错,就执行reject函数,传入错误信息
if(err) reject(err.message);
// 如果异步任务成功, 就执行resolve函数, 传入成功的数据
else resolve(data)
})
})
// 用promise对象调用then方法,传入成功的回调函数,对应resolve
promise.then(function(res){
console.log(6, res)
})
// 用promise对象调用catch方法,传入失败的回调函数,对应reject
promise.catch(function(err){
console.log(6, err)
})
// 上边是分别调取成功和失败的回调, 也可以使用下边的链式调用结构, 因为then的返回值依然是这个promise对象
promise.then(function(res){
console.log(6, res)
}).catch(function(err){
console.log(6, err)
})
// 其实then函数可以直接传入两个回调,一个成功函数一个失败函数
promise.then(function(res){
console.log(6, res)
},function(err){
console.log(6, err)
})
// 注意:
// 1, 成功的回调函数resolve是必选的,必须在then中传入, 失败的回调函数reject是可选的, 可以省略
// 2, then函数获取promise异步结果不管在任何时间,任何位置调用, 不管调用多少次, 总能拿到异步结果
2.Promise的用法
// 需求: 读取data目录下的4个文件中的4句诗, 并按顺序拼接
var fs = require("fs")
// 方案一: 使用fs同步读取
var data1 = fs.readFileSync("./data/a.txt")
var data2 = fs.readFileSync("./data/b.txt")
var data3 = fs.readFileSync("./data/c.txt")
var data4 = fs.readFileSync("./data/d.txt")
console.log(1, data1 + data2 + data3 + data4)
// 缺点: 大量的同步操作会阻塞进程,造成代码响应迟缓,降低效率
// 方案二: 使用fs异步读取
fs.readFile("./data/a.txt",function(err,data1){
fs.readFile("./data/b.txt",function(err,data2){
fs.readFile("./data/c.txt",function(err,data3){
fs.readFile("./data/d.txt",function(err,data4){
console.log(2, data1+data2+data3+data4)
})
})
})
})
// 缺点: 多个文件读取造成异步回调多层嵌套, 结构复杂, 可读性差
// 方案三: 使用promise解决异步任务多层嵌套问题
new Promise(function(resolve){
fs.readFile("./data/a.txt",function(err,data){
resolve(data)
})
}).then(function(data1){
return new Promise(function(resolve){
fs.readFile("./data/b.txt",function(err,data2){
resolve(data1+data2)
})
})
}).then(function(data12){
return new Promise(function(resolve){
fs.readFile("./data/c.txt",function(err,data3){
resolve(data12+data3)
})
})
}).then(function(data123){
return new Promise(function(resolve){
fs.readFile("./data/d.txt",function(err,data4){
resolve(data123+data4)
})
})
}).then(function(data){
console.log(3, data)
})
// 缺点: promise虽然解决了多任务嵌套问题, 但是以上三种方案存在一个共性弊端, 就是多异步任务并发执行的问题, 如,a,b,c,d四个文件的读取, 上边的写法都是按顺序读取,上一个读完再读下一个, 相当于过独木桥, 每个时刻只有一个文件在读, 效率很低, 假设 四个文件读取分别耗时 5ms, 12ms, 8ms, 6ms 那么读取结束需要的总时间是 31ms , 如果可以让多个异步同时开始执行也就是并发执行, 就可以个大大提高执行效率
// 方案四: 使用promise解决多异步任务并发执行问题
var p1 = new Promise(function(resolve){
fs.readFile("./data/a.txt",function(err,data){
resolve(data)
})
})
var p2 = new Promise(function(resolve){
fs.readFile("./data/b.txt",function(err,data){
resolve(data)
})
})
var p3 = new Promise(function(resolve){
fs.readFile("./data/c.txt",function(err,data){
resolve(data)
})
})
var p4 = new Promise(function(resolve){
fs.readFile("./data/d.txt",function(err,data){
resolve(data)
})
})
// 注意: promise中的异步任务是在new创建是直接开始执行的, then函数只是读取异步结果, 而不是开始执行异步请求
// Promise 中提供了两个类方法: all race
// 把多个promise对象合并成一个, 参数是数组,数组中放多个promise示例, 返回一个新的promise对象
var mergeP = Promise.all([p1,p2,p3,p4])
// 使用合并后的对象调用then获取所有promise的结果, 是一个数组,数组中数据顺序和all参数顺序保持一致 (注意不是按照执行结束的先后顺序排列)
mergeP.then(function(dataArr){
console.log(4, dataArr.join(""))
})
// all: 当合并的所有promise全部完成之后, 才会执行then回调,拿到所有结果
// race: 任意一个合并的promise任务完成, 立即调用then回调, 只能拿到这一个结果
// 类方法: 使用类名直接调用的方法 如: Promise.all() Promise.race()
// 实例方法: 创建出来的对象调用的方法 如 : p1.then(), p1.catch()
// 总结: promise的两个主要用法
// 1, 解决多异步任务多层嵌套问题
// 2, 解决多异步任务并发问题
3.Promise的原理
// promise的基本结构语法
// var promise = new Promise(function(resolve, reject){
// setTimeout(() => {
// var random = Math.random()
// if(random < 0.7) resolve("成功的数据")
// else reject("错误的信息")
// }, 1000);
// })
// promise.then(function(data){
// console.log(data)
// }, function(err){
// console.log(err)
// })
// 下边, 我们自己写一个类MyPromise, 模拟实现它的构造函数和then方法
// 自定义类的构造函数实现
function MyPromise(callback){
console.log(1, "执行了MyPromise的构造函数");
// 每一个promise对象内部,都会有一个状态信息, 有三个可能值
// pending 状态 表示等待状态, promise对象的默认状态,
// resolve 状态 表示成功状态, 当调用了resolve函数时,状态变成成功状态
// reject 状态 表示失败状态, 当调用了reject函数时,状态变成失败状态
// 注意: 状态值只能变化一次,一旦变更为成功或失败状态,则会一致保持这个状态
this.state = "pending"; // 初始化状态
console.log("out", this) // promise对象
// myResolve和myReject是在外部调用的,所以函数中this指向并不是当前promise对象
let self = this
// 定义成功时的回调函数
function myResolve(data){
console.log(3, data, this, self) // Object [global] , promise
self.state = "resolve" // 修改状态值为成功
self.value = data; // 成功时value属性记录成功数据
self.success(data) // 成功时,调用then函数中成功回调
}
// 定义失败时的回调函数
function myReject(err){
console.log(3, err, this, self)
self.state = "reject" // 修改状态值为失败
self.msg = err; // 失败时用msg属性记录失败信息
self.fail(err) // 失败时,调用then函数中失败回调
}
// MyPromise在创建对象时,其回调函数会直接执行, 所以在构造函数中直接调用, 并传入成功和失败状态对应的函数
callback(myResolve, myReject);
}
// then方法是promise对象调用的方法,所以定义到构造函数原型中
MyPromise.prototype.then = function(success, fail=()=>{}){
// 由于then函数可以在任意时刻调用, 所以调用then时,promise状态值不确定
if(this.state == "pending"){
// 说明此时异步任务还未结束, 还不能调用success或fail, 此时可以把success和fail这个回调函数传入this这个promise对象, 在异步任务结束后调用
this.success = success;
this.fail = fail;
}
if(this.state == "resolve"){
// 如果当前状态是成功状态, 则调用then参数中的第一个成功回调
success(this.value) // 参数传入成功数据
}
if(this.state == "reject"){
// 如果当前状态是失败状态, 则调用then参数中的第二个失败回调
fail(this.msg) // 参数传入失败信息
}
// then函数返回当前promise对象, 用于链式调用
return this;
}
// 使用构造函数创建实例对象, 创建时,构造函数会执行
var p = new MyPromise(function(resolve, reject){
console.log(2, '执行了myPeomise回调')
setTimeout(() => {
var random = Math.random()
if(random < 0.7) resolve("成功的数据")
else reject("错误的信息")
}, 1000);
})
// 在异步任务结束前,调用then函数
p.then(function(data){
console.log(4, data)
},function(err){
console.log(4, err)
}).then(function(){})
// 在异步任务结束后, 调用then函数
setTimeout(() => {
p.then(function(data){
console.log(5, data)
},function(err){
console.log(5, err)
})
}, 2000);