【Promise】Promise 的使用 / API / 回调地狱问题 / 宏队列与微队列

一、复习回顾知识

1、函数对象与实例对象

函数对象:函数作为对象使用时,简称为函数对象。

    <script>
        function Person(name, age) {
            this.name = name;
            this.age = age;
        }
        Person.sex = 'boy';
        console.log(Person.sex);
        console.log(Person.name); // Person.name 默认是构造函数名,无法自己定义
    </script>

实例对象:new 构造函数或类产生的对象,我们称之为实例对象。

    const p = new Person('zs', 20);
    console.log(p); // Person {name: "zs", age: 20}

2、回调函数(重点理解)

2.1 什么是回调函数?

        我们定义的,我们没有调用,最终执行了。

2.2 回调函数的分类

(1)同步的回调函数:

         理解:立即在主线程上执行,不会放入回调队列中。

         举例:数组遍历相关的回调函数  / Promise 的 executor 函数。

(2)异步的回调函数:

         理解:不会立即执行,会放入回调队列以后执行。

         举例:定时器回调 / ajax 回调 / Promise 的成功、失败的回调。

JS 执行顺序:

事件循环:

参考之前写的博客:【JavaScript - Web API】BOM( 定时器 / JS执行机制 / location对象 )_咕啾啾的博客-CSDN博客

3、错误(Error)

官方文档:Error - JavaScript | MDN

3.1 错误的类型

Error所有错误的父类型
ReferenceError引用的变量不存在
TypeError数据类型不正确
SyntaxError语法错误

3.2 错误处理

捕获错误:try{ }catch( ){ }

MDN文档:try...catch - JavaScript | MDN

try {
  nonExistentFunction();
} catch (error) {
  console.error(error);
  // expected output: ReferenceError: nonExistentFunction is not defined
  // Note - error messages will vary depending on browser
}

抛出错误:throw error

3.3 错误对象

        message 属性:错误相关信息。

        stack 属性:记录信息。

二、Promise 的理解和使用

1、promise 理解

1.1 promise 是什么?

抽象表达

        (1)Promise 是一门新的技术(ES6 规范)。

        (2)Promise 是 JS 中进行异步编程新解决方案(旧方案是:单纯使用回调函数)。

具体表达:

        (1)从语法上来说:Promise 是一个构造函数。

        (2)从功能上来说:promise 对象用来封装一个异步操作并可以获取其成功 / 失败的结果值。

理解:

(1)Promise 不是回调,是一个内置的构造函数,是程序员自己 new 调用的。

(2)new Promise 的时候,要传入一个回调函数(executor 函数),它是同步的回调,会立即在主线程上执行。

(3)executor 函数会接收到 2 个参数,它们都是函数,分别用形参:resolve、reject 接收。

        ① 调用 resolve,会让 Promise 实例状态变为:成功 ( fulfilled ),同时可以指定成功的 value。

        ② 调用 reject,会让 Promise 实例状态变为:失败 ( rejected ),同时可以指定失败的 reason。

1.2 promise 的状态改变

        每一个 Promise 实例都有 3 种状态,分贝是:初始化(pending)、成功(fulfilled)、失败(rejected)。

        每一个 Promise 实例被 new 出来的那一刻,状态都是初始化(pending)。

        Promise 的状态只能改变一次,并且改变只有两种:pending => fulfilled;pending => rejected。

1.3 promise 的基本流程

2、Promise 的使用

2.1 使用1:promise 基本编码流程

<script>
// 1) 创建 promise 对象(pending 状态), 指定执行器函数
const p = new Promise((resolve, reject) => {
// 2) 在执行器函数中启动异步任务
setTimeout(() => {
    const time = Date.now()
    // 3) 根据结果做不同处理
    if (time % 2 === 1) {
        // 3.1) 如果成功了, 调用 resolve(), 指定成功的 value, 变为 resolved 状态
        resolve('成功的值 '+ time) } else { 
        // 3.2) 如果失败了, 调用 reject(), 指定失败的 reason, 变为rejected 状态
        reject('失败的值' + time) }
    }, 2000)
})
// 4) 能 promise 指定成功或失败的回调函数来获取成功的 vlaue 或失败的 reason
p.then(
    value => { // 成功的回调函数 onResolved, 得到成功的 vlaue
        console.log('成功的 value: ', value)
    },
    reason => { // 失败的回调函数 onRejected, 得到失败的 reason
        console.log('失败的 reason: ', reason) 
    } 
)
</script>

