Promise

本文最后更新于:2023年12月5日 晚上

// 模拟网络请求 start
const request = params =>
 new Promise(resolve => {
  setTimeout(() => {
   resolve("Hello World " + params)
  }, 1000)
 })
// 模拟网络请求 end

const get = async s => {
 const res = await request(s)
 console.log(res)
}

const hangings = []

;[1, 2, 3].forEach((_, index) => {
 new Promise(resolve => {
  hangings.push(() => {
   resolve(get("lujinkai " + index))
  })
 })
})

hangings.forEach(h => h())

Promise:一个承诺, 成功或者失败, 要么成功, 要么失败. 不存在先成功然后失败, 或者先成功然后失败.

语法:

new Promise( function(resolve, reject) {...} /* executor */ );

参数:

executor 是一个带有 reslove 和 reject 两个参数的函数. exceutor 函数在 Promise 构造函数执行时同步执行, 被传递 resolve 和 reject 函数(excetor 函数在 Promise 构造函数返回新建对象前被调用). reslove 和 reject 函数被调用时, 分别将 promise 的状态改为 fulfilled(完成)或 rejected(失败)。exceutor 内部通常会执行一些异步操作,一旦完成,可以调用 resolve 函数来将 promise 状态改成 fulilled,或者在发生错误的时将他的状态改为 rejected。
如果在 executor 函数中抛出一个错误,那么该 promise 状态为 rejected。executor 函数的返回值被忽略。

描述:

Promise 对象是一个代理对象(代理一个值),被代理的值在 Promise 对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。这让异步的方法可以像同步的方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的 promise 对象。
一个 Promise 有以下几种状态:
pending: 初始状态, 成功或失败状态
fulfilled: 意味着操作成功完成
rejected: 意味着操作失败

pending 状态的 Promise 对象可能出发 fulfilled 状态并传递一个值给相应处理方法, 也可能触发失败状态(rejected)并传递失败信息。当其中任一种情况出现时,Promise 对象的 then 方法绑定的处理方法(handlers)就会被调用(then 方法包含两个参数: onfulfilled 和 onrejected, 他们都是 Function 类型。当 Promise 状态为 fulfilled 时,调用 then 的 onfulfilled 方法,当 Promise 状态为 rejected 时,调用 then 的 onjected 方法,所有在异步操作的完成和绑定处理方法之间不存在竞争)。

因为 Promise.prototype.then 和 Promise.prototype.catch 方法返回 Promise 对象,所以他们可以被链式调用。

创建 Promise:

const p = new Promise((resolve, reject) => {
 //异步操作, 最终会调用:
 //    resolve(someValue);    //fulfilled
 //or
 //    reject("failure reason")    //rejected
})

想要某个函数拥有 promise 功能, 只需让其返回一个 promise 即可:

function myAsyncFunction(url) {
    return new Promise((resolve, reject) => {
        const xhr new XMLHttpRequest();
        xhr.open('GET',url);
        xhr.onload = () => resolve(xhr.responseText);
        xhr.onerror = () => reject(xhr.statusText);
        xhr.send();
    });
}

Promise 的含义

Promise 是异步编程的一种解决方案, 比传统的解决方案–回调函数和事件–更合理和更强大。原生提供了 Promise 对象。

所谓 Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

Promise 对象有以下两个特点。
(1)对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:pending、fulfilled、rejected。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能: 从 pending 为 fulfilled 和从 pending 变为 rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

注意: 为了方便,本章后面的 resolved 统一只指 fulfilled 状态,不包含 rejected 状态。

有了 Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise 对象提供统一的接口,使得控制异步操作更加容易。

Promise 也有一些缺点。首先,无法取消 Promise,一但新建他就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。第三,当处于 pending 状态时,无法得知目前进展到哪一阶段。

如果某些事件不断的反复发生,一般来说,使用 Stream 模式是比部署 Promise 更好的选择。

基本用法

ES6 规定,Promise 对象是一个构造函数,用来生成 Promise 实例。

下面代码创造了一个 Promise 实例:

const promise = new Promise(function (resolve, reject) {
  // ... some code
  if (/* 异步操作成功 */) {
    resolve(value);
  } else {
    reject(error);
  }
});

Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject。他们是两个函数,由 JavaScript 引擎提供,不用自己部署。

resolve 函数的作用是,将 Promise 对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject 函数的作用是,将 Promise 对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise 实例生成以后,可以用 then 方法分别指定 resolved 状态和 rejected 状态的回调函数:

promise.then(function(value){
    //success
}, function(error){
    //failure
});

then 方法可以接受两个回调函数作为参数。第一个回调函数是 Promise 对象的状态变为 resolved 时调用,第二个回调函数是 Promise 对象的状态变为 rejected 时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受 Promise 对象传出的值作为参数。

下面是是一个 Promise 对象的简单例子:

function timeout(ms) {
 return new Promise((resolve, reject) => {
  setTimeout(resolve, ms, "done")
 })
}

