js分享18-回调函数(小白必看)

96 阅读9分钟

回调函数 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(chenggong, shibai) {
    const timer = Math.ceil(Math.random() * 3000) + 2000;
    setTimeout(() => {
        if (timer > 3500) {
            console.log("买水失败, 耗时 ", timer);
            shibai();
        } else {
            console.log("买水成功, 耗时: ", timer);
            chenggong();
        }
    }, timer);
}
fn(
    () => {
        console.log("谢谢班长, 在帮我把水退了吧");
    },
    () => {
        console.log("辛苦班长了, 买不到别回来了");
    }
);

回调地狱

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

  • 需求:
    1. 在买水成功后, 让班长帮忙退掉
    2. 在退掉以后, 再次让班长帮忙去买水 (此时必须要在前一个水购买完成之后再去购买)
    3. 在第二次买水成功以后, 再次让班长去买水
function fn(chenggong, shibai) {
    // const timer = Math.ceil(Math.random() * 3000) + 2000
    const timer = Math.ceil(Math.random() * 3000);
    setTimeout(() => {
        if (timer > 3500) {
            console.log("买水失败, 耗时 ", timer);
            shibai();
        } else {
            console.log("买水成功, 耗时: ", timer);
            chenggong();
        }
    }, timer);
}
fn(
    () => {
        console.log("班长第一次买水成功, 帮我退掉");
        fn(
            () => {
                console.log("班长第二次买水成功");
                fn(
                    () => {
                        console.log("班长第三次买水成功");
                    },
                    () => {
                        console.log("班长第三次买水失败");
                    }
                );
            },
            () => {
                console.log("班长第二次买水失败");
            }
        );
    },
    () => {
        console.log("班长第一次买水失败");
    }
);
  • 原因:
    1. 按照回调函数的语法进行封装, 只能通过 传递一个函数作为参数来调用
    2. 当你使用回调函数过多的时候, 会出现回调地狱的代码结构
  • 解决:
    1. 不按照回调函数的语法封装
    2. ES6 推出了一种新的封装异步代码的方式, 叫做 Promise (承诺, 期约)
  • Promise:
    1. 是一种异步代码的封装方案
    2. 因为换了一种封装方案, 不需要按照回调函数的方式去调用, 需要按照 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 的链式调用
fn()
    .then(function (type) {
        // 这个函数执行代码 promise 状态为成功状态!!!
        console.log("因为", type, "谢谢班长, 我准备了20个bug, 回馈给你");
    })
    .catch(function (type) {
        // 这个函数执行代码
        console.log("因为", type, "谢谢班长, 我准备了800个bug, 开心死你");
    });
  • Promise 的调用方式
    • 当你在第一个 then 里面返回(return) 一个新的 Promise 对象的时候
    • 可以在第一个 then 后面 继续 第二个 then
fn()
    .then(function (type) {
        console.log(
            "第一次: 因为",
            type,
            "谢谢班长, 我准备了20个bug, 回馈给你"
        );
        return fn();
    })
    .then(function (type) {
        console.log(
            "第二次: 因为",
            type,
            "谢谢班长, 我准备了20个bug, 回馈给你"
        );
        return fn();
    })
    .then(function (type) {
        console.log(
            "第三次: 因为",
            type,
            "谢谢班长, 我准备了20个bug, 回馈给你"
        );
        return fn();
    })
    .catch(function (type) {
        console.log("因为", type, "谢谢班长, 我准备了800个bug, 开心死你");
    });

async 和 await

  • 注意: 需要配合的必须是 Promise 对象
  • 注意: Promise 的调用方案
  • 意义: 把 异步代码 写的看起来 "像" 同步代码
  • async 关键字的用法:
    • 直接书写在函数的前面即可, 表示该函数是一个异步函数
    • 意义: 表示在该函数内部可以使用 await 关键字
  • await 关键字的用法:
    • 必须书写在一个有 async 关键字的函数内
    • await 后面等待的内容必须是一个 promise 对象
    • 本该使用 then 接受的结果, 可以直接定义变量接受了
  • 常规使用 Promise 语法封装的函数
fn()
    .then(function (res) {
        console.log(res);
    })
    .catch(function (res) {
        console.log(res);
    });
  • 利用 async 和 await 关键字来使用
async function newFn() {
    /**
     * await 是等待的意思
     *
     *  在当前 fn 函数内, await 必须要等到后面的 Promise 结束以后, 才会继续执行后续代码
     */
    const r1 = await fn();
    console.log("第一次: ", r1);
    const r2 = await fn();
    console.log("第二次: ", r1);
    const r3 = await fn();
    console.log("第三次: ", r1);
}

newFn();

async 和 await 语法的缺点

  • await 只能捕获到 Promise 成功的状态, 如果失败, 会报错, 终止程序继续执行
async function newFu() {
    const r1 = await fn();
    console.log(r1);
    console.log("失败后, 提示用户网络错误");
}
newFu();
  • 解决方法 1:
    • 使用 try...catch...
    • 语法: try{ 执行代码 } catch(err) { 执行代码 }
      • 首先执行 try 里面的代码, 如果不报错, catch 的代码不执行了
      • 如果报错, 不会爆出错误, 不会终止程序, 而是执行 catch 的代码
async function newFu() {
    try {
        const r1 = await fn();
        console.log(r1);
    } catch (error) {
        console.log("网络错误, 请检查网络并重新请求");
    }
}
newFu();
  • 解决方法 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 方法
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(), ...
    4. 轮询: 轮流询问 宏任务 和 微任务队列的任务来执行
    5. 注意: WEBApi, 用来负责提供异步机制, 记时, 分配任务去指定队列

数组扁平化

  1. 利用递归实现
const arr = [1, 2, 3, 4, { name: "QF001" }, [5, 6, [7, 8, [9]]]];
function flat(origin) {
    const newArr = [];

    function fn(fnOrigin) {
        fnOrigin.forEach((item) => {
            if (item.constructor === Array) {
                // 如果 item 是数组, 应该递归调用
                fn(item);
            } else {
                // 如果 item 不是数组, 直接插入到 newArr
                newArr.push(item);
            }
        });
    }
    fn(origin);

    return newArr;
}
const res = flat(arr);
console.log(res);
  1. 利用 数组的 toString
    • 会将数组的中括号全部去掉, 并把所有元素都转化为字符串拼接在一起
    • 弊端: 数组内不能出现引用数据类型
let newArr = arr.toString().split(",");
  1. 利用数组.flat(拆几层), 该方法会帮助我们实现数组扁平化
let newArr = arr.flat(Infinity);