2.2 使用2:使用 promise 封装基于定时器的异步

    <script>
        function doDelay(time) {
            // 1. 创建 promise 对象
            return new Promise((resolve, reject) => {
                // 2. 启动异步任务
                console.log('启动异步任务')
                setTimeout(() => {
                    console.log('延迟任务开始执行...')
                    const time = Date.now() // 假设: 时间为奇数代表成功, 为偶数代表失败
                    if (time % 2 === 1) { // 成功了
                        // 3. 1. 如果成功了, 调用 resolve()并传入成功的 value
                        resolve('成功的数据 ' + time)
                    } else { // 失败了
                        // 3.2. 如果失败了, 调用 reject()并传入失败的 reason
                        reject('失败的数据 ' + time)
                    }
                }, time)
            })
        }
        const promise = doDelay(2000)
        promise.then(
            value => {
                console.log('成功的 value: ', value)
            },
            reason => {
                console.log('失败的 reason: ', reason)
            },
        )
    </script>

2.3 使用3:使用 promise 封装 ajax 异步请求

    <script>
        /*
        可复用的发 ajax 请求的函数: xhr + promise
        */
        function promiseAjax(url) {
            return new Promise((resolve, reject) => {
                const xhr = new XMLHttpRequest()
                xhr.onreadystatechange = () => {
                    if (xhr.readyState !== 4) return
                    const { status, response } = xhr
                    // 请求成功, 调用 resolve(value)
                    if (status >= 200 && status < 300) {
                        resolve(JSON.parse(response))
                    } else { // 请求失败, 调用 reject(reason)
                        reject(new Error('请求失败: status: ' + status))
                    }
                }
                xhr.open("GET", url)
                xhr.send()
            })
        }
        promiseAjax('https://api.apiopen.top2/getJoke?page=1&count=2&type=video').then(
            data => {
                console.log('显示成功数据', data)
            },
            error => {
                alert(error.message)
            })
    </script>

3、Promise 的 API

3.1 Promise 构造函数:new Promise ( executor ) { }

        executor 函数:是同步执行的,( resolve, reject ) => { }

        resolve 函数:调用 resolve 将 Promise 实例内部状态改为成功 ( fulfiled )。

        reject 函数:调用 reject 将 Promise 实例内部状态改为失败 ( rejected )。

        说明:executor 函数会在 Promise 内部立即同步调用,异步代码放在 executor 函数中。

3.2 Promise.prototype.then 方法:Promise 实例.then( onFulfilled, onRejected )

        onFulfilled:成功的回调函数 —— (value) => { }

        onRejected:失败的回调函数 —— (reason) => { }

        注意:then 方法会返回一个新的 Promise 实例对象。

3.3 Promise.prototype.catch 方法:Promise 实例.catch ( onRejected )

        onRejected:失败的回调函数 —— (reason) => { }

        说明:catch 方法是 then 方法的语法糖,相当于:then( undefined, onRejected )。

 3.4 Promise.resolve( value ) / Promise.reject( reason )

        Promise.resolve( value ):用于快速返回一个状态为 fulfilled 的 Promise 实例对象。

                value 的值可能是:(1)非 Promise 值,例如数组,字符串等等。(2)Promise 值。

        Promise.reject( reason ):用于快速返回一个状态为 rejected 的 Promise 实例对象。

    // 测试:如果方法给的是 p0 参数,打印出各种可能的结果
    // 为了方便看,不进行代码的注释。
    <script>
        // resolve - resolve
        const p0 = Promise.resolve('ok');
        const p = Promise.resolve(p0); // succ:  ok

        // reject - reject
        const p0 = Promise.reject('no!');
        const p = Promise.reject(p0); //fail:  Promise {<rejected>: "no!"} + 报错:Uncaught (in promise) no!

        // resolve - reject
        const p0 = Promise.resolve('ok');
        const p = Promise.reject(p0); // fail:  Promise {<fulfilled>: "ok"}

        // reject - resolve
        const p0 = Promise.reject('no!');
        const p = Promise.resolve(p0); // fail:  no!

        p.then(
            (value) => { console.log('succ: ', value); },
            (reason) => { console.log('fail: ', reason); }
        )
    </script>

 3.5 Promise.all( promiseArr )

        promiseArr:包含 n 个 Promise 实例的数组。

        说明:返回一个新的 Promise 实例,只有所有的 promise 都成功才成功,只有一个失败了就直接失败。

    <script>
        const p1 = Promise.resolve('0');
        const p2 = new Promise((resolve, reject) => {
            setTimeout(() => {
                // resolve('500');
                reject('500');
            }, 500)
        });
        const p3 = new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve('2000');
            }, 2000)
        })
        const x = Promise.all([p1, p2, p3]);
        const then = x.then(
            (value) => { console.log('success', value); },
            (reason) => { console.log('fail', reason); }
        )
    </script>

