ES6 - Promise

277 阅读12分钟

Promise:对异步操作的状态管理

基本使用方法

// resolve 成功
// reject  失败
let p = new Promise((resolve, reject) => {
   setTimeout(() => {
       console.log('ES6之Promise');
       // resolve()
       // reject()
       // if(){
       //     resolve()
       // } else {
       //     reject()
       // }
       
   }, 1000);
}).then(() => {
   console.log('成功1');
}, () => {
   console.log('失败1');
})
  • then(()=>{// success},()=>{// fail})then接收2个回调作为参数,第一个是成功回调,第二个是失败回调,第一个参数必传,第二个参数可以省略
  • Promise的参数是一个回调,回调的的2个参数是函数,第一个参数resolve表示回调成功后执行的方法,第二个参数表示回调失败后执行的方法,当调用resolve的时候会进入then的第一个方法里,当调用reject的时候会进入then的第二个方法里
  • 如果异步的结果成功了,需要手动的调用下resolve(),这样才能进入下一个then的成功回调
  • 如果异步的结果失败了,需要手动的调用下reject(),这样才能进入下一个then的失败回调
  • 如果后面的异步操作想得到上一步的异步操作的结果,成功了就将成功的结果放到成功回调resolve(成功的结果)的参数中,在then的成功回调中用参数接收,如果失败了就将失败的结果放到失败回调reject(失败的结果)的参数中,在then的失败回调中参数接收

Promise与异步的关系

Promise和异步没有任何关系!

Promise中也可以放同步的代码,eg:

let p = new Promise((resolve,reject) => {
    console.log(1)
    resolve()     // 如果想进入then,必须手动调用then/reject
})
console.log(2);
p.then(res => {
    console.log(3);
})                 // 输出结果: 1  2  3

只不过大部分时候,Promise都会结合异步的操作去使用,否则就失去了Promise的意义了

new Promise里面的代码会被立即执行

then相当于是Promise的微任务,而正常的代码是一个宏任务,JS中会先进行宏任务再执行微任务

Promise的三种状态

三种状态

promise.04ed9cc2.png

Promise中只有三种状态:当 new Promise的时候它的状态是pending(进行中);如果异步执行结果成功就调用resolve(),状态变为fulfiled(已成功);如果异步执行结果失败就调用reject(),状态变为rejected(已失败)

Promise的这三种状态并不受外界的影响,只有当前异步处理的结果resolve/reject,才会决定当前Promise到底处于哪种状态,其他的操作都没办法改变它,它的状态是不可逆的

eg:

let p1 = new Promise((resolve,reject)=>{
    resolve(1)
})
let p2 = new Promise((resolve,reject)=>{
    setTimeout(() => {
        resolve(2)  
    }, 1000);
})
let p3 = new Promise((resolve,reject)=>{
    setTimeout(() => {
        reject(3)  
    }, 1000);
})
console.log(p1);                 // resolved
console.log(p2);                 // pending
console.log(p3);                 // pending
// p2、p3 resove或者reject改变状态之后的结果
setTimeout(() => {
    console.log(p2);                 
    
}, 2000);
setTimeout(() => {
    console.log(p3);                 
    
}, 2000);
// then中拿到Promise的结果
p1.then(res => {
    console.log(res);
})
p2.then(res => {
    console.log(res);
})
p3.catch(err => {
    console.log(err);
})

为什么Promise外面的状态和里面的状态不一致?

image.png

为什么输出的p2外面的Promise的状态是pending,而里面的状态又是fulfiled?

因为当我们写了new Promise(...)之后会立即执行它里面的代码,但是此时还没有resolve(),resolve()是在1秒后执行,因此转态先是pending,1秒后状态变成fulfiled

其实当我们在1秒之前打开,结果里p2的Promise的状态也显示是pending状态,注意不管是外面的还是里面的Promise的状态都是同一个P2的状态,我们只需要在意外面的状态就行

image.png

p2、p3 resove或者reject改变状态之后的结果

如果我们想看p2、p3 resove或者reject改变状态之后的结果,不妨加个定时器,2秒后,p2的resole已经执行,Promise的状态已被改变。2秒后,p3的reject已经执行,Promise的状态也已被改变。

image.png

then中拿到Promise的结果

失败的回调函数可以放到then的第二个参数中,或者使用catch,catch相当于只有失败回调的then

为什么Promise的状态是不可逆的?

let p = new Promise((resolve, reject) => {
    resolve(1)
    reject(2)
})

p.then(success => {
    console.log(success);
},err => {
    console.log(err);
})

//等价于下面的写法
p.then(success => {
    console.log(success);
}).catch(err => {
    console.log(err);
})

结果是只输出1,2并没有被输出

image.png

为什么这样?因为Promise就有“承诺/一诺千金”的意思,当前的Promise的状态如果由pending,经过resolve()变成fulfiled,那么状态就已经被“凝固”了,即使后面再调用reject那么状态也不可逆、不可被改变

使用Promise改造Callback Hell 的例子

原始的方式

1. 错误的写法

function ajax(url, callback) {
    // 1. 创建XMLHttpRequest对象
    var xtmlhttp
    if (window.XMLHttpRequest) {
        xmlhttp = new XMLHttpRequest()
        // 兼容早期浏览器
    } else {
        xmlhttp = new ActiveXObject('Microsoft.XMLHTTP')
    }
    // 2. 发送请求
    xmlhttp.open('GET', url, true)
    xmlhttp.send()
    // 3. 接收服务端相应
    xmlhttp.onreadystatechange = function () {
        if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
            var obj = JSON.parse(xmlhttp.responseText)
            callback(obj);
        }
    }

}

new Promise((resolve,reject) => {
    ajax('static/a.json',res => {
        console.log(res);
        resolve()
    })
}).then(res => {
    console.log('a读取成功');
    new Promise((resolve,reject)=>{
        ajax('static/b.json',res => {
            console.log(res);
            resolve()
        })
    })
}).then(res => {
    console.log('b读取成功');
})

看下运行的结果:

image.png

结果输出的顺序好像不对,不应该:a的结果 -> a读取成功 -> b的结果 -> b读取成功吗?

第二个then原本是想对第一个then返回的Promise进行链式操作,接着then,但是由于第一个then中的并没有返回值(没有写return),因此第二个then相当于对一个空的Promise对象进行then的操作

如果想进行链式操作,我们需要将Promise对象return出去(return出去的Promise对象相当于得到了一个新的Promise对象),以供后面的then继续的then,进行链式操作

2.修正后的写法

function ajax(url, callback) {
    // 1. 创建XMLHttpRequest对象
    var xtmlhttp
    if (window.XMLHttpRequest) {
        xmlhttp = new XMLHttpRequest()
        // 兼容早期浏览器
    } else {
        xmlhttp = new ActiveXObject('Microsoft.XMLHTTP')
    }
    // 2. 发送请求
    xmlhttp.open('GET', url, true)
    xmlhttp.send()
    // 3. 接收服务端相应
    xmlhttp.onreadystatechange = function () {
        if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
            var obj = JSON.parse(xmlhttp.responseText)
            callback(obj);
        }
    }

}

