乐趣区

关于javascript:JavaScript-Promise-的理解和使用

一、什么是 Promise

1.1 Promise 的前世今生

Promise 最早呈现在 1988 年,由 Barbara Liskov、Liuba Shrira 独创(论文:Promises: Linguistic Support for Efficient Asynchronous Procedure Calls in Distributed Systems)。并且在语言 MultiLisp 和 Concurrent Prolog 中曾经有了相似的实现。

JavaScript 中,Promise 的风行是得益于 jQuery 的办法 jQuery.Deferred(),其余也有一些更精简独立的 Promise 库,例如:Q、When、Bluebird。

// Q / 2010
import Q from 'q'

function wantOdd () {const defer = Q.defer()
    const num = Math.floor(Math.random() * 10)
    if (num % 2) {defer.resolve(num)
    } else {defer.reject(num)
    }
    return defer.promise
}

wantOdd()
    .then(num => {log(`Success: ${num} is odd.`) // Success: 7 is odd.
    })
    .catch(num => {log(`Fail: ${num} is not odd.`)
    })

因为 jQuery 并没有严格依照标准来制订接口,促使了官网对 Promise 的实现规范进行了一系列重要的廓清,该实现标准被命名为 Promise/A+。起初 ES6(也叫 ES2015,2015 年 6 月正式公布)也在 Promise/A+ 的规范上官网实现了一个 Promise 接口。

new Promise(function(resolve, reject) {...} /* 执行器 */  );

想要实现一个 Promise,必须要遵循如下规定:

  1. Promise 是一个提供符合标准的 then() 办法的对象。
  2. 初始状态是 pending,可能转换成 fulfilledrejected 状态。
  3. 一旦 fulfilledrejected 状态确定,再也不能转换成其余状态。
  4. 一旦状态确定,必须要返回一个值,并且这个值是不可批改的。

ECMAScript’s Promise global is just one of many Promises/A+ implementations.

支流语言对于 Promise 的实现:Golang/go-promise、Python/promise、C#/Real-Serious-Games/c-sharp-promise、PHP/Guzzle Promises、Java/IOU、Objective-C/PromiseKit、Swift/FutureLib、Perl/stevan/promises-perl。

1.1.1 旨在解决的问题

因为 JavaScript 是单线程事件驱动的编程语言,通过回调函数治理多个工作。在疾速迭代的开发中,因为回调函数的滥用,很容易产生被人所诟病的回调天堂问题。Promise 的异步编程解决方案比回调函数更加正当,可读性更强。

传说中比拟夸大的回调:

事实业务中依赖关系比拟强的回调:

// 回调函数
function renderPage () {const secret = genSecret()
    // 获取用户令牌
    getUserToken({
        secret,
        success: token => {
            // 获取游戏列表
            getGameList({
                token,
                success: data => {
                    // 渲染游戏列表
                    render({
                        list: data.list,
                        success: () => {
                            // 埋点数据上报
                            report()},
                        fail: err => {console.error(err)
                        }
                    })
                },
                fail: err => {console.error(err)
                }
            })
        },
        fail: err => {console.error(err)
        }
    })
}

应用 Promise 梳理流程后:

// Promise
function renderPage () {const secret = genSecret()
    // 获取用户令牌
    getUserToken(token)
        .then(token => {
            // 获取游戏列表
            return getGameList(token)
        })
        .then(data => {
            // 渲染游戏列表
            return render(data.list) 
        })
        .then(() => {
            // 埋点数据上报
            report()})
        .catch(err => {console.error(err)
        })
}

1.2 实现一个超简易版的 Promise

Promise 的运行实际上是一个观察者模式,then() 中的匿名函数充当观察者,Promise 实例充当被观察者。

const p = new Promise(resolve => setTimeout(resolve.bind(null, 'from promise'), 3000))

