回调函数 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("辛苦班长了, 买不到别回来了");
}
);
回调地狱
当你使用回调函数过多的时候, 会出现的一种代码书写结构
- 需求:
- 在买水成功后, 让班长帮忙退掉
- 在退掉以后, 再次让班长帮忙去买水 (此时必须要在前一个水购买完成之后再去购买)
- 在第二次买水成功以后, 再次让班长去买水
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("班长第一次买水失败");
}
);
- 原因:
- 按照回调函数的语法进行封装, 只能通过 传递一个函数作为参数来调用
- 当你使用回调函数过多的时候, 会出现回调地狱的代码结构
- 解决:
- 不按照回调函数的语法封装
- ES6 推出了一种新的封装异步代码的方式, 叫做 Promise (承诺, 期约)
- Promise:
- 是一种异步代码的封装方案
- 因为换了一种封装方案, 不需要按照回调函数的方式去调用, 需要按照 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 本身有一些方法
- all:
- 作用: 可以同时触发多个 Promise 行为
- 只有所有的 Promise 都成功的时候, all 才算成功
- 只要任何一个 Promise 失败的时候, all 就算失败了
- 语法: Promise.all([多个 promise])
- 作用: 可以同时触发多个 Promise 行为
- race:
- 作用: 可以同时触发多个 Promise 行为
- 按照速度计算, 当第一个结束的时候就结束了, 成功或失败取决于第一个执行结束的 promise
- 语法: Promise.race([ 多个 Promise ])
- 作用: 可以同时触发多个 Promise 行为
- allSettled:
- 作用: 可以同时触发多个 Promise 行为
- 不过多个成功还是失败, 都会触发
- 会在结果内以数组的形式给你返回 每一个 Promise 行为的成功还是失败
- resolve()
- 强行返回一个成功状态的 promise 对象
- reject()
- 强行返回以恶个失败状态的 promise 对象
- all:
// 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("失败");
});
事件轮询
- 时间: 从开始执行代码就开始执行轮询
- 规则:
- 从宏任务开始
- 每执行完毕 一个 宏任务, 清空一次微任务队列(不管微任务队列内有多少任务, 都执行完毕)
- 再次执行一个宏任务
- 循环往复, 直到所有任务队列清空结束
- 关键词:
- 单线程: JS 是一个单线程的代码执行机制, 逐行依次执行代码, 会阻塞代码执行
- 调用栈: 用来执行代码的, 所有的代码进栈执行, 执行完毕出栈
- 队列: 用来存放异步任务的, 先进先出
- 宏任务队列: JS 整体代码, setTimerout, setInterval, ...
- 微任务队列: Promise.then(), ...
- 轮询: 轮流询问 宏任务 和 微任务队列的任务来执行
- 注意: WEBApi, 用来负责提供异步机制, 记时, 分配任务去指定队列
数组扁平化
- 利用递归实现
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);
- 利用 数组的 toString
- 会将数组的中括号全部去掉, 并把所有元素都转化为字符串拼接在一起
- 弊端: 数组内不能出现引用数据类型
let newArr = arr.toString().split(",");
- 利用数组.flat(拆几层), 该方法会帮助我们实现数组扁平化
let newArr = arr.flat(Infinity);