new Promise((resolve,reject) => {
    ajax('static/a.json',res => {
        console.log(res);
        resolve()
    })
}).then(res => {
    console.log('a读取成功');
    return new Promise((resolve,reject)=>{
        ajax('static/b.json',res => {
            console.log(res);
            resolve()
        })
    })
}).then(res => {
    console.log('b读取成功');
})

输出的结果的顺序也符合预期了

image.png

3. 再把读取c.json加进来

function ajax(url, callback) {
    // 1. 创建XMLHttpRequest对象
    var xtmlhttp
    if (window.XMLHttpRequest) {
        xmlhttp = new XMLHttpRequest()
        // 兼容早期浏览器
    } else {
        xmlhttp = new ActiveXObject('Microsoft.XMLHTTP')
    }
    // 2. 发送请求
    xmlhttp.open('GET', url, true)
    xmlhttp.send()
    // 3. 接收服务端相应
    xmlhttp.onreadystatechange = function () {
        if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
            var obj = JSON.parse(xmlhttp.responseText)
            callback(obj);
        }
    }

}

new Promise((resolve,reject) => {
    ajax('static/a.json',res => {
        console.log(res);
        resolve()
    })
}).then(res => {
    console.log('a读取成功');
    return new Promise((resolve,reject)=>{
        ajax('static/b.json',res => {
            console.log(res);
            resolve()
        })
    })
}).then(res => {
    console.log('b读取成功');
    return new Promise((resolve,reject)=>{
        ajax('static/c.json',res => {
            console.log(res);
            resolve()
        })
    })
}).then(res => {
    console.log('c读取成功');
})