p.then(console.log.bind(null, 1))
p.then(console.log.bind(null, 2))
p.then(console.log.bind(null, 3))
p.then(console.log.bind(null, 4))
p.then(console.log.bind(null, 5))
// 3 秒后
// 1 2 3 4 5 from promise

// 实现
const defer = () => {let pending = [] // 充当状态并收集观察者
    let value = undefined
    return {resolve: (_value) => { // FulFilled!
            value = _value
            if (pending) {pending.forEach(callback => callback(value))
                pending = undefined
            }
        },
        then: (callback) => {if (pending) {pending.push(callback)
            } else {callback(value)
            }
        }
    }
}

// 模仿
const mockPromise = () => {let p = defer()
    setTimeout(() => {p.resolve('success!')
    }, 3000)
    return p
}

mockPromise().then(res => {console.log(res)
})

console.log('script end')
// script end
// 3 秒后
// success!

二、Promise 怎么用

2.1 应用 Promise 异步编程

Promise 呈现之前往往应用回调函数治理一些异步程序的状态。

// 常见的异步 Ajax 申请格局
ajax(url, successCallback, errorCallback)

Promise 呈现后应用 then() 接管事件的状态,且只会接管一次。

案例:插件初始化。

应用回调函数:

// 插件代码
let ppInitStatus = false
let ppInitCallback = null
PP.init = callback => {if (ppInitStatus) {callback && callback(/* 数据 */)
    } else {ppInitCallback = callback}
}
// ...
// ...
// 经验了一系列同步异步程序后初始化实现
ppInitCallback && ppInitCallback(/* 数据 */)
ppInitStatus = true

// 第三方调用
PP.init(callback)

应用 Promise:

// 插件代码
let initOk = null
const ppInitStatus = new Promise(resolve => initOk = resolve)
PP.init = callback => {ppInitStatus.then(callback).catch(console.error)
}
// ...
// ...
// 经验了一系列同步异步程序后初始化实现
initOk(/* 数据 */)

// 第三方调用
PP.init(callback)

绝对于应用回调函数,逻辑更清晰,什么时候初始化实现和触发回调高深莫测,不再须要反复判断状态和回调函数。当然更好的做法是只给第三方输入 状态 数据,至于如何应用由第三方决定。

// 插件代码
let initOk = null
PP.init = new Promise(resolve => initOk = resolve)
// ...
// ...
// 经验了一系列同步异步程序后初始化实现
initOk(/* 数据 */)

// 第三方调用
PP.init.then(callback).catch(console.error)

2.2 链式调用

then() 必然返回一个 Promise 对象,Promise 对象又领有一个 then() 办法,这正是 Promise 可能链式调用的起因。

const p = new Promise(r => r(1))
    .then(res => {console.log(res) // 1
        return Promise.resolve(2)
        .then(res => res + 10) // === new Promise(r => r(1))
        .then(res => res + 10) // 由此可见,每次返回的是实例前面跟的最初一个 then
    })
    .then(res => {console.log(res) // 22
        return 3 // === Promise.resolve(3)
    })
    .then(res => {console.log(res) // 3
    })
    .then(res => {console.log(res) // undefined
        return '最强王者'
    })

p.then(console.log.bind(null, '是谁活到了最初:')) // 是谁活到了最初: 最强王者

因为返回一个 Promise 构造体永远返回的是链式调用的最初一个 then(),所以在解决封装好的 Promise 接口时没必要在里面再包一层 Promise

// 包一层 Promise
function api () {return new Promise((resolve, reject) => {axios.get(/* 链接 */).then(data => {
            // ...
            // 经验了一系列数据处理
            resolve(data.xxx)
        })
    })
}

// 更好的做法:利用链式调用
function api () {return axios.get(/* 链接 */).then(data => {
        // ...
        // 经验了一系列数据处理
        return data.xxx
    })
}

2.3 治理多个 Promise 实例

Promise.all() / Promise.race() 能够将多个 Promise 实例包装成一个 Promise 实例,在解决并行的、没有依赖关系的申请时,可能节约大量的工夫。

