从异步编程说起

但是对于异步代码,我们就不好推断到底什么时候会执行完成了。比如举一个实际的例子,我们去动态加载某个脚本,会这样做:

function loadScript(src) {
    let script = document.createElement('script')
    script.src = src
    document.head.append(script)
}

这个脚本加载完成的时候会去执行定义在脚本里的一些函数,比如里面的test()方法,那么我们可以会这样写:

function loadScript(src) {
    let script = document.createElement('script')
    script.src = src
    document.head.append(script)
}
loadScript('./js/script.js')
test()  // 定义在 ./js/mytest.js 里的函数

但是实际执行后却发现,这样根本不行,因为加载脚本是需要花时间的,是一个异步的行为,浏览器执行 JavaScript 的时候并不会等到脚本加载完成的时候再去调用 test 函数。

以往,对于这种异步编程的做法通常就是通过给函数传递一个回调函数来处理,上面那个例子可以这样做:

function loadScript(src, success, fail) {
    let script = document.createElement('script')
    script.src = src
    script.onload = success
    script.onerror = fail
    document.head.append(script)
}
loadScript('./js/mytest.js', success, fail)//这里将见下面的函数提升,当参数
function success() {
    console.log('success')
    test()  // 定义在 ./js/script.js 中的函数
}
function fail() {
    console.log('fail')
}

上面这样做能够保证在脚本加载完成的时候,再去执行脚本里的函数。但是多考虑一个问题,如果 success 里又需要加载别的 js 文件呢,那岂不是需要多层嵌套了。是的,这样的多层嵌套会使得代码层次变得更加深入,难以阅读以及后期维护成本非常高,尤其是当里面加上了很多的判断逻辑的时候情况会更加糟糕,这就是所谓的 “回调地狱”,且又因为它的代码形状很像躺着的金字塔,所以有的人也喜欢叫它 “噩运金字塔”。

而为了避免这类 “回调地狱” 问题,目前最好的做法之一就是使用 Promise

Promise正篇

使用 Promise 可以很好的解决上面提到的 “回调地狱” 问题,直接来看结果:

    function addScript(src) {
            return new Promise((resolve, reject) => {
                let script = document.createElement('script');
                script.src = src;
                script.onload = ()=> resolve('成功');
                script.onerror = ()=> reject(new Error(`Script load error for ${src}`));
                document.head.append(script);
            })
        }

        addScript('./mytest.js').then(res =>{
            console.log(res)
            test();
        }).catch(err =>{
            console.log(err)
        })

这里通过使用 Promise 实例的 then 和 catch 函数将多层嵌套的代码改成了同步处理流程,看起来效果还是不错的,那什么是 Promise 呢?

Promise 首先是一个对象,它通常用于描述现在开始执行,一段时间后才能获得结果的行为(异步行为),内部保存了该异步行为的结果。然后,它还是一个有状态的对象:

  • pending:待定
  • fulfilled:兑现,有时候也叫解决(resolved
  • rejected:拒绝

一个 Promise 只有这 3 种状态,且状态的转换过程有且仅有 2 种:

  • pending 到 fulfilled
  • pending 到 rejected

可以通过如下的 Promise 对象构造器来创建一个 Promise

let promise = new Promise((resolve, reject) => {})

传递给 new Promise 的是 executor 执行器。当 Promise 被创建的时候,executor 会立即同步执行。executor 函数里通常做了 2 件事情:初始化一个异步行为和控制状态的最终转换。

new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve()
    }, 1000)
})

如上代码所示,setTimeout 函数用来描述一个异步行为,而 resolve 用来改变状态。executor 函数包含 2 个参数,他们都是回调函数,用于控制 Promise 的状态转换:

  • resolve:用来将状态 pending 转换成 fulfilled
  • reject:用来将状态 pending 转换成 rejected

一个 Promise 的状态一旦被转换过,则无法再变更:

let p = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('第一次 resolve')
        resolve('第二次 resolve')  // 将被忽略
        reject('第一次 reject')  // 将被忽略
    }, 0)
})
setTimeout(console.log, 1000, p)  // Promise {<fulfilled>: "第一次 resolve"}

可以看到执行了 2 次 resolve 函数和 1 次 reject 函数,但是 promise 的最终结果是取的第一次 resolve 的结果,印证了上面的结论。