输出的结果:

image.png

看下之前回调深渊的写法:

ajax('static/a.json',res =>{
    console.log(res)           // {a: '我是A'}
    ajax('static/b.json',res => {
        console.log(res);     // {b: '我是B'}
        ajax('static/c.json',res => {
            console.log(res); // {c: '我是C'}
        })
    })
})

上面的写法层层嵌套

使用Promise改写之后,就将之前层层嵌套的方式,改为扁平式的,代码更利于阅读和维护,也更好的管理失败后做什么,失败后做什么

但是可能会产生疑问:这样代码变得更多了

改造的方式

我们发现有很多重复的部分,每次都是返回一个Promise对象,在Promise对象中进行Ajax请求,我们不妨封装成函数

function ajax(url, callback) {
    // 1. 创建XMLHttpRequest对象
    var xtmlhttp
    if (window.XMLHttpRequest) {
        xmlhttp = new XMLHttpRequest()
        // 兼容早期浏览器
    } else {
        xmlhttp = new ActiveXObject('Microsoft.XMLHTTP')
    }
    // 2. 发送请求
    xmlhttp.open('GET', url, true)
    xmlhttp.send()
    // 3. 接收服务端相应
    xmlhttp.onreadystatechange = function () {
        if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
            var obj = JSON.parse(xmlhttp.responseText)
            callback(obj);
        }
    }

}
function getPromise(url) {
    return new Promise((resolve, reject) => {
        ajax(url, res => {
            resolve(res)
        })
    })
}
getPromise('static/a.json')
    // then里面的结果res都是通过函数getPromise中resolve传递过来的
    .then(res => {
        console.log(res);
        return getPromise('static/b.json')
    }).then(res => {
        console.log(res);
        return getPromise('static/c.json')
    }).then(res => {
        console.log(res);
    })

image.png

这样代码就变得更加的扁平化、语义化,便于阅读和对结果进行管理,这就是Promise的好处

再加入对失败结果的处理吧

function ajax(url, successCallback, failCallback) {
    // 1. 创建XMLHttpRequest对象
    var xtmlhttp
    if (window.XMLHttpRequest) {
        xmlhttp = new XMLHttpRequest()
        // 兼容早期浏览器
    } else {
        xmlhttp = new ActiveXObject('Microsoft.XMLHTTP')
    }
    // 2. 发送请求
    xmlhttp.open('GET', url, true)
    xmlhttp.send()
    // 3. 接收服务端相应
    xmlhttp.onreadystatechange = function () {
        if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
            var obj = JSON.parse(xmlhttp.responseText)
            successCallback && successCallback(obj);
        } else if (xmlhttp.readyState === 4 && xmlhttp.status === 404) {
            failCallback && failCallback(xmlhttp.statusText)
        }
    }

}
function getPromise(url) {
    return new Promise((resolve, reject) => {
        ajax(url, res => {
            resolve(res)
        }, err => {
            reject(err)
        })
    })
}
getPromise('static/aa.json')
    .then(res => {
        console.log(res);
        return getPromise('static/b.json')
    }, err => {
        console.log(err);
    }).then(res => {
        console.log(res);
        return getPromise('static/c.json')
    }).then(res => {
        console.log(res);
    })

看下运行结果:

image.png

  • Promise中不会因为第一个异步操作的结果失败而影响第二个、第三个异步操作,第二个、第三个异步操作并没有停止执行,也执行了
  • 为什么输出undefined? ==> Promise中发起的ajax请求的路径'static/aa.json'并不存在,因此第一个then会进入失败回调,然而第一个then中的失败回调并没有返回任何的Promise对象,相当于返回一个空的Promise对象,而第二个then是对第一个then失败结果返回的Promise进行then,所以第二个then中输出undefined。或者说第二个then中的res是第一个then的成功回调的结果,但是由于第一个then并没有执行成功回调而是执行失败回调,所以第二个then中的成功回调res结果是undefined
  • 为什么还能正确的输出{c:'我是C'}? ==>因为它的上一个then中对'static/c.json'发起了ajax请求,并成功的返回了Promise对象(返回了成功函数的结果)

