回调函数、Promise--js(二十四)

168 阅读12分钟

一、回调函数 callback

  • 一种函数的调用方式
  • 作用: 当你在 "封装" 异步操作的时候使用回调函数
    • 把 函数 A 当作参数传递到 函数 B 内
    • 在 函数 B 内 以形参的方式调用函数 A
    • 函数 A 是 函数 B 的回调函数
  • 回调函数示例
function A() {
    console.log("函数A开始执行");
}
function B(cb) {
    console.log("函数B开始执行");
    cb();
}
B(A);
  • 封装一个异步函数
function fn(jinnang = () => {}) {
    console.log("班长去买水了");
    const timer = Math.ceil(Math.random() * 3000);
    setTimeout(() => {
        console.log("班长买完水了");
        console.log("耗时", timer);
        console.log("按照锦囊内的内容行事");
        jinnang();
    }, timer);
}
/**
 * fn 函数一旦调用, 班长出发开始去买水
 *      在班长出发的时候, 给他一个锦囊
 */
fn(() => {
    console.log("去买一瓶牛奶");
});
fn();
  • 当我们发送一个网络请求的时候, 是存在失败现象的
    • 我们此时还没有办法发送网络请求, 所以以时间为界限, 约定执行时间如果超过 3500ms 那么就算 失败
const timer = Math.ceil(Math.random() * 3000) + 2000;
setTimeout(() => {
    if (timer > 3500) {
        console.log("请求失败", timer);
    } else {
        console.log("请求成功", timer);
    }
}, timer);
  • 封装异步(模拟成功失败的状态)
 function fn(res, rej) {
            /**
             *  函数内部模拟一个网络请求
             *      因为请求具有延迟性, 所以我们书写一个随机数, 通过随机数的值 决定 当前请求的延迟时间
             * 
             *      并且, 如果 随机数 > 3000        假定为 请求失败
            */

            const time = Math.floor(Math.random() * 4000)

            console.log('此时开始发送请求')

            setTimeout(() => {
                if (time > 1000) {
                    console.log('请求超时', time)

                    rej()
                } else {
                    console.log('请求成功', time)

                    res()
                }
            }, time)
        }

        fn(
            () => { console.log('第一次请求成功的时候调用') },
            () => { console.log('第一次请求失败~~~~~~~~~~~~~~~') }
        )

二、回调地狱

  • 当你使用回调函数过多的时候, 会出现的一种代码书写结构

  • 需求:

    1. 发送注册请求
    2. 如果注册请求成功, 那么发送 登录请求
    3. 如果登陆成功
      • 那么发送 轮播图请求
      • 那么发送 商品列表请求
         function fn(res, rej) {
            const time = Math.floor(Math.random() * 2000)
            setTimeout(() => {
                if (time > 3000) {
                    rej(time)
                } else {
                    res(time)
                }
            }, time)
        }
        
         fn(
            (time) => {
                console.log('我是第一次 注册请求, 成功的时候会调用', time)
                fn(
                    (time) => {
                        console.log('我是第二次 登录请求, 我会在 注册成功的时候发送, 登陆成功~~~~', time)
                        fn(
                            (time) => {
                                console.log('我是第三次 首页-轮播图请求, 我会在 登录成功的时候发送, 轮播图请求成功~~~~', time)
                            },
                            (time) => {
                                console.log('我是第三次 首页-轮播图请求, 我会在 登录成功的时候发送, 轮播图请求失败!!!!', time)
                            }
                        )
                        fn(
                            (time) => {
                                console.log('我也是第三次 首页-商品列表, 我会在 登录成功的时候发送, 商品列表请求成功~~~~', time)
                            },
                            (time) => {
                                console.log('我也是第三次 首页-商品列表, 我会在 登录成功的时候发送, 商品列表请求失败!!!!', time)
                            }
                        )
                    },
                    (time) => {
                        console.log('我是第二次 登录请求, 我会在 注册成功的时候发送, 登陆失败!!!!!', time)
                    }
                )
            },
            (time) => {
                console.log('我是第一次 注册请求, 失败的时候会调用', time)
            }
        )
  • 原因:
    1. 按照回调函数的语法进行封装, 只能通过 传递一个函数作为参数来调用
    2. 当你使用回调函数过多的时候, 会出现回调地狱的代码结构
    3. 回调地狱的问题在于书写完代码后,不利于阅读和后续的维护,并不是说回调地狱会导致我们的功能无法实现
  • 解决:
    1. 不按照回调函数的语法封装
    2. ES6 推出了一种新的封装异步代码的方式, 叫做 Promise (承诺, 期约)
  • Promise:
    1. 是一种异步代码的封装方案,为了解决回调地狱导致的代码不利于维护与阅读的问题,出现了promise
    2. promise最重要的是解决了回调地狱,而不是解决了异步代码;异步代码一直会用,不管你用不用promise
    3. 利用promise书写的代码, 相对于回调地狱,更方便阅读与后续的维护
    4. 因为换了一种封装方案, 不需要按照回调函数的方式去调用, 需要按照 Peomise 的形式去调用

