一、JavaScript中函数的几个概念
1. 高阶函数
如果一个函数的参数是另外一个函数(回调函数)或者一个函数返回一个函数(函数柯利化),那么我们就称这个函数为高阶函数。
比如我们现在有一个功能函数,在很多地方有使用到,甚至被其他模块引入使用。我们现在需要做一个新的功能,但是大部分还是依赖之前的这个函数,这时候我们就不能改这个函数,而是基于这个函数在封装一个新函数。
// 之前的函数
function dealThings(a,b,c){
// 之前的处理逻辑
console.log('执行之前的函数逻辑', a, b, c)
}
// 现在要求在执行这个dealThings函数之前,选执行一下其他的逻辑。
// 此时我们不可以修改dealThings函数,因为很多其他地方有使用,不可以影响其他地方的逻辑
function dealThings(a,b,c){
// todo... 这里添加新的逻辑是不可以的,影响其他所有使用的地方
console.log('执行之前的函数逻辑', a, b, c)
}
我们就需要创建一个新的函数,在其中执行完新的逻辑之后在调用之前的dealThings函数。但是新的逻辑也不是固定的,需要以传入一个函数的形式去处理。
// 这里直接在Function构造函数的原型链上添加boforeDoThings方法,可以让所有的函数都有这个功能,
// 让所有的函数执行之前,都可以先执行一下指定的callback函数。
Function.prototype.boforeDoThings(callback){
// 这里接受的参数,还是之前dealThings函数锁需要的参数
return (...agrs) => {
// 这就是需要在执行dealThings之前执行的逻辑函数
callback()
// 这里使用箭头函数,让这里的this永远指向定义时作用域的this,即dealThings函数,
// 执行dealThings函数之前的处理逻辑
this(...agrs)
}
}
// 给之前的这个dealThings函数对象添加一个boforeDoThings高阶函数,用来生产新的处理函数。
const newDealThing = dealThings.boforeDoThings(function callback(){
console.log('执行添加的新逻辑')
})
// 这个newDealThing就是新封装好的处理逻辑函数,会先执行上面传入的callback函数,
// 然后再执行之前dealThings的处理逻辑。实现要求并不会对其他引用到dealThings的模块产生影响。
newDealThing(a,b,c)
2. 函数柯里化
举例说明:实现一个函数,判断一个数据是不是指定的类型?
// 简单的实现逻辑:
function checkType(data, type){
return Object.prototype.toString.call(data) === `[object ${type}]`
}
checkType(123, 'Number') // true
checkType('a', 'string') // false 不小心把String写成了string,导致检查结果不正确
上面的写法很显然可以实现,判断的类型type是固定的那几种,每次调用都要传入,很麻烦而且容易出错。采用函数柯里化的方式将类型参数固定,每次判断需要调用对应的方法,并传入对应的数据即可得到正确的结果,不必要传入对应的类型字符串,减少出错的概率。
function checkType(type){
// 这里的type就是一个闭包的私有化属性。返回的函数总可以拿到创建这个函数时对应的type,
// 不会被销毁,这里也使用到了函数的闭包的概念
return function(data) {
return Object.prototype.toString.call(data) === `[object ${type}]`
}
}
// 柯里化之后返回的判断函数更为明确,将两个参数的函数变成了一个参数的子函数
const isString = checkType('String')
const isNumber = checkType('Number')
const isArray = checkType('Array')
isString(123) // true
isNumber('a') // true
// 快速生产util里面的检测方法
const util = {}
['String', 'Number', 'Boolean', 'Array'].forEach(type => {
util[`is${type}`] = checkType(type)
})
函数柯里化就是将一个函数的范围进行缩小,让这个函数变成多个更具体的函数。通常会用到js中函数闭报的概念,并伴随着函数参数的减少。
实现一个通用的柯里化函数curring:
通过柯里化函数的方式实现add函数的功能:
const add = (a, b, c, d, e) => {
return a + b + c + d + e
}
const curring = (add, ...arr) => {
let len = add.length //总的参数长度
return (...args) => {
arr = arr.concat(args)
if(arr.length < len){
return curring(add, ...arr)
}
return add(...arr)
}
}
console.log(curring(add,1,2)(3)(4,5)) // 15
通过柯里化函数的方式实现一个函数,连续调用计算n个数值(数值)的和:
let count = 10;
const add = (...args) => {
let num = 0
args.forEach(item => num += item)
return num
}
const curring = (add, ...arr) => {
return (...args) => {
arr = arr.concat(args)
if(arr.length < count){
return curring(add, ...arr)
}
return add(...arr)
}
}
const ideatro = curring(add,1,2)(3)(4,5) // 还没有到10个参数,ideatro返回依然是一个函数
ideatro(2,4)()(4)(6,7) // 调用传入了n=10个参数,计算出结果,并返回38
结合上面两个demo实现的util对象组装:
function checkType(type){
return function(data) {
return Object.prototype.toString.call(data) === `[object ${type}]`
}
}
const curring = (add, ...arr) => {
let len = add.length //总的参数长度
return (...args) => {
arr = arr.concat(args)
if(arr.length < len){
return curring(add, ...arr)
}
return add(...arr)
}
}
const util = {};
['String', 'Number', 'Boolean', 'Array'].forEach(type => {
util[`is${type}`] = curring(checkType)(type)
})
控制一个函数在执行了n次之后才执行:
function say(){
console.log('speak~~~~')
}
// 执行3次后才执行say方法,后面在不能再次执行
function after(times, fn){
return () => {
if(--times == 0){
fn()
}
}
}
const newSay = after(3, say)
// 每执行3次后才执行一次say方法
function after(n, fn){
let count = 0
return () => {
count++
if(count >= n){
count = 0
fn()
}
}
}
const newSay = after(3, say)
控制一个函数在执行了n次之后才执行的应用场景:
const fs = require('fs')
cosnt userInfo = {}
fs.readFile('./name.txt','utf8', (err,data)=> {
userInfo.name = data
})
fs.readFile('./age.txt','utf8', (err,data)=> {
userInfo.age = data
})
console.log(userInfo) // 前面两个读取操作是异步的,这里获取的数据肯定是空对象
// 将这个获取参数的逻辑放在一个函数被执行了两次之后才会执行的函数里面
const fs = require('fs')
cosnt userInfo = {}
cosnt after = (times, fn) => () => --times === 0 && fn()
const out = after(2, () => {
console.log(userInfo)
})
fs.readFile('./name.txt','utf8', (err,data)=> {
userInfo.name = data
out()
})
fs.readFile('./age.txt','utf8', (err,data)=> {
userInfo.age = data
out()
})
// 上面的userInfo参数直接暴露在全局变量中,外部可以访问到不安去,优化一下:
const fs = require('fs')
cosnt after = (times, fn) => {
cosnt userInfo = {} //将这个存储数据的对象放在生产out函数的作用域下面,
return (key, value) => {
userInfo[key] = value // 每次调用out就会吧对应的数据存在这个userInfo对象中
if(--times === 0){ // out函数执行了两次之后,才会执行这个fn的回调函数,打印userInfo对象
fn(userInfo)
}
}
}
const out = after(2, (userInfo) => {
console.log(userInfo)
})
fs.readFile('./name.txt','utf8', (err,data)=> {
out(name, data)
})
fs.readFile('./age.txt','utf8', (err,data)=> {
out(age, data)
})
函数反柯里化: 让一个函数的范围变大
二、ES6中的Promise
1. 什么是Promise
Promise 是异步编程的一种解决方案,比传统的回调函数和事件的解决方案更合理、更强大。所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。Promise为实现者提供的一个健全、可互操作的JavaScript开放标准,参见 Promise规范文档
Promise解决的问题:
- 回到地狱:层层嵌套的回调函数,套娃式代码逻辑不好维护,错误处理也非常麻烦;
- 多个异步请求并发的问题,如果不是使用层层嵌套的回调函数,就需要使用采用发布订阅的模式或者定时器的形式完成功能,具体可参见 发布订阅模式和观察者模式。
Promise对象有以下两个特点:
- 对象的状态不受外界影响。
Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。 - 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
Promise的缺点:
- 首先,无法取消
Promise,一旦新建它就会立即执行,无法中途取消 - 其次,如果不设置回调函数,
Promise内部抛出的错误,不会反应到外部 - 第三,当处于
pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)
2. ES6中原生Promise的基本使用
Promise实际上就是一个class类,常见一个Promise实例时new一下即可。
- 在
new Promise时需要传入一个执行器函数executor,这个执行器函数默认就会被立即执行; - 每个Primise实例都有三个状态:
pending(等待中)、fulfilled(已成功)和rejected(已失败); - 默认创建一个promise的状态是
pending等待态,传入的执行器函数executor默认接受两个函数参数resolve和rejeact,resolve让promise的状态从pending变为fulfilled,rejeact让promise的状态从pending变为rejected; - 每个promise的实例都具有一个
then方法,then方法中传递两个参数:成功的回调 和 失败的回调; - 执行器函数
executor中,让promise变成失败态,只能调用rejected,实际上抛出一个错误也会让promise变成是失败态,内部其实也是在报错后调用了rejected; - 在执行器函数
executor中,如果多次调用成功或者失败,只会执行一次;
// 原生Promise简单使用介绍
let promise = new Promise((resolve, reject) => {
// 在这个执行器函数中,下面三种方式都可以将当前promise的pending状态修改为fulfilled或者rejected状态;
// 下面三种方式只有第一个方式被执行,并将promise的状态修改为相应的状态,后面的都不会被执行;
resolve('data 成功后返回的数据') //resolve将状态修改为fulfilled已成功
reject('reason 失败的原因') //reject将状态修改为rejected已失败
throw new Error('执行逻辑失败了') //throw Error将状态修改为rejected已失败
})
// promise都有一个then方法,接受 成功和失败 的回调函数
promise.then((data) => {
// 成功回调函数,接受执行器函数中修改状态为成功的resolve函数的参数
console.log('promise成功后返回的data数据', data)
}, (err) => {
// 失败回调函数,接受执行器函数中修改状态为失败的reject函数的参数,或者throw Error的错误原因
console.log('promise失败后返回的错误err数据信息', err)
})
3. Promise基本功能点实现
根据上面2中Promise的功能点,实现一个最基本最简单的Promise类
// 使用Prmose对象的index.js文件
const Promise = require('./Promise') // 开启这个模块引入下面自己实现的Promise,覆盖原生的Promise对象
let promise = new Promise((resolve, reject) => {
resolve('data 成功后返回的数据') //resolve将状态修改为fulfilled已成功
// reject('reason 失败的原因') //reject将状态修改为rejected已失败
// throw new Error('执行逻辑失败了') //throw Error将状态修改为rejected已失败
})
// promise都有一个then方法,接受 成功和失败 的回调函数
promise.then((data) => {
// 成功回调函数,接受执行器函数中修改状态为成功的resolve函数的参数
console.log('promise成功后返回的data数据')
}, (err) => {
// 失败回调函数,接受执行器函数中修改状态为失败的reject函数的参数,或者throw Error的错误原因
console.log('promise失败后返回的错误err数据信息')
})
// 自实现的Prmose.js文件
// 定义promise的三个状态常量
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'
module.exports = class Promise{
constructor(executor){
// new创建的每个promise实例都有一个状态,他的值只可能有 PENDING、FULFILLED、REJECTED三种状态
this.state = PENDING;
// 存储promise对象执行后的结果,给到than方法中获取并传出
this.resolveData = null
this.rejectData = null
// 每个promise都有一个resolve函数将promise的状态由pending改为fulfilled
let resolve = (data) => {
// 只有在state为PENDING态的时候才可以修改promise实例的状态
if(this.state === PENDING){
this.resolveData = data
this.state = FULFILLED
}
}
// 每个promise都有一个resolve函数将promise的状态由pending改为rejected
let reject = (err) => {
// 只有在state为PENDING态的时候才可以修改promise实例的状态
if(this.state === PENDING){
this.rejectData = err
this.state = REJECTED
}
}
// 执行执行器函数executor时,如果报错了,将错误传给rejecat函数并执行抛出
try {
executor(resolve, rejecat)
} catch (error) {
// 执行报错了,将错误传给rejecat函数并执行抛出
rejecat(error)
}
}
// 定义promise的then方法,传入promise的成功和失败函数回调
then(onFulfilled, onRejected) {
if(this.state === FULFILLED){
// 如果成功了执行onResolve方法,并将执行的resolve函数的data数据传给这个onResolve方法
onFulfilled(this.resolveData)
}else if(this.state === REJECTED){
// 如果失败了执行onReject方法,并将执行的rejecat函数的data数据传给这个onResolve方法
onRejected(this.rejectData)
}
}
}
上面最最基础的Promise,如果使用下面的调用方式,在执行器函数executor异步执行resolve或者rejecat函数,并且同一个promise实例多次调用then方法的时候,会出问题,then中的回调函数都不会执行。
const Promise = require('./Promise')
let promise = new Promise((resolve, reject) => {
setTimeout(() => { // 这里的代码异步执行,下面的then方法会先执行
resolve('data 成功后返回的数据') //resolve将状态修改为fulfilled已成功
// reject('reason 失败的原因') //reject将状态修改为rejected已失败
// throw new Error('执行逻辑失败了') //throw Error将状态修改为rejected已失败
},1000)
})
promise.then((data) => {
console.log('promise成功后返回的data数据1', data)
}, (err) => {
console.log('promise失败后返回的错误err数据信息1', err)
})
promise.then((data) => {
console.log('promise成功后返回的data数据2', data)
}, (err) => {
console.log('promise失败后返回的错误err数据信息2', err)
})
第一个问题:在执行器函数executor中采用异步的方式调用resolve或者rejecat函数的时候,promise实例调用then方法是同步执行的,所有会在then执行回调的时候resolve或者rejecat还没有执行,此时promise实例的状态还是pending(等待中),所以then中的onFulfilled和onRejected都不会执行;
第二个问题:同一个promise实例多次调用then方法的时候,正常每一个then里面对应的回调函数依然会被执行,如果执行器函数里面是异步的调用resolve或者rejecat函数修改promise实例的状态,那么在执行then的时候promise实例状态为pending(等待中),这时候应该将多次调用then成功和失败的回调函数存起来。
// 定义promise的三个状态常量
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'
module.exports = class Promise{
constructor(executor){
this.state = PENDING;
this.resolveData = null;
this.rejectData = null;
this.onResolvedCallbacks = []; // 存放成功的回调函数
this.onRejectedCallbacks = []; // 存放成功的回调函数
let resolve = (data) => {
if(this.state === PENDING){
this.resolveData = data
this.state = FULFILLED
// 状态变更为FULFILLED之后,再依次执行成功的回调函数
this.onResolvedCallbacks.forEach(fn => fn())
}
}
let reject = (err) => {
if(this.state === PENDING){
this.rejectData = err
this.state = REJECTED
// 状态变更为REJECTED之后,再依次执行失败的回调函数,保存函数列表的时候没有传入参数,所以这里要将err的参数传入
this.onRejectedCallbacks.forEach(fn => fn(err))
}
}
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
then(onFulfilled, onRejected) {
if(this.state === FULFILLED){
onFulfilled(this.resolveData)
}
if(this.state === REJECTED){
onRejected(this.rejectData)
}
// 如果调用then方法的时候,promise既没有成功也没有失败,就将成功和失败的回调函数存起来
if(this.state === PENDING){
this.onResolvedCallbacks.push(() => {
// TODO... AOP切片编程,执行回调之前可以再添加一些其他的处理逻辑。
onFulfilled(this.resolveData)
})
// 也可以直接将回调函数存起来,但是在执行的时候记得传入对应的参数
this.onRejectedCallbacks.push(onRejected)
}
}
}
4. ES6中Promise的链式调用
应用案例:读取一个文件(address.txt)的内容,根据读取到的这个文件的内容再去读取类外一个文件(home.txt)的内容。
不使用Promise的原生node写法:
const fs = require('fs');
const path = require('path');
fs.readFile(path.resolve(__dirname, './files/address.txt'), 'utf8', (err, data) => {
if(err) throw Error(err)
console.log(data)
if(data){
fs.readFile(path.resolve(__dirname, data), 'utf-8', (err, userStr) => {
if(err) throw Error(err)
console.log(userStr)
const info = JSON.parse(userStr)
console.log(`${info.name}的年龄是${info.age}`)
})
}
})
显然上面的方式代码会出现层次的嵌套,出现会掉地狱的情况,代码不好维护,而且没有个地方都需要对错误进行单独的处理。下面采用Promise的链式调用的方式实现:
const fs = require('fs');
const path = require('path');
function getPath(filePaht){
return path.resolve(__dirname, filePaht)
}
function read(filePaht, unicode = 'utf8') {
return new Promise(function(resolve, reject){
fs.readFile(getPath(filePaht), unicode, (err, data) => {
if(err){
reject(err)
}else{
resolve(data)
}
})
})
}
read('./files/address.txt').then(data => {
read(data).then(userStr => {
console.log(userStr)
const info = JSON.parse(userStr)
console.log(`${info.name}的年龄是${info.age}`)
}, err => {
throw Error(err)
})
},err => {
throw Error(err)
})
可以看出上面的写法知识嵌套了一个promise的函数,还是多层的嵌套在一起。这中写法依然会层在相同的问题,实际上Promise可以采用链式调用的方式解决这个问题,知识不是上面的这种写法,在内层调用的时候,直接返回一个promise的实例,外层继续使用then方法接受内层返回的值。
promise的成功回调和失败回调都可以返回一个结果,这个结果可以是任意的数据类型。通常情况有:
- 情况一:如果返回的值是一个primise,那么会让这个promise立即执行,并且采用它的状态,将成功或者失败的结果传递给外层的下一个then中
- 情况二:如果返回的是一个普通值,会把这个值作为外层的下一次then的成功回调函数的参数, 即使上一层中返回了一个错误,下一层的成功回调中依然可以回去到这个错误信息
- 情况三:抛出一个异常,会让下一层的then执行失败的回调函数
- 情况四:如果在这个链式调用中抛出了错误,没有一个then的失败回调接受处理,会导致程序报错而终止,通常可以在最后使用catch方法兜底捕获链式调用没有被捕获的错误,并且catch之后还可以继续调用then方法
const fs = require('fs');
const path = require('path');
function getPath(filePaht){
return path.resolve(__dirname, filePaht)
}
function read(filePaht, unicode = 'utf8') {
return new Promise(function(resolve, reject){
fs.readFile(getPath(filePaht), unicode, (err, data) => {
if(err){
reject(err)
}else{
resolve(data)
}
})
})
}
read('./files/address.txt').then(data => {
// 第一层 调用之后返回一个promise,这个promise回立即执行,将数据传递到下一层
return read(data)
}).then(userStr => {
// 第二层 接受第一层执行返回的promise成功之后的数据
const info = JSON.parse(userStr)
console.log(222,`${info.name}的年龄是${info.age}`)
return '第二层成功回调后返沪的数据'
}, err => {
// 第二层 接受第一层执行返回的promise失败之后的数据
console.log(err)
return err
}).then(data => {
// 第三层 接受第二层执行之后的数据,不管成功还是失败,都返回一个普通的数据类型,都是第三层的成功回调接受到数据并执行。
console.log(333, '第三层接受到第二层返回的数据', data)
// 第三层执行过程中跑出一个了错误
throw Error('第三层执行报错')
}).then(data => {
// 第四层 由于第三层抛出了一个错误,第四层的成功回调不会执行
console.log(data)
}, err => {
// 第四层 由于第三层抛出了一个错误,执行第四层的失败回调,并
console.log(444, err)
}).then(data => {
// 第五层 由于第四层没有返回值,相当于返回了undefined,所以第五层的成功回调获取的data为undefined
console.log(555, data) // undefined
// 如果希望这个then的在下一次走then的失败函数。 可以直接抛出一个错误或者返回一个promie的reject状态
// throw Error('抛出错误让下一个then执行失败的回调')
return Promise.reject('返回一个promise的reject态')
}).then(null, (err) => {
console.log(666, err)
throw Error('第六层 抛出一个错误之后,后面没有then的错误回调接受处理,会让程序报错而终止')
}).catch(err => {
console.log(777, err)
}).then((data) => {
console.log(888, data) // undefined
})
5. Promise链式调用的实现
采用3中自己实现的简易Promise实现这个链式调用的功能。 每个promise执行完成之后都返回一个全新的Promise实例,这样保证之前的promise变更之后就是固定不变的,返回新的promise为新的状态。
在链式调用的成功或者失败回调函数中,有如下三种情况将结果传递给下一个then的回调函数中。
- 1.链式调用中返回一个普通的数据类型,会被下一个then的成功回调接受
- 2.链式调用中抛出错误了,会被捕获并传递到下一个then的失败回调中
- 3.链式调用中返回一个新的promise
const Promise = require('./Promise') // 开启这个模块映入就会引入自实现的Promise,覆盖原生的Promise
let promise1 = new Promise((resolve, reject) => {
resolve('第一个promise执行成功传入的数据!')
// reject('第一个promise执行成功error!')
})
let Promise2 = promise1.then((data) => {
console.log('接受第一个promise执行成功的数据:', data)
// 1.链式调用中返回一个普通的数据类型,会被下一个then的成功回调接受
// return '第二层promise成功回调返回到数据'
// 2.链式调用中抛出错误了,会被下一个then的失败回调接受
// 第二层执行回调抛错了,依然可以被下一层then的失败回调捕获到
// throw Error('第二层执行成功回调的时候抛出一个错误!')
// 3.链式调用中返回一个新的promise
return new Promise((resolve, reject) => {
resolve('返回新的promise的成功回调执行了!')
})
}, err => {
console.log('接受第一个promise执行失败的数据:', err)
return '第二层promise失败回调返回到数据'
})
let promise3 = Promise2.then((data) => {
console.log('接受第二层promise执行返回的数据:', data)
},err => {
console.log(11222, err)
})
下面的then方法,在链式调用的时候,上一个promise返回一个普通的数据类型或者报错了,都能正确的执行下去。但是如果上一个promise在执行回调函数的时候返回了一个新的promise实例,那么x接受的是一个promise的实例对象,并没有让这个promise的回调函数立即执行,这和原生的promise不相符的。
module.exports = class Promise{
constructor(executor){
this.state = PENDING;
this.resolveData = null
this.rejectData = null
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
let resolve = (data) => {
if(this.state === PENDING){
this.resolveData = data
this.state = FULFILLED
this.onFulfilledCallbacks.forEach(cb => cb())
}
}
let rejecat = (err) => {
if(this.state === PENDING){
this.rejectData = err
this.state = REJECTED
this.onRejectedCallbacks.forEach(cb => cb())
}
}
try {
executor(resolve, rejecat)
} catch (error) {
// 链式调用的时候,依然是new一个新的promise,如果抛出了错误,这里已让可以被捕获到并且传到下一次then的失败回调中处理
this.rejecat(error)
}
}
then(onFulfilled, onRejected) {
// 调用then方法之后返回一个全新的promise实例
// promise的执行器函数会立即执行,可以将之前的逻辑放入到当前promise的执行器函数中,
// 并且可以让新的primise实例获取到上一个primise执行的结果,并将结果传出
return new Promise((resolve, reject) => {
if(this.state === FULFILLED){
let x = onFulfilled(this.resolveData)
// 1. 这里如果返回的x是普通数据类型或者报错,都能被下一个then正确捕获和执行
// 2. 如果返回的x是一个全新的promise实例,这里不应该直接将promise实例直接返回,而是调用这个promise实例的then方法
resolve(x)
}
if(this.state === REJECTED){
let x = onRejected(this.rejectData)
// 这里不管之前的primise是成功还是失败,只要返回的是普通的数据类型,就调用新的promise实例的resolve成功回调
resolve(x)
}
if(this.state === PENDING){
this.onFulfilledCallbacks.push(() => {
let x = onFulfilled(this.resolveData)
resolve(x)
})
this.onRejectedCallbacks.push(() => {
let x = onRejected(this.rejectData)
resolve(x)
})
}
})
}
}
为了让链式调用中,上一个promise返回的promie实例的回调函数被立即执行,我们需要判断这个返回值x的类型,并且如果这个x是一个promise对象,就让这个promise执行then方法即可,这样就可以继续返回一个promise实例。
根据返回值x的不同来进行不同的处理,这里将调用resolve方法抽取成一个公共的resolvePromise方法,替换上面调用resolve的那个方法,并将链式调用时生成的promise的resolve和reject回调函数传入进去。在如下实现中,同时还考虑到一下几点问题的处理逻辑,详细见代码注释。
- 1.then方法可以不传入任何的成功和失败回调函数,所以then的onFulfilled, onRejected回调函数参数要做默认函数的处理;
- 2.当执行一个primise的成功或者失败回调的时候,再次返回了一个promise对象,需要在对应resolve的时候再次执行这个promise的then方法,即递归调用这个resolvePromise方法;
- 3.返回值x是一个promise,需要防止它的成功和失败回调函数被多次调用,要确保只有第一次被调用;
- 4.如果执行promise的成功或者失败回调函数时,传入的参数就是一个新promise,需要立即执行这个primise的then方法
// 定义promise的三个状态常量
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'
function resolvePromise(promise2, x, resolve, reject) {
// 按照promise的实现标准文档可以看出,这里有很多的兼容性判断和错误检测校验,
// 甚至当前封装的Promise和其他人实现的Primise混用,不同promise库之间相互调用,需要处理兼容问题
if(promise2 === x){
// x 如果和 promise2 同一个实例对象,就是自己等待自己,这是不可能,抛出标准文档中指定的错误
return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
}
// ============== 判断x的状态,是不是promise ==============
// 1. 先判断x是不是对象或者函数
if(typeof x === 'object' && x !== null || typeof x === 'function'){
// x是一个对象或者是函数,再判断是否有then的方法。
// 这里在取出then方法的时候,如果这里then方法采用 definedProperty来定义,读取可能回报错,所以 try catch 一下
let called; // 这个标记为了防止x的promise状态执行了多次,保障只有第一次被执行;同事限制x这个promise的状态只能被改变一次
try {
const then = x.then
if(typeof then === 'function'){
// 判断then是不是一个函数
// 如果then是一个函数,只能认为这个x是一个promise,采用call绑定这个x为this执行then方法,没有直接x.then的方式调用就是防止下一次读取then的时候可能会报错,直接call调用前面已经读取到的
then.call(x, (y) => { // x是一个promise,采用这个promise成功的回调结果返回
if(called) return
called = true
// 为了防止调用的时候,这个y返回的依然是一个promise,就地柜调用这个resolvePromise处理函数
resolvePromise(promise2, y, resolve, reject)
}, (r) => {
if(called) return
called = true
reject(r) // x是一个promise,采用这个promise失败的回调结果返回
})
}else{
if(called) return
called = true
// 如果then不是一个函数,说明不是promise x = {then: 123}
resolve(x)
}
} catch (e) {
if(called) return
called = true
// 取then时报报错了,直接出发promise2的失败逻辑
reject(e)
}
}else{ // 肯定不是promise,普通的值直接返回
resolve(x)
}
}
class Promise{
constructor(executor){
this.state = PENDING;
this.resolveData = null
this.rejectData = null
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
let resolve = (data) => {
// 判断resolve传入的结果是不是promise,如果是promise的话执行这个primise的then方法并返回一个新的promise
if(data instanceof Promise){
return data.then(resolve, reject)
}
if(this.state === PENDING){
this.resolveData = data
this.state = FULFILLED
this.onFulfilledCallbacks.forEach(cb => cb())
}
}
let reject = (err) => {
if(this.state === PENDING){
this.rejectData = err
this.state = REJECTED
this.onRejectedCallbacks.forEach(cb => cb())
}
}
try { // try catch只能捕获痛同步的错误
// the方法中执行构造器函数的时候,为了能拿到promise2的实例,采用异步的方式,这里就不能正常捕获到executor中的报错
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
then(onFulfilled, onRejected) {
// 可选参数的处理,promise的then方法中的成功和失败回调函数是可选的,
// 不传回调函数的时候上一个primise的数据会一直往后面的promise中传递。
// 1. 没有成功回调的时候,将上一个成功的data一直往下传递,即 默认的成功回调函数为: v => v
// 2. 没有失败回调的时候,将上一个失败的reject错误或者抛出的错误一直往下抛出,即 默认的失败回调函数为: err => {throw err}
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : data => data
onRejected = typeof onRejected === 'function' ? onRejected : err => {throw err}
let promise2 = new Promise((resolve, reject) => {
if(this.state === FULFILLED){
setTimeout(() => {
// 这里面异步执行,在上面try catch调用构造器函数的时候,无法捕获到发生的错误,
// 所以这里需要重新try catch捕获错误,错误时将错误信息传递给then的失败回调
try {
let x = onFulfilled(this.resolveData)
// 抽取resolvePromise处理返回值x为不同类型时候的状态
// 这里的resolvePromise要获取到promise2的引用,但是这个时候还在new生成promise2,所以要放在异步中执行这个逻辑
resolvePromise(promise2, x, resolve, reject)
} catch (err) {
reject(err)
}
});
}
if(this.state === REJECTED){
setTimeout(() => {
try {
let x = onRejected(this.rejectData)
resolvePromise(promise2, x, resolve, reject)
} catch (err) {
reject(err)
}
});
}
if(this.state === PENDING){
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.resolveData)
resolvePromise(promise2, x, resolve, reject)
} catch (err) {
reject(err)
}
});
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.rejectData)
resolvePromise(promise2, x, resolve, reject)
} catch (err) {
reject(err)
}
});
})
}
})
return promise2
}
// catch方法实际上就是then方法的一种不传成功回调then(null, err => {})的调用方式
catch(rejectCallback) {
return this.then(null, rejectCallback)
}
// promise 的静态方法
static resolve(data){
return new Promise((resolve, reject) => {
reject(data)
})
}
static reject(err){
return new Promise((resolve, reject) => {
reject(err)
})
}
}
module.exports = Promise
6. 对实现的Promise进行标准测试
官方提供promises-aplus-tests库,可以对实现的promise进行自动化测试,npm全局安装promises-aplus-tests包,并给自己实现的Promise添加如下的deferred静态方法,然后使用promises-aplus-tests命令运行当前的字实现promise:
Promise.deferred = function(){
let dtd = {}
dtd.promise = new Promise((resolve,reject) => {
dtd.resolve = resolve
dtd.reject = reject
})
return dtd
}
// 安装完包之后执行命令开始测试:promises-aplus-tests ./Promise.js
如果实现的Promise完全符合标准,则测试提示全部成功,否则回给出对应不符合标准的错误提示,进行优化修改。通过本地的自动化检测,5中最后实现的完整Promise通过了全部的检测,说明这个Promise符合promise的官方规范,撒花~~🌸🌼🌻🌹🌷
三、Promise的其他方法
1. Promise的resolve方法
Promise的resolve方法是部署在Promise类上面的静态方法,将现有对象转为 Promise 对象,并且这个promise对象的状态为fulfilled状态。
const jsPromise = Promise.resolve($.ajax('/whatever.json'));
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
Promise.resolve()方法的参数分成四种情况:
- 1.参数是一个 Promise 实例,那么
Promise.resolve将不做任何修改、原封不动地返回这个实例。 - 2.参数是一个
thenable对象,thenable对象指的是具有then方法的对象,Promise.resolve()方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then()方法。 - 3.参数不是具有
then()方法的对象,或根本就不是对象,Promise.resolve()方法返回一个新的 Promise 对象,状态为resolved - 4.不带有任何参数,直接返回一个
resolved状态的 Promise 对象。 原生实现resolve方法
Promise.resolve = fucntion(data){
function isPromise(data){
if(typeof data === 'object' && data !== null || typeof data === 'function'){
return typeof data.then === 'function'
}else{
return false
}
}
return new Promise((resolve, reject) => {
if(isPromise(data)){
data.then(value => resolve(value), err => reject(err))
}else{
return resolve(data)
}
})
}
2. Promise的reject方法
Promise的reject方法是部署在Promise类上面的静态方法,将现有对象转为 Promise 对象,并且这个promise对象的状态为rejected状态。
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function (s) {
console.log(s)
}); // 出错了
原生实现reject方法
Promise.reject = fucntion(err){
return new Promise((resolve, reject) => {
return reject(err)
})
}
3. Promise的all方法
Promise的all方法是部署在Promise类上面的静态方法,用来将多个Promise实例包装成一个新的Promise实例。Promise.all()方法接受一个可迭代的对象(具有Iterator接口)作为参数,对象的每一项都是一个Promise实例,如果不是,则会先调用Promise.resolve方法将其转为Promise实例,再进一步处理。
执行中等待所有promise全部成功之后,返回一个新的promise实例,如果这一系列的promise中有一个失败了,则返回的promise状态就是失败,第一个被reject的实例的返回值,会传递给新的promise实例的回调函数,用法如下:
const fs = require('fs').promises
Promise.all([1, 2, 3, fs.readFile(__dirname + '/address.txt', 'utf8'), 4, 5]).then(results => {
console.log(results) //返回成功的数据:[ 1, 2, 3, './files/home.txt', 4, 5 ]
}, err => {
console.log(err)
})
Promise.all([1, 2, 3, fs.readFile(__dirname + '/address11.txt', 'utf8'), 4, 5]).then(results => {
console.log(results)
}, err => {
console.log(err) // 读取文件失败,返回错误 [Error: ENOENT: no such file or directory
})
原生实现all方法
Promise.all = function(promises){
return new Promise((resolve, reject) => {
if (promises.length === 0) {//如果数组长度为0则返回空数组
resolve([]);
return
}
let resList = []
let i = 0 //记录执行后的结果,同时保证输出的结果顺序和输入的顺序一一对应
// 判断一个数据是不是pomise实例
function isPromise(data){
if(typeof data === 'object' && data !== null || typeof data === 'function'){
return typeof data.then === 'function'
}else{
return false
}
}
// 判断一个数据是不是pomise实例的简单方式
function isPromise(data){
// return Object.prototype.toString.call(data) === '[object Promise]'
return data.toString() === '[object Promise]'
}
function dealData(data,index){
resList[index] = data
// 什么时候resolve返回?当添加的结果数量和传入的数量相同时resolve
if(++i === promises.length){
resolve(resList)
}
}
promises.forEach((item, index) => {
// 判断是不是promise实例
if(isPromise(item)){
item.then(data => {
// 这里不能直接使用push方法 resList.push(item),因为promise的异步执行,会导致返回的结果数据顺序和参数的顺序不一致
// 采用index序号标记
dealData(data, index)
}, err => {
return reject(err)
})
}else{
dealData(item, index)
}
})
})
}
function promiseAll2 (promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) {
return reject(new Error('必须传入一个数组类型'));
}
const len = promises.length;
const result = [];
let index = 0; // 当前处理的promise数量
for (let i = 0; i < len; i++) {
// if (promises[i].toString() === '[object Promise]') {
// promises[i].then(data => {
// result.push(data) // 直接push是不行的,不能保证结果数组和传入的数组的顺序是否一一对应
// })
// } else {
// result.push(promises[i]) // 结果要根据判断是否为promises数据来以不同的方式操作结果。直接 Promise.resolve 全部转换为promise即可
// }
Promise.resolve(promises[i]).then(data => {
result[i] = data;
index++;
if (index === len) {
resolve(result); // 执行结束了就resolve返回结果
}
}).catch(err => {
reject(err);
})
}
})
}
4. Promise的race方法
Promise的race方法同all方法一样,都是部署在Promise类上面的静态方法,用来将多个Promise实例包装成一个新的Promise实例。执行过程中如果Promise.race()参数中的任一个Promise的实例状态发生变化,则新的Promise实例的状态也随之变化,那个状态率先改变的Promise的返回值,就传递给新的Promise的回调函数作为参数。
var p1 = new Promise((resolve, reject) => {
setTimeout(reject, 100, 'foo'); //这个promise的状态最先改为Rejected
});
var p2 = new Promise((resolve, reject) => {
setTimeout(resolve, 200, 'fun');
});
var p3 = new Promise((resolve, reject) => {
setTimeout(resolve, 300, 'bad');
});
Promise.race([p1, p2, p3]).then(values => {
console.log('Resolved: ' + values);
}, values => {
console.log('Rejected: ' + values);
}); // Rejected: foo
原生实现race方法
Promise.race = function(promises){
return new Promise((resolve) => {
if (promises.length === 0) {//如果数组长度为0则返回空数组
return
}
for(let i = 0; i < promises.length; i++){
// Promise.resolve将非promise的数据转换为promise的实例
Promise.resolve(promises[i]).then(data => {
resolve(data)
}, err => {
resolve(err)
})
}
})
}
5. Promise的finally方法
Promise的finally是部署在promise实例上面的方法,用于指定不管 Promise 对象最后状态如何,都会执行的操作。finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果
new Promise((resolve, reject) => {
resolve(100)
}).finally(() => {
console.log('hello')
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(1000)
}, 2000)
})
}).then((data) => {
console.log('success:', data)
},(err) => {
console.log('err:', err)
}).catch(err => {
console.log('catch err:', err)
})
原生实现finally方法
Promise.prototype.finally = function (callback) {
let P = this.constructor; //拿到实例对应的Promise构造函数,一般也可以直接使用Promise构造函数
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
)
}
四、Promise的应用
1. Promise实现缓存
// 实现一个类方法的缓存装饰器
const cacheMap = new Map();
function enableCache (target, name, descriptor) {
const val = descriptor.value;
descriptor.value = async function (...args) {
const cacheKey = name + JSON.stringify(args);
if (!cacheMap.get(cacheKey)) {
const cacheValue =
Promise.resolve(val.apply(this, args))
.cache(_ => {
cacheMap.set(cacheKey, null)
})
cacheMap.set(cacheKey, cacheValue)
}
return descriptor
}
}
class PromiseClass {
@enableCache
static async getInfo () {}
}
PromiseClass.getInfo() // 请求数据
PromiseClass.getInfo() // 同样的请求读取缓存的数据