timeout(100).then(value => {
 console.log(value)
})

上面的代码中,timeout 方法返回一个 Promise 实例, 表示一段时间以后才会发生的结果. 过了指定的时间(ms 参数)以后, Promise 实例的状态变为 resolved, 就会触发 then 方法绑定的回调函数。

Promise 新建后就会立即执行:

let promise = new Promise(function (resolve, reject) {
 console.log("Promise")
 resolve()
})

promise.then(function () {
 console.log("resolved.")
})

console.log("Hi!")
// Promise
// Hi!
// resolved

下面是异步加载图片的例子:

function loadImageAsync(url) {
 return new Promise(function (reslove, reject) {
  const image = new Image()
  image.onload = function () {
   resolve(image)
  }
  image.onerror = function () {
   reject(new Error("Could not load image at" + url))
  }
  image.src = url
 })
}

下面是一个用 Promise 对象实现的 Ajax 操作的例子:

const getJSON = function(url) {
    const promise = new Promise(function(){
        const handler = function() {
            if(this.readyState !== 4) return
            if(this.status === 200) {
                resolve(this.response)
            } else {
                reject(new Error(this.statusText))
            }
        }
        const client = new XMLHttpRequest()
        client.open("GET",url)
        client.onreadystatechange = handler
        client.responseType = "json"
        client.setRequestHeader("Accept", "application/json")
        client.send()
    })
    return promise
}

getJSON('/post.json').then(function(json) {
    console.log('content: ' + json)
}, functioin(error) {
    console.error('出错了',error)
})

reject 函数的参数通常是 Error 对象的实例, 表示抛出的错误; resolve 函数的参数除了正常的值以外, 还可能是另一个 Promise 实例, 比如像下面这样:

const p1 = new Promise(function (resolve, reject) {
    //...
}
const p2 = new Promise(function (resolve, reject) {
    //...
    return p1
}

上面的代码中, p1 和 p2 都是 Promise 的实例, 但是 p2 的 resolve 方法将 p1 作为参数, 即一个异步操作的结果是返回另一个异步操作。

注意,这时 p1 的状态就会传递给 p2,也就是说,p1 的状态决定了 p2 的状态。如果 p1 的状态是 pending,那么 p2 的回调函数就会等待 p1 的状态改变;如果 p1 的状态已经是 resolved 或者 rejected,那么 p2 的回调函数将会立即执行。
(Promise 对象(P1)的参数(函数)会立即同步执行, 通过在这个函数内调用 resolve 或者 reject 来确定 Promise 对象的状态, 调用 resolve 回调函数,将 Promise 对象的状态转为 fulfilled, 调用 reject 回调函数,将 Promise 对象的状态转为 rejected。如果这个函数内既没有调用 resolve, 也没有调用 reject, 而是返回另一个 Promise 对象(P2), 则 P1 的状态完全失效, 由 P2 的状态来决定 P1 的状态。此时 P1 的 then 语句也变成针对 P2)

一般来说,调用 resolve 或 reject 以后,Promise 的使命就完成了,后继操作应该放到 then 方法里面,而不应该写在 resolve 或 reject 的后面。所以,最好在它前面加上 return 语句, 这样就不会有意外。

new Promise((resolve, reject) => {
 return resolve(1)
 //后面的语句不会执行
 console.log(2)
})

Promise.prototype.then()

Prototype 实例具有 then 方法, 也就是说, then 方法是定义在原型对象 Promise.prototype 上的。它的作用是为 Promise 实例添加状态改变时的回调函数。前面说过,then 方法的第一个参数是 resolved 状态的回调函数,第二个参数(可选)是 rejected 状态的回调函数。

then 方法(不是 then 的参数 res 和 rej)返回的是一个新的 Promise 实例(注意,不是原来的那个 Promise 实例)。因此可以采用链式写法,即 then 方法后面再调用另一个 then 方法。

getJSON("/post.json").then(function(json) {
    return json.post
}).then(function(post) {}
    //...
})

上面的代码使用 then 方法, 依次指定了两个回调函数。第一个回调函数完成之后,会将返回结果作为参数,传入第二个回调函数。

采用链式的 then,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个 Promise 对象(即有异步操作),这时后一个回调函数,就会等待该 Promise 对象的状态发生变化,才会被调用。

getJSON("/post/1.json")
 .then(post => {
  return getJSON(post.commentURL)
 })
 .then(
  comments => {},
  err => {}
 )

上面的代码中, 第一个 then 方法指定的回调函数, 返回的是另一个 Promise 对象( 大公鸡 )。这时,第二个 then 方法指定的回调函数,就会等待这个新的 Promise 对象状态发生改变。

Promise.prototype.catch()

Promise.prototype.catch 方法是.then(null, rejection)的别名, 用于指定发生错误时的回调函数.

Promise 对象的错误具有” 冒泡 “性质, 会一直向后传递, 直到被捕获为止。也就是说,错误总是会被下一个 catch 语句捕获。

一般来说,不要在 then 方法里面定义 Reject 状态的回调函数(即 then 的第二个参数),总是使用 catch 方法。

//bad
Promise.then(
 data => {},
 err => {}
)

//good
Promise.then(data => {}).catch(err => {})

上面代码中, 第二种写法要好于第一种写法,理由是第二种写法可以捕获前面 then 方法执行中的错误,也能接近同步的写法(try/catch)。因此,建议总是使用 catch 方法,而不使用 then 方法的第二个参数。

跟传统的 try/catch 代码块不同的是,如果没有使用 catch 方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。

const someAsyncThing = function () {
 return new Promise(function (resolve, reject) {
  // 下面一行会报错,因为x没有声明
  resolve(x + 2)
 })
}
someAsyncThing().then(function () {
 console.log("everything is great")
})
setTimeout(() => {
 console.log(123)
}, 2000)
// Uncaught (in promise) ReferenceError: x is not defined
// 123

上面代码中,someAsyncThing 函数产生的 Promise 对象,内部有语法错误。浏览器运行到这一行,会打印出错误提示 ReferenceError: x is not defined,但是不会退出进程、终止脚本执行,2 秒之后还是会输出 123。这就是说,Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说就是“Promise”会吃掉错误。

再看下面的例子:

const promise = new Promise(function (resolve, reject) {
 resolve("ok")
 setTimeout(function () {
  throw new Error("test")
 }, 0)
})
promise.then(function (value) {
 console.log(value)
})
// ok
// Uncaught Error: test    报错

上面的代码中,Promise 指定在下一轮“事件循环”再抛出错误。到了这个时候,Promise 的运行已经结束了,所以这个错误是在 Promise 函数体外抛出的, 会冒泡到最外层, 成了未捕获的错误。

一般总是建议,Promise 对象后面要跟着 catch 方法,这样可以处理 Promise 内部发生的错误。catch 方法返回的还是一个 Promise 对象,因此后面还可以接着调用 then 方法。

Promise.all()

Promise.all 可以将多个 Promise 实例包装成一个新的 Promise 实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被 reject 失败状态的值。

具体代码如下:

let p1 = new Promise((resolve, reject) => {
 resolve("成功了")
})

let p2 = new Promise((resolve, reject) => {
 resolve("success")
})

let p3 = Promse.reject("失败")

Promise.all([p1, p2])
 .then(result => {
  console.log(result) //['成功了', 'success']
 })
 .catch(error => {
  console.log(error)
 })

Promise.all([p1, p3, p2])
 .then(result => {
  console.log(result)
 })
 .catch(error => {
  console.log(error) // 失败了,打出 '失败'
 })

Promse.all 在处理多个异步处理时非常有用,比如说一个页面上需要等两个或多个 ajax 的数据回来以后才正常显示,在此之前只显示 loading 图标。

代码模拟:

let wake = time => {
 return new Promise((resolve, reject) => {
  setTimeout(() => {
   resolve(`${time / 1000}秒后醒来`)
  }, time)
 })
}

let p1 = wake(3000)
let p2 = wake(2000)

Promise.all([p1, p2])
 .then(result => {
  console.log(result) // [ '3秒后醒来', '2秒后醒来' ]
 })
 .catch(error => {
  console.log(error)
 })

需要特别注意的是,Promise.all 获得的成功结果的数组里面的数据顺序和 Promise.all 接收到的数组顺序是一致的,即 p1 的结果在前,即便 p1 的结果获取的比 p2 要晚。这带来了一个绝大的好处:在前端开发请求数据的过程中,偶尔会遇到发送多个请求并根据请求顺序获取和使用数据的场景,使用 Promise.all 毫无疑问可以解决这个问题。

Promise.race()

顾名思义,Promse.race 就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。

let p1 = new Promise((resolve, reject) => {
 setTimeout(() => {
  resolve("success")
 }, 1000)
})

let p2 = new Promise((resolve, reject) => {
 setTimeout(() => {
  reject("failed")
 }, 500)
})

Promise.race([p1, p2])
 .then(result => {
  console.log(result)
 })
 .catch(error => {
  console.log(error) // 打开的是 'failed'
 })

原理是挺简单的,但是在实际运用中还没有想到什么的使用场景会使用到。

Promise.resolve()

Promise.reject()

两个有用的附加方法

应用

Promise.try()

补充

then 和 catch 要么不写,写了就一定要定义 return,否则默认 return undefined

promise 是链式的,对于链中间的一个 promise,以下两种写法是等价的:

// 1
promise.then(res => {...})
// 2
promise.then(res => {...}).catch(err => {return Promise.reject(err)})

Promise
http://blog.lujinkai.cn/前端/JavaScript/ES6-Promise/
作者
像方便面一样的男子
发布于
2020年12月6日
更新于
2023年12月5日
许可协议