三、认识 Promise

  • Promise 的三个状态
    • 持续: pending
    • 成功: fulfilled
    • 失败: rejected
  • promise 的两种转换
    • 从持续转为 成功
    • 从持续转为 失败
  • promise 的基础语法
    • ES6 内置构造函数
    const p = new Promise(function () {
        // 书写我们异步想要做的事
    });
    // p 就是一个 pormise 实例对象
    
  • promise 对象可以触发两个方法
    • p.then(函数)
    • p.catch(函数)
    • 这两个方法只是注册一个 成功 或者 失败 的时候会执行的函数
const p = new Promise(function (resolve, reject) {
    // resolve: 是一个形参, 名字自定义, 值是一个函数, 当你调用的时候, 会把当前 promise 的状态转换为 成功
    // reject: 是一个形参, 名字自定义, 值是一个函数, 当你调用的时候, 会把当前 promise 的状态转换为 失败
    // resolve 和 reject 调用时可以传递一个参数, 这个参数会被传递给对应的 then catch
    const timer = Math.ceil(Math.random() * 3000) + 2000;
    setTimeout(() => {
        if (timer > 3500) {
            console.log("买水失败, 耗时 ", timer);
            reject("奖励一个bug");
        } else {
            console.log("买水成功, 耗时: ", timer);
            resolve("送你十个bug");
        }
    }, timer);
});

p.then(function (address) {
    console.log("班长买水成功咯~~~", address);
});
p.catch(function (address) {
    console.log("班长买水失败咯~~~", address);
});
  • 封装 promise 为函数
function fn() {
    const p = new Promise(function (resolve, reject) {
        const timer = Math.ceil(Math.random() * 3000) + 2000;
        setTimeout(() => {
            if (timer > 3500) {
                reject("班长买水失败");
            } else {
                resolve("班长买水成功");
            }
        }, timer);
    });
    return p;
}
// 将来在使用的时候 res 得到的是 promise 的实例对象 p

const res = fn();
res.then(function (type) {
    // 这个函数执行代码 promise 状态为成功状态!!!
    console.log("因为", type, "谢谢班长, 我准备了20个bug, 回馈给你");
});
res.catch(function (type) {
    // 这个函数执行代码
    console.log("因为", type, "谢谢班长, 我准备了800个bug, 开心死你");
});
  • promise 的链式调用
    • 链式调用, 在一个 then 后 返回一个 新的 promise 实例化对象, 那么我们可以再添加一个 then 函数
 function fn() {
            return new Promise(function (reslove, rejected) {
                const time = Math.floor(Math.random() * 3000) + 2000
                setTimeout(() => {
                    if (time > 3000000) {
                        rejected(time)
                    } else {
                        reslove(time)
                    }
                }, time)
            })
        }



        const p = fn()


        // 链式调用, 在一个 then 后 返回一个 新的 promise 实例化对象, 那么我们可以再添加一个 then 函数
        p.then((res) => {
            console.log('成功', res)
            return fn()
        }).then((res) => {
            console.log('如果第二次成功, 那么我也会执行')

            return fn()
        }).then((res) => {
            console.log('如果第三次成功, 那么我也会执行')
        }).catch((err) => {
            console.log('失败', err)
        })


  • Promise 解决回调地狱
    • 当你在第一个 then 里面返回(return) 一个新的 Promise 对象的时候
    • 可以在第一个 then 后面 继续 第二个 then
 function fn() {
            return new Promise(function (reslove, rejected) {
                const time = Math.floor(Math.random() * 3000) + 2000
                setTimeout(() => {
                    if (time > 30000000000) {
                        rejected(time)
                    } else {
                        reslove(time)
                    }
                }, time)
            })
        }


        /**
         *  1. 发送注册请求
         *  2. 如果注册请求成功, 那么发送 登录请求
         *  3. 如果登陆成功
         *          那么发送 轮播图请求
         *          那么发送 商品列表请求
        */



        // 相当于发送了 注册请求
        const p = fn()


        p.then((res) => {
            console.log('发送注册请求成功后我会执行, 然后我们会再发送一个登录请求',res)
            // 相当于发送了第二次登录请求
            return fn()
        }).then((res) => {
            console.log('我是第二次 登陆请求, 如果成功我会执行, 并且我会再发送两个请求, 分别是 轮播图和商品列表',res)
            // 轮播图
            return fn()
        }).then((res) => {
            console.log('我是轮播图发送成功后又发送的 商品列表',res)
        }).catch((res) => {
            console.log('上述的所有 promise 实例化对象 失败的时候, 都会执行我')
        })