function wait (ms) {return new Promise(resolve => setTimeout(resolve.bind(null, ms), ms))
}

// Promise.all
Promise.all([wait(2000), wait(4000), wait(3000)])
    .then(console.log)
// 4 秒后 [2000, 4000, 3000]

// Promise.race
Promise.race([wait(2000), wait(4000), wait(3000)])
    .then(console.log)
// 2 秒后 2000

2.4 Promiseasync / await

async / await 实际上只是建设在 Promise 之上的语法糖,让异步代码 看上去 更像同步代码,所以 async / await 在 JavaScript 线程中是非阻塞的,但在以后函数作用域内具备阻塞性质。

let ok = null
async function foo () {console.log(1)
    console.log(await new Promise(resolve => ok = resolve))
    console.log(3)
}
foo() // 1
ok(2) // 2 3

应用 async / await 的劣势:

  1. 简洁洁净

    写更少的代码,不须要顺便创立一个匿名函数,放入 then() 办法中期待一个响应。

    // Promise
    function getUserInfo () {return getData().then(
            data => {return data}
        )
    }
    
    // async / await
    async function getUserInfo () {return await getData()
    }
  2. 解决 条件语句

    当一个异步返回值是另一段逻辑的判断条件,链式调用将随着层级的叠加变得更加简单,让人很容易在代码中迷失自我。应用 async / await 将使代码可读性变得更好。

    // Promise
    function getGameInfo () {getUserAbValue().then(
            abValue => {if (abValue === 1) {return getAInfo().then(
                        data => {// ...}
                    )
                } else {return getBInfo().then(
                        data => {// ...}
                    )
                }
            }
        )
    }
    
    // async / await
    async function getGameInfo () {const abValue = await getUserAbValue()
        if (abValue === 1) {const data = await getAInfo()
            // ...
        } else {// ...}
    }
  3. 解决 两头值

    异步函数经常存在一些异步返回值,作用仅限于成为下一段逻辑的入场券,如果经验层层链式调用,很容易成为另一种模式的“回调天堂”。

    // Promise
    function getGameInfo () {getToken().then(
            token => {getLevel(token).then(
                    level => {getInfo(token, level).then(
                            data => {// ...}
                        )
                    }
                )
            }
        )
    }
    
    // async / await
    async function getGameInfo() {const token = await getToken()
        const level = await getLevel(token)
        const data = await getInfo(token, level)
        // ...
    }

    对于 多个 异步返回两头值,搭配 Promise.all 应用可能晋升逻辑性和性能。

    // async / await & Promise.all
    async function foo() {
      // ...
      const [a, b, c] = await Promise.all([promiseFnA(), promiseFnB(), promiseFnC() ])
      const d = await promiseFnD()
      // ...
    }
  4. 靠谱的 await

    await 'str' 等于 await Promise.resolve('str')await 会把任何不是 Promise 的值包装成 Promise,看起来貌似没有什么用,然而在解决第三方接口的时候能够“Hold”住同步和异步返回值,否则对一个非 Promise 返回值应用 then() 链式调用则会报错。

应用 async / await 的毛病:

await 阻塞 async 函数中的代码执行,在上下文关联性不强的代码中略显累赘。

// async / await
async function initGame () {render(await getGame()) // 期待获取游戏执行结束再去获取用户信息
    report(await getUserInfo())
}

// Promise
function initGame () {getGame()
        .then(render)
        .catch(console.error)
    getUserInfo() // 获取用户信息和获取游戏同步进行
        .then(report)
        .catch(console.error)
}

2.5 错误处理

  1. 链式调用中尽量结尾跟 catch 捕捉谬误,而不是第二个匿名函数。因为标准里注明了若 then() 办法外面的参数不是函数则什么都不做,所以 catch(rejectionFn) 其实就是 then(null, rejectionFn) 的别名。

    anAsyncFn().then(
      resolveSuccess,
      rejectError
    )

    在以上代码中,anAsyncFn() 抛出来的谬误 rejectError 会失常接住,然而 resolveSuccess 抛出来的谬误将无奈捕捉,所以更好的做法是永远应用 catch

    anAsyncFn()
      .then(resolveSuccess)
      .catch(rejectError)

    假使考究一点,也能够通过 resolveSuccess 来捕捉 anAsyncFn() 的谬误,catch 捕捉 resolveSuccess 的谬误。

    anAsyncFn()
      .then(
        resolveSuccess,
        rejectError
      )
      .catch(handleError)
  2. 通过全局属性监听未被解决的 Promise 谬误。

    浏览器环境(window)的回绝状态监听事件:

    • unhandledrejection 当 Promise 被回绝,并且没有提供回绝处理程序时,触发该事件。
    • rejectionhandled 当 Promise 被回绝时,若回绝处理程序被调用,触发该事件。
    // 初始化列表
    const unhandledRejections = new Map()
    // 监听未解决回绝状态
    window.addEventListener('unhandledrejection', e => {unhandledRejections.set(e.promise, e.reason)
    })
    // 监听已解决回绝状态
    window.addEventListener('rejectionhandled', e => {unhandledRejections.delete(e.promise)
    })
    // 循环解决回绝状态
    setInterval(() => {unhandledRejections.forEach((reason, promise) => {console.log('handle:', reason.message)
        promise.catch(e => {console.log(`I catch u!`, e.message)
        })
      })
      unhandledRejections.clear()}, 5000)

留神:Promise.reject()new Promise((resolve, reject) => reject()) 这种形式不能间接触发 unhandledrejection 事件,必须是满足曾经进行了 then() 链式调用的 Promise 对象才行。

2.6 勾销一个 Promise

当执行一个超级久的异步申请时,若超过了可能忍耐的最大时长,往往须要勾销此次申请,然而 Promise 并没有相似于 cancel() 的勾销办法,想完结一个 Promise 只能通过 resolvereject 来扭转其状态,社区曾经有了满足此需要的开源库 Speculation。

或者利用 Promise.race() 的机制来同时注入一个会超时的异步函数,然而 Promise.race() 完结后主程序其实还在 pending 中,占用的资源并没有开释。

Promise.race([anAsyncFn(), timeout(5000)])

2.7 迭代器的利用

若想按程序执行一堆异步程序,可应用 reduce。每次遍历返回一个 Promise 对象,在下一轮 await 住从而顺次执行。

function wasteTime (ms) {return new Promise(resolve => setTimeout(() => {resolve(ms)
        console.log('waste', ms)
    }, ms))
}

// 顺次节约 3 4 5 3 秒 === 15 秒
const arr = [3000, 4000, 5000, 3000]
arr.reduce(async (last, curr) => {
    await last
    return wasteTime(curr)
}, undefined)

三、总结

  1. 每当要应用异步代码时,请思考应用 Promise
  2. Promise 中所有办法的返回类型都是 Promise
  3. Promise 中的状态扭转是一次性的,倡议在 reject() 办法中传递 Error 对象。
  4. 确保为所有的 Promise 增加 then()catch() 办法。
  5. 应用 Promise.all() 行运行多个 Promise
  6. 假使想在 then()catch() 后都做点什么,可应用 finally()
  7. 能够将多个 then() 挂载在同一个 Promise 上。
  8. async(异步)函数返回一个 Promise,所有返回 Promise 的函数也能够被视作一个异步函数。
  9. await 用于调用异步函数,直到其状态扭转(fulfilled or rejected)。
  10. 应用 async / await 时要思考上下文的依赖性,防止造成不必要的阻塞。

版权申明

本博客所有的原创文章,作者皆保留版权。转载必须蕴含本申明,放弃本文残缺,并以超链接模式注明作者后除和本文原始地址:https://blog.mazey.net/understand-promise

(完)

退出移动版