由 new Promise 构造器返回的 Promise 对象具有如下内部属性:

  • PromiseState:最初是 pendingresolve 被调用的时候变为 fulfilled,或者 reject 被调用时会变为 rejected
  • PromiseResult:最初是 undefinedresolve(value) 被调用时变为 value,或者在 reject(error) 被调用时变为 error

比如上面例子中打印出来的 Promise 对象结果中,fulfilled 是其内部的 PromiseState,而 “第一次 resolve” 是其 PromiseResult

 

 

 

 

Promise实例方法

Promise.prototype.then()

 

Promise.prototype.then() 将用于为 Promise 实例添加处理程序的函数。它接受 2 个可选的参数:

  • onResolved:状态由 pending 转换成 fulfilled 时执行;
  • onRejected:状态由 pending 转换成 rejected 时执行。
function onResolved(res) {
    console.log('resolved' + res)  // resolved3
}
function onRejected(err) {
    console.log('rejected' + err)
}
new Promise((resolve, reject) => {
    resolve(3)
}).then(onResolved, onRejected)

简单的方式:

new Promise((resolve, reject) => {
    resolve(3)
}).then(res => {
    console.log('resolved' + res)  // resolved3
}, err => {
    console.log('rejected' + err)
})

因为状态的变化只有 2 种,所以 onResolved 和 onRejected 在执行的时候必定是互斥。

上面介绍到了 then() 的参数是可选的,当只有 onResolved 的时候可以这样写:

new Promise((resolve, reject) => {
    resolve()
}).then(res => {})

当参数只有 onRejected 的时候,需要把第一个参数设置为 null

new Promise((resolve, reject) => {
    reject()
}).then(null, err => {})

Promise.prototype.catch()

Promise.prototype.catch() 用于给 Promise 对象添加拒绝处理程序。只接受一个参数:onRejected 函数。实际上,下面这两种写法是等效的:

function onRejected(err){}
new Promise((resolve, reject) => {
    reject()
}).catch(onRejected)
new Promise((resolve, reject) => { reject() }).then(null, onRejected)

Promise.prototype.finally()

Promise.prototype.finally() 用于给 Promise 对象添加 onFinally 函数,这个函数主要是做一些清理的工作,只有状态变化的时候才会执行该 onFinally 函数。

function onFinally() {
    console.log(888)  // 并不会执行  
}
new Promise((resolve, reject) => {
    
}).finally(onFinally)

因为 onFinally 函数是没有任何参数的,所以在其内部其实并不知道该 Promise 的状态是怎么样的。

 

链式调用

链式调用里涉及到的知识点很多,我们不妨先看看下面这道题,你能正确输出其打印顺序嘛?

new Promise((resolve, reject) => {
    resolve()
}).then(() => {
    console.log('A')
    new Promise((resolve, reject) => {
        resolve()
    }).then(() => {
        console.log('B')
    }).then(() => {
        console.log('C')
    })
}).then(() => {
    console.log('D')
})

这里我不给出答案,希望你能动手敲一敲代码,然后思考下为什么?容我讲完这部分知识,相信你能自己理解其中缘由。

从上面这串代码里,我们看到 new Promise 后面接了很多的 .then() 处理程序,这个其实就是 Promise 的链式调用,那它为什么能链式调用呢?

基于onResolved生成一个新的Promise

因为 Promise.prototype.then() 会返回一个新的 Promise,来看下:

let p1 = new Promise((resolve, reject) => {
    resolve(3)
})
let p2 = p1.then(() => 6)
setTimeout(console.log, 0, p1)  // Promise {<fulfilled>: 3}
setTimeout(console.log, 0, p2)  // Promise {<fulfilled>: 6}

可以看到 p1 和 p2 的内部 PromiseResult 是不一样的,说明 p2 是一个新的 Promise 实例。

所以这里出现了promise的第三种返回方式 return, 第一二种 resolve,rejceted

新产生的 Promise 会基于 onResolved 的返回值进行构建,构建的时候其实是把返回值传递给 Promise.resolve() 生成的新实例,比如上面那串代码里 p1.then(() => 6) 这里的 onResolved 函数返回了一个 6 ,所以新的 Promise 的内部值会是 6。

如果 .then() 没有提供  onResolved 这个处理程序,则 Promise.resolve() 会基于上一个实例 resolve 后的值来初始化一个新的实例:

 

posted on 2020-12-18 15:26  京鸿一瞥  阅读(191)  评论(0)    收藏  举报