四、async 和 await

  • 注意: 需要配合的必须是 Promise 对象
  • 注意: Promise 的调用方案
  • 意义: 把 异步代码 写的看起来 "像" 同步代码
  • async 关键字的用法:
    • 直接书写在函数的前面即可, 表示该函数是一个异步函数
    • 意义: 表示在该函数内部可以使用 await 关键字
  • await 关键字的用法:
    • 必须书写在一个有 async 关键字的函数内
    • await书写在 promise 实例化对象前面
    • 只有当前这个对象的状态确定, 才会往下一行运行
    • 本该使用 then 接受的结果, 可以直接定义变量接受了
  • 常规使用 Promise 语法封装的函数
  • 利用 async 和 await 关键字来使用
  function post() {
            return new Promise(function (reslove, rejected) {
                const time = Math.floor(Math.random() * 3000) + 2000
                setTimeout(() => {
                    if (time > 30) {
                        rejected(time)
                    } else {
                        reslove(time)
                    }
                }, time)
            })
        }


        // 原本的写法
        // fn().then((res) => {
        //     console.log(res, '成功')
        // }).catch((err) => {
        //     console.log(err, '失败')
        // })

        // 利用 async await 优化
        async function fn () {
            const res = await post()
            console.log('第一次请求完毕的结果: ', res)

            const res_2 = await post()
            console.log('第二次请求完毕的结果: ', res_2)

            console.log('一定是上边的异步代码运行完毕后, 才会执行我这个代码')
        }

        fn()

五、async 和 await 语法的缺点

  • await 只能捕获到 Promise 成功的状态, 如果失败, 会报错, 终止程序继续执行
async function newFu() {
    const r1 = await fn();
    console.log(r1);
    console.log("失败后, 提示用户网络错误");
}
newFu();

46934

1、解决方法

  • 使用 try...catch...
  • 语法: try{ 执行代码 } catch(err) { 执行代码 }
    • 首先执行 try 里面的代码, 如果不报错, catch 的代码不执行了
    • 如果报错, 不会爆出错误, 不会终止程序, 而是执行 catch 的代码
  function post() {
            return new Promise(function (reslove, rejected) {
                const time = Math.floor(Math.random() * 3000) + 2000
                setTimeout(() => {
                    if (time > 3000) {
                        rejected('当前请求超时, 目前的时间是' + time)
                    } else {
                        reslove(time)
                    }
                }, time)
            })
        }

        async function fn() {

            // 当前写法 不能处理 promise 的错误状态
            // const res = await post()
            // console.log('第一次请求完毕的结果: ', res)

            /**
             *  解决方案
             *      推荐做法
            */
            try {
                // 如果当前分支的代码, 运行完毕没有报错, 那么直接结束
                const res = await post()
                console.log('第一次请求完毕的结果: ', res)

                // 如果当前分支的代码运行完毕后, 有报错, 那么会阻断报错, 并且将错误信息, 传递给 catch 分支的形参
            } catch (err) {
                console.log(err, '如果我执行, 说明 第一次请求出现报错')
            }


            try {
                // 如果当前分支的代码, 运行完毕没有报错, 那么直接结束
                const res = await post()
                console.log('第二次请求完毕的结果: ', res)

                // 如果当前分支的代码运行完毕后, 有报错, 那么会阻断报错, 并且将错误信息, 传递给 catch 分支的形参
            } catch (err) {
                console.log(err, '如果我执行, 说明 第二次请求出现报错')
            }
        }

        fn()

2、解决方法:因为改变封装的思路

  • 原因: 因为 promise 对象 有成功状态和失败状态, 所以会在失败状态时报错
  • 解决: 我就让当前的 promise 对象 百分百成功, 让成功和时报都按照 resolve 的形式来执行
  • 只不过传递出去的参数, 记录一个表示成功或者失败的信息