3.6 Promise.race( promiseArr )

        promiseArr:包含 n 个 Promise 实例的数组。

        说明:返回一个新的 Promise 实例,以最先出结果的 promise 为准。

4、Promise 解决回调地狱问题

4.1 回调地狱

什么是回调地狱?

        回调函数嵌套调用,外部回调函数异步执行的结果是嵌套的回调执行的条件。

        例如:要求在第一次请求成功后开启第二次请求,第二次请求后开启第三次请求,导致层层嵌套。

回调地狱的缺点:不便于程序员阅读,不便于异常处理,不便于后期维护。

4.2 then 的链式调用

        Promise 实例.then() 返回的是一个新的 Promise 实例,它的值和状态由 then() 所指定的回调函数执行的结果决定。

        (1)如果 then 所指定的回调返回的是非 Promise 值 a:那新的 Promise 实例状态为成功( fulfillled ),成功的 value 为 a。

    <script>
        const p = new Promise((resolve, reject) => {
            resolve('a');
        })

        const x = p.then(
            (value) => { console.log('succ_then1:', value); }, // succ_then1: a
            (reason) => { console.log('fail_then1:', reason); }
        )

        x.then(
            (value) => { console.log('succ_then2:', value); }, // succ_then2: undefined
            (reason) => { console.log('fail_then2:', reason); }
        )
    </script>
    <script>
        const p = new Promise((resolve, reject) => {
            resolve('a');
        })

        const x = p.then(
            (value) => { console.log('succ_then1:', value); return false }, // succ_then1: a
            (reason) => { console.log('fail_then1:', reason); }
        )

        x.then(
            (value) => { console.log('succ_then2:', value); }, // succ_then2: false
            (reason) => { console.log('fail_then2:', reason); }
        )
    </script>

        (2)如果 then 所指定的回调返回的是 Promise 实例 p:那新的 Promise 实例的状态、值都与 p 一致。

    <script>
        const p = new Promise((resolve, reject) => {
            resolve('a');
        })

        const x = p.then(
            (value) => { console.log('succ_then1:', value); return Promise.resolve('a') }, // succ_then1: a
            (reason) => { console.log('fail_then1:', reason); }
        )

        x.then(
            (value) => { console.log('succ_then2:', value); }, // succ_then2: a
            (reason) => { console.log('fail_then2:', reason); }
        )
    </script>
    <script>
        const p = new Promise((resolve, reject) => {
            resolve('a');
        })

        const x = p.then(
            (value) => { console.log('succ_then1:', value); return Promise.reject('a') }, // succ_then1: a
            (reason) => { console.log('fail_then1:', reason); }
        )

        x.then(
            (value) => { console.log('succ_then2:', value); },
            (reason) => { console.log('fail_then2:', reason); } // fail_then2: a
        )
    </script>

        (3)如果 then 所指定的回调抛出异常:那新的 Promise 实例状态为 rejected,reason 为抛出的异常。

    <script>
        const p = new Promise((resolve, reject) => {
            resolve('a');
        })

        const x = p.then(
            (value) => { console.log('succ_then1:', value); throw 404 }, // succ_then1: a
            (reason) => { console.log('fail_then1:', reason); }
        )

        x.then(
            (value) => { console.log('succ_then2:', value); }, 
            (reason) => { console.log('fail_then2:', reason); } // fail_then2: 404
        )
    </script>

综合案例:

    <script>
        const p = new Promise((resolve, reject) => {
            resolve('a');
        })

        p.then(
            (value) => { console.log('succ_then1:', value); throw 404 }, // succ_then1: a
            (reason) => { console.log('fail_then1:', reason); return 10 }
        ).then(
            (value) => { console.log('succ_then2:', value); return 100 },
            (reason) => { console.log('fail_then2:', reason); return Promise.reject('20') } // fail_then2: 404
        ).then(
            (value) => { console.log('succ_then3:', value); return true },
            (reason) => { console.log('fail_then3:', reason); return false } // fail_then3: 20
        ).then(
            (value) => { console.log('succ_then4:', value); },
            (reason) => { console.log('fail_then4:', reason); } // succ_then4: false
        )
    </script>

4.3 then 的链式调用解决回调地狱(不是最优秀的方法)