aa.json读取失败但是不要影响b.json的读取

aa.json读取失败后,也继续返回Primise对象,对'static/b.json'发起ajax请求

getPromise('static/aa.json')
    .then(res => {
        console.log(res);
        return getPromise('static/b.json')
    }, err => {
        console.log(err);
        // a.json读取失败但是不要影响b.json的读取 
        return getPromise('static/b.json')
    }).then(res => {
        console.log(res);
        return getPromise('static/c.json')
    }).then(res => {
        console.log(res);
    })

输出的结果

image.png

不对失败的结果进行单独的处理,而是进行统一的处理

function ajax(url, successCallback, failCallback) {
    // 1. 创建XMLHttpRequest对象
    var xtmlhttp
    if (window.XMLHttpRequest) {
        xmlhttp = new XMLHttpRequest()
        // 兼容早期浏览器
    } else {
        xmlhttp = new ActiveXObject('Microsoft.XMLHTTP')
    }
    // 2. 发送请求
    xmlhttp.open('GET', url, true)
    xmlhttp.send()
    // 3. 接收服务端相应
    xmlhttp.onreadystatechange = function () {
        if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
            var obj = JSON.parse(xmlhttp.responseText)
            successCallback && successCallback(obj);
        } else if (xmlhttp.readyState === 4 && xmlhttp.status === 404) {
            failCallback && failCallback(xmlhttp.statusText)
        }
    }

}
function getPromise(url) {
    return new Promise((resolve, reject) => {
        ajax(url, res => {
            resolve(res)
        }, err => {
            reject(err)
        })
    })
}
getPromise('static/aa.json')
    .then(res => {
        console.log(res);
        return getPromise('static/b.json')
    }).then(res => {
        console.log(res);
        return getPromise('static/c.json')
    }).then(res => {
        console.log(res);
    }).catch(err => {
        console.log(err);
    })

运行的结果:

image.png

这样如果出现了失败就直接进入catch,错误之前的then就不会再进入

Promise静态方法

Promise.resolve()、Promise.reject()

有的时候我们并没有Promise对象,但是我们又想调用then方法,就可以使用Promise.resolve()或者Promise.reject(),把它包装成成功的状态或者失败的状态

基本使用方法

Promise.resolve()它表示成功的状态,它返回一个Promise对象

Promise.reject()它表示失败的状态,它返回一个Promise对象

let p1 = Promise.resolve('success')
console.log(p1);
p1.then(res => {
    console.log(res);
})

image.png

let p2 = Promise.reject('fail')
console.log(p2);
p2.catch(err => {
    console.log(err);
})

image.png

实际应用场景

eg: 如果失败的返回值是字符串,而不是一个Promise对象,但是也想使用then方法该怎么办?

function foo(flag) {
    if(flag) {
        return new Promise(resolve => {
            // 异步操作
            resolve('success')
        })
    } else {
        return 'fail'    // Uncaught TypeError: foo(...).then is not a function
 
    }
}

foo(false).then(res => {
    console.log(res);
}).catch(err => {
    console.log(err);
})

使用Promise.reject()改进

function foo(flag) {
    if(flag) {
        return new Promise(resolve => {
            // 异步操作
            resolve('success')
        })
    } else {
        // return 'fail'   
        return Promise.reject('fail')
 
    }
}

foo(false).then(res => {
    console.log(res);
}).catch(err => {
    console.log(err);      // fail
})

Promise.all()

接收数组作为参数,数组中的每个值都是一个Promise对象,它后面的then的成功函数的参数,是数组中每个Promise对象resolve返回值组成的数组

基本使用

eg: 场景1

3个Promise对象,里面都有异步操作,现在想让这3个异步操作执行完成之后再去做另外一件事

let p1 = new Promise((resolve,reject)=>{
    setTimeout(() => {
        console.log(1);
        resolve('1成功')
    }, 1000);
})
let p2 = new Promise((resolve,reject)=>{
    setTimeout(() => {
        console.log(2);
        resolve('2成功')
    }, 2000);
})
let p3 = new Promise((resolve,reject)=>{
    setTimeout(() => {
        console.log(3);
        resolve('3成功')
    }, 3000);
})
Promise.all([p1,p2,p3]).then(res => {
    console.log(res);
})