function fn() {
    const p = new Promise(function (resolve, reject) {
        const timer = Math.ceil(Math.random() * 3000) + 2000;
        setTimeout(() => {
            if (timer > 3500) {
                resolve({ code: 0, msg: "班长买水失败" });
            } else {
                resolve({ code: 1, msg: "班长买水成功" });
            }
        }, timer);
    });
    return p;
}

async function newFn() {
    const r1 = await fn();
    if (r1.code === 0) {
        console.log("第一次请求失败, 请检查您的网络信息");
    } else {
        console.log("第一次请求成功", r1.msg);
    }

    const r2 = await fn();
    if (r2.code == 0) {
        console.log("第二次请求失败, 请检查您的网络信息");
    } else {
        console.log("第二次请求成功", r2.msg);
    }
}
newFn();

六、Promise 的其他方法

  • Promise 实例化对象的 finally 方法
  • 注意:Promise实例化对象的方法是按照异步任务执行
fn()
    .then(function (res) {
        console.log("成功");
    })
    .catch(function (res) {
        console.log("失败");
    })
    .finally(function () {
        console.log(
            "不管promise是成功还是失败, 只要 promise 执行结束, 我都会执行"
        );
    });
  • Promise 本身有一些方法-构造函数上的方法
    1. all:
      • 作用: 可以同时触发多个 Promise 行为
        • 只有所有的 Promise 都成功的时候, all 才算成功
        • 只要任何一个 Promise 失败的时候, all 就算失败了
      • 语法: Promise.all([多个 promise])
    2. race:
      • 作用: 可以同时触发多个 Promise 行为
        • 按照速度计算, 当第一个结束的时候就结束了, 成功或失败取决于第一个执行结束的 promise
      • 语法: Promise.race([ 多个 Promise ])
    3. allSettled:
      • 作用: 可以同时触发多个 Promise 行为
      • 不过多个成功还是失败, 都会触发
      • 会在结果内以数组的形式给你返回 每一个 Promise 行为的成功还是失败
    4. resolve()
      • 强行返回一个成功状态的 promise 对象
    5. reject()
      • 强行返回一个失败状态的 promise 对象
// 1. all
Promise.all([fn(), fn(), fn()])
    .then(function () {
        console.log("所有的 参数 都返回 成功状态");
    })
    .catch(function () {
        console.log("这些参数中, 有一个 为 失败状态");
    });
// 2. race
Promise.race([fn(), fn(), fn()])
    .then(function () {
        console.log("速度最快的那个执行完毕, 并且是成功状态时 执行");
    })
    .catch(function () {
        console.log("速度最快的那个执行完毕, 并且是失败状态时 执行");
    });
// 3. allSettled
Promise.allSettled([fn(), fn(), fn()])
    .then(function (res) {
        console.log(res);
    })
    .catch(function (res) {
        console.log(res);
    });
// 4. resolve
Promise.resolve()
    .then(function (res) {
        console.log("成功");
    })
    .catch(function (res) {
        console.log("失败");
    });
// 5. reject
Promise.reject()
    .then(function (res) {
        console.log("成功");
    })
    .catch(function (res) {
        console.log("失败");
    });

七、事件轮询

  • 时间: 从开始执行代码就开始执行轮询
  • 规则:
    • 从宏任务开始
    • 每执行完毕 一个 宏任务, 清空一次微任务队列(不管微任务队列内有多少任务, 都执行完毕)
    • 再次执行一个宏任务
    • 循环往复, 直到所有任务队列清空结束
  • 关键词:
    1. 单线程: JS 是一个单线程的代码执行机制, 逐行依次执行代码, 会阻塞代码执行
    2. 调用栈: 用来执行代码的, 所有的代码进栈执行, 执行完毕出栈
    3. 队列: 用来存放异步任务的, 先进先出
      • 宏任务队列: JS 整体代码, setTimerout, setInterval, ...
      • 微任务队列: Promise.then(),Promise.catch(),Promise.finally() ...
    4. 轮询: 轮流询问 宏任务 和 微任务队列的任务来执行
    5. 注意: WEBApi, 用来负责提供异步机制, 记时, 分配任务去指定队列
  • promise 内的 异步代码
      1. new Promise 内部 是按照 同步的顺序执行, 除非你写了异步代码 (定时器/请求相关的)
      1. promise 的实例化对象中的方法 (then/catch/finally) 这三个代码是按照异步任务执行
  • JS 代码运行流程, 从上往下运行,如果有异步任务.先执行一个 宏任务 (如果你要这样回答一定要强调 我是把 JS 整体代码当作一个 宏任务),然后在执行 微任务队列的清空操作