一、回调函数 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('第一次请求失败~~~~~~~~~~~~~~~') }
)
二、回调地狱
-
当你使用回调函数过多的时候, 会出现的一种代码书写结构
-
需求:
- 发送注册请求
- 如果注册请求成功, 那么发送 登录请求
- 如果登陆成功
- 那么发送 轮播图请求
- 那么发送 商品列表请求
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)
}
)
- 原因:
- 按照回调函数的语法进行封装, 只能通过 传递一个函数作为参数来调用
- 当你使用回调函数过多的时候, 会出现回调地狱的代码结构
- 回调地狱的问题在于书写完代码后,不利于阅读和后续的维护,并不是说回调地狱会导致我们的功能无法实现
- 解决:
- 不按照回调函数的语法封装
- ES6 推出了一种新的封装异步代码的方式, 叫做 Promise (承诺, 期约)
- Promise:
- 是一种异步代码的封装方案,为了解决回调地狱导致的代码不利于维护与阅读的问题,出现了promise
- promise最重要的是解决了回调地狱,而不是解决了异步代码;异步代码一直会用,不管你用不用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 的链式调用
- 链式调用, 在一个 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 本身有一些方法-构造函数上的方法
- 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(),Promise.catch(),Promise.finally() ...
- 轮询: 轮流询问 宏任务 和 微任务队列的任务来执行
- 注意: WEBApi, 用来负责提供异步机制, 记时, 分配任务去指定队列
- promise 内的 异步代码
-
- new Promise 内部 是按照 同步的顺序执行, 除非你写了异步代码 (定时器/请求相关的)
-
- promise 的实例化对象中的方法 (then/catch/finally) 这三个代码是按照异步任务执行
-
- JS 代码运行流程, 从上往下运行,如果有异步任务.先执行一个 宏任务 (如果你要这样回答一定要强调 我是把 JS 整体代码当作一个 宏任务),然后在执行 微任务队列的清空操作