解释:当第一次请求成功的时候,调用 value => {} 这个成功的回调,返回的是一个 Promise 实例 (第二次请求的实例)。由上一点 then() 方法的相关说明可知,如果 then 所指定的回调返回的是 Promise 实例 p,那新的 Promise 实例的状态、值都与 p 一致。所以实际上我们是将第二次请求的 Promise 实例作为第一个 then 的返回值传递下去。

        promiseAjax(url)
            .then(
                value => {
                    console.log('显示第1次成功的数据', value);
                    // 返回第二次请求的实例
                    return promiseAjax(url)
                },
                reason => { alert(reason.message); }
            )

完整 code: 

    <script>
        // 定义一个发送请求的函数,返回一个新的 Promise 实例(封装了 ajsx 异步任务)
        function promiseAjax(url) {
            return new Promise((resolve, reject) => {
                const xhr = new XMLHttpRequest();
                xhr.onreadystatechange = () => {
                    if (xhr.readyState !== 4) return;
                    const { status, response } = xhr;
                    if (status >= 200 && status < 300) {
                        // 请求成功, 调用 resolve(value)
                        resolve(JSON.parse(response));
                    } else {
                        // 请求失败, 调用 reject(reason)
                        reject(new Error('请求失败: status: ' + status));
                    }
                }
                xhr.open("GET", url);
                xhr.send();
            })
        }
        
        // 定义正确和错误的 url 地址便于之后的测试
        const url = 'https://api.apiopen.top/api/getHaoKanVideo?page=0&size=2';
        const url_error = 'https://api.apiopen.top/api22/getHaoKanVideo?page=0&size=2';

        // then 的链式调用解决回调地狱问题
        promiseAjax(url)
            .then(
                value => {
                    console.log('显示第1次成功的数据', value);
                    return promiseAjax(url)
                },
                reason => { alert(reason.message); }
            )
            .then(
                value => {
                    console.log('显示第2次成功的数据', value);
                    return promiseAjax(url)
                },
                reason => { alert(reason.message); }
            )
            .then(
                value => {
                    console.log('显示第3次成功的数据', value);
                    return promiseAjax(url)
                },
                reason => { alert(reason.message); }
            )
            .then(
                value => {
                    console.log('显示第4次成功的数据', value);
                    return promiseAjax(url)
                },
                reason => { alert(reason.message); }
            )

    </script>

4.4 中断 Promise 链

问题产生:如果我们按照 4.3 的代码运行,假设第 2 次请求发送失败,那么会抛出一个 ERROR 错误,调用 reason => {} 失败的回调,但可惜的是我们并没有给失败的回调返回值,所以返回 undefined。而 undefined 恰好是非 Promise 值,这导致在之后的请求中变为成功!这显然是不对的。

解决的问题:请求失败后就不再继续请求。

解决方法:为每一个失败的回调返回一个状态为 pending 的 Promise 实例。

        promiseAjax(url)
            .then(
                value => {
                    console.log('显示第1次成功的数据', value);
                    return promiseAjax(url_error)
                },
                reason => { alert(reason.message); return new Promise(() => { }) }
            )
            .then(
                value => {
                    console.log('显示第2次成功的数据', value);
                    return promiseAjax(url)
                },
                reason => { alert(reason.message); return new Promise(() => { }) }
            )

4.5 错误的穿透

问题的产生:我们在每一个 then() 方法中都要指定失败的回调,并且需要返回值以中断 Promise 链,这些失败的回调代码极其相似,显得繁琐。

解决方案:使用 .catch() 方法兜底。

基本思想:我们使用 .catch() 方法为所有错误指定一个失败的回调,实际的思想是,虽然我们没有在 then() 方法中指定失败的回调,但是底层为我们补了 reason => {throw reason},如果 then 所指定的回调抛出异常,那新的 Promise 实例状态为 rejected,reason 为抛出的异常。所以实际上这个 reason 不断通过失败的回调传递给 .catch() 方法,最终抛出异常。

        promiseAjax(url)
            .then(
                value => {
                    console.log('显示第1次成功的数据', value);
                    return promiseAjax(url_error)
                },
                // 我们虽然没有写失败的回调,但实际上底层为我们补了以下代码:
                // reason => {throw reason}
            )
            .then(
                value => {
                    console.log('显示第2次成功的数据', value);
                    return promiseAjax(url)
                },
                // 我们虽然没有写失败的回调,但实际上底层为我们补了以下代码:
                reason => {throw reason}
            )
            .catch(
                reason => { alert(reason.message); }
            )

4.6 async - await 解决回调地狱(终极解决方案)

语法说明:

(1)async 修饰的函数:

        ① 函数的返回值为 promise 对象。

        ② promise 实例的结果由 async 函数执行的返回值决定。