image.png

如果3个Promise中有一个异步的结果是失败的呢?

let p1 = new Promise((resolve,reject)=>{
    setTimeout(() => {
        console.log(1);
        resolve('1成功')
    }, 1000);
})
let p2 = new Promise((resolve,reject)=>{
    setTimeout(() => {
        console.log(2);
        reject('2失败')
    }, 2000);
})
let p3 = new Promise((resolve,reject)=>{
    setTimeout(() => {
        console.log(3);
        resolve('3成功')
    }, 3000);
})
Promise.all([p1,p2,p3]).then(res => {
    console.log(res);
},err => {
    console.log(err);
})

image.png

从上图可以发现只要Promise.all([p1,p2,p3])中一个Promise的结果是失败的,就认为这个结果是失败的,代码直接跳到then的失败处理函数中,只有当这3个Promise的结果都是成功的,才会走到then的成功处理函数中

实际应用场景

上传3张图片,但是服务器给的接口每次只能上传一张图片,当三张图片上传完,给个提示图片上传完成,但是我们怎么知道什么时候图片上传完成?(上传图片的过程也是异步的过程)

// 伪代码
let imgArr = ['1.jpg','2.jpg','3.jpg']
let promiseArr = []
imgArr.forEach(item => {
    promiseArr.push(new Promise((resolve,reject) => {
        // 图片上传的异步操作
        resolve()
    }))
})
Promise.all(promiseArr).then(res => {
    console.log('图片全部上传完成');
})

Promise.race()

基本使用

接收数组作为参数,数组中的每个值都是一个Promise对象。它和Promise.all()不同,只要上面例子中3个Promise的结果中只要找到第一个是成功的,那么就认为整个结果是成功的,在then的第一个成功回调中就可以拿到结果

let p1 = new Promise((resolve,reject)=>{
    setTimeout(() => {
        console.log(1);
        resolve('1成功')
    }, 1000);
})
let p2 = new Promise((resolve,reject)=>{
    setTimeout(() => {
        console.log(2);
        reject('2失败')
    }, 2000);
})
let p3 = new Promise((resolve,reject)=>{
    setTimeout(() => {
        console.log(3);
        resolve('3成功')
    }, 3000);
})
Promise.race([p1,p2,p3]).then(res => {
    console.log(res);
},err => {
    console.log(err);
})

image.png

如果上面例子中3个Promise的结果中只要找到第一个是失败的,那么就认为整个结果是失败的,在then的第二个失败回调中就可以拿到结果

let p1 = new Promise((resolve,reject)=>{
    setTimeout(() => {
        console.log(1);
        reject('1失败')
    }, 1000);
})
let p2 = new Promise((resolve,reject)=>{
    setTimeout(() => {
        console.log(2);
        resolve('2成功')
    }, 2000);
})
let p3 = new Promise((resolve,reject)=>{
    setTimeout(() => {
        console.log(3);
        resolve('3成功')
    }, 3000);
})
Promise.race([p1,p2,p3]).then(res => {
    console.log(res);
},err => {
    console.log(err);
})

image.png

实际应用场景

eg: 当前页面需要加载图片,图片可能加载成功也可能加载失败,如果加载失败就给用户一个提示图片加载失败,给图片加载设置一个超时时间2秒,如果超过2秒没有加载出来就给提示,如果加载出来就展示图片

// 伪代码

function getImge() {
    return new Promise((resolve, reject) => {
        let img = new Image()
        img.src = 'https://www.imooc.com/static/img/index/logo2020.png'
        img.onload = function () {
            resolve(img.src)
        }
    })
}
function timeout() {
    return new Promise((res, reject) => {
        setTimeout(() => {
            reject('图片加载超时')
        }, 6000);
    })
}

Promise.race([getImge(),timeout()]).then(res => {
    console.log(res);
},err => {
    console.log(err);
})

image.png

总结:Promise的强大之处不仅在于处理回调地狱的问题,Promise也可以处理多个并发的请求,获取多个并发请求的数据 - Promise可以对异步的状态进行管理