(2)await 表达式:

        ① 如果表达式是 promise 实例对象,await 后的返回值是 promise 成功的值。

        ② 如果表达式是其他值,直接将此值作为 await 的返回值(相当于赋值操作,好像没啥用的样子...)

(3)注意:

        ① await 必须写在 async 函数中,但 async 函数中可以没有 await(单纯函数前面写 async 好像也没什么用的样子...)。

        ② 如果 await 的 promise 实例对象失败了,就会抛出异常,需要通过 try...catch 来捕获处理。

async - await 语法: 

// 函数形式写法
async function demo() {
    try {
        const result = await promiseAjax(url);
        console.log(result);
        console.log(100);
    } catch (error) {
        console.log(error);
    }
}
demo();

// 箭头函数形式写法
// 注意:如果前面没分号,需要在函数前面加分号或者感叹号,否则会报错。
(async () => {
    try {
        const result = await promiseAjax(url);
        console.log(result);
        console.log(100);
    } catch (error) {
        console.log(error);
    }
})()

await 的原理:

        (async () => {
            try {
                // 程序员“实际上”的写法
                const result = await promiseAjax(url);
                console.log(result);
                console.log(100);

                // 浏览器翻译后的代码
                promiseAjax(url).then(
                     (value) => {
                         console.log(value);
                         console.log(100);
                     }
                 )
            } catch (error) {
                console.log(error);
            }
        })()
        console.log('主线程');
        // 输出:先执行主线程,函数瞬间调用完,将异步函数推进队列,等待调用执行。
        //      主线程
        //      {code: 200, message: "成功!", result: {…}}
        //      100

5、Promise 的几个关键性问题

5.1 如何改变一个 Promise 实例的状态?

(1)执行 resolve( value ):如果当前是 pending 就会变成 fulfilled。

(2)执行 reject( reason ):如果当前是 pending 就会变成 rejected。

(3)执行器函数( executor )抛出异常:如果当前是 pending 就会变成 rejected。如果当前是 fulfilled 或 rejected,则状态不会改变,因为状态只能改变一次。

5.2 改变 promise 状态和指定回调函数谁先谁后?

(1)都有可能, 正常情况下是先指定回调再改变状态,但也可以先改状态再指定回调。

(2)如何先改状态再指定回调?

        ① 在执行器中直接调用 resolve()/reject()。

        ② 延迟更长时间才调用 then()。

(3)什么时候才能得到数据?

        ① 如果先指定的回调,那当状态发生改变时,回调函数就会调用,得到数据。

        ② 如果先改变的状态,那当指定回调时,回调函数就会调用,得到数据。

三、宏队列与微队列

1、概述

JS 中用来存储待执行回调函数的队列包含 2 个不同特定的列队:
        宏队列:用来保存待执行的宏任务(回调),比如:定时器回调、DOM 事件回调、ajax 回调。

        微队列:用来保存待执行的微任务(回调),比如:promise的回调、MutationObserver 的回调。

        执行:JS 引擎首先必须先执行所有的初始化同步任务代码,每次准备取出第一个宏任务执行前, 都要将所有的微任务一个一个取出来执行,也就是优先级比宏任务高,且与微任务所处的代码位置无关。

 参考:JS异步之宏队列与微队列 - BAHG - 博客园

2、相关面试题

2.1 题型一

        setTimeout(() => {
            console.log('timeout1');
        })
        setTimeout(() => {
            console.log('timeout2');
        });
        Promise.resolve(3).then(
            value => { console.log('成功了1'); }
        )
        Promise.resolve(4).then(
            value => { console.log('成功了2'); }
        )

2.2 题型二

        setTimeout(() => {
            console.log('timeout1');
            setTimeout(() => {
                console.log('timeout3');
            })
            Promise.resolve(5).then(
                value => { console.log('成功了5'); }
            )
        }, 5000)
        setTimeout(() => {
            console.log('timeout2');
        }, 3000)
        Promise.resolve(3).then(
            value => { console.log('成功了3'); }
        )
        Promise.resolve(4).then(
            value => { console.log('成功了4'); }
        )

2.3 题型三

    <script>
        setTimeout(() => {
            console.log('0');
        });
        new Promise((resolve, reject) => {
            console.log('1');
            resolve();
        }).then(() => {
            console.log('2');
            new Promise((resolve, reject) => {
                console.log('3');
                resolve();
            }).then(() => {
                console.log('4');
            }).then(() => {
                console.log('5');
            })
        }).then(() => {
            console.log('6');
        })
        new Promise((resolve, reject) => {
            console.log('7');
            resolve();
        }).then(() => {
            console.log('8');
        })
    </script>

输出:1 7 2 3 8 4 6 5 0

  • 15
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值