Ajax_原生学习
目标
了解原生JS如何编写和发送Ajax请求
讲解
Ajax是一个技术的名称, 而以前我们具体使用的是axios.js
插件来发送请求 ,但是axios.js也是一个库, 内部是原生JS代码, 实际它是对原生的代码进行了封装。 今天我们就来解开神秘的面纱 Ajax如果用原生JS实现, 需要使用window提供的一个构造函数, 叫XMLHttpRequest
XMLHttpRequest
使用语法如下:
// 请求省份名字列表 // 接口地址: http://ajax-api.itheima.net/api/province // 请求方式:GET // 原生Ajax编写↓ // 1.创建一个xhr对象 let xhr = new XMLHttpRequest() // 2.设置请求方式和请求地址 xhr.open("请求方式", "请求地址") xhr.open('GET', 'http://ajax-api.itheima.net/api/province') // 3.发送请求 xhr.send() // 4.监听load(请求成功)事件 ==> 可以获取响应数据 xhr.addEventListener('load', function () { console.log(xhr.response) // 响应体数据 // 知识点: 把JSON格式字符串 ==> 转成JS数据类型 // 语法: JSON.parse(JSON字符串) // 返回值: JS数据类型 let result = JSON.parse(xhr.response) console.log(result); })
代码运行流程
###
小结
Ajax原理是?
-
window提供的XMLHttpRequest构造函数
Ajax_原生传参
目标
了解Ajax原生编码传参使用
讲解
新建Ajax_原生传参.html
// 目标: 请求辽宁省下所有城市列表 let xhr = new XMLHttpRequest() // 复习: 之前说过query查询参数要携带在url?后面 // 格式: 参数名=值&参数名=值 xhr.open('GET', `http://ajax-api.itheima.net/api/city?pname=辽宁省`) xhr.send() xhr.addEventListener('load', function () { let result = JSON.parse(xhr.response) console.log(result); })
小结
-
原生Ajax如何传递查询参数?
-
在url?后面拼接查询参数
Ajax_原生POST
目标
了解原生POST请求和请求体使用
讲解
新建Ajax_原生POST.html
, 请求POST测试接口
// 目标: 测试POST请求 // 接口地址: http://ajax-api.itheima.net/api/books // 请求方式: POST // 参数随意 let xhr = new XMLHttpRequest() xhr.open('POST', `http://ajax-api.itheima.net/api/books`) // 1. 请求体要在这里携带 // 请求体支持三种内容类型, 用哪个还是要和后端对上 xhr.send(JSON.stringify({ bookname: '黑马', author: '老李', publisher: '黑马出版社' })) xhr.addEventListener('load', function () { let result = JSON.parse(xhr.response) console.log(result); })
-
发现后端并没有解析到我们的参数, 通过后台提示和Network检查, 我们请求头Content-Type的值不对
-
解决: 所以axios会自动携带Content-Type内容请求头, 而原生的就需要自己携带啦!
// 2. 需要携带请求头Content-Type和固定值指定传递的请求体内容类型, 让后端用对应方式解析 // 要注意: 在open后send前之间设置 xhr.setRequestHeader('Content-Type', 'application/json')
-
最终落地完整代码
// 目标: 测试POST请求 // 接口地址: http://ajax-api.itheima.net/api/books // 请求方式: POST // 参数随意 let xhr = new XMLHttpRequest() xhr.open('POST', `http://ajax-api.itheima.net/api/books`) // 2. 需要携带请求头Content-Type和固定值指定传递的请求体内容类型, 让后端用对应方式解析 // 要注意: 在open后send前之间设置 xhr.setRequestHeader('Content-Type', 'application/json') // 1. 请求体要在这里携带 // 请求体支持三种内容类型, 用哪个还是要和后端对上 xhr.send(JSON.stringify({ bookname: '黑马', author: '老李', publisher: '黑马出版社' })) xhr.addEventListener('load', function () { let result = JSON.parse(xhr.response) console.log(result); })
小结
-
请求体内容类型有哪三种?
-
键值对字符串
-
JSON字符串
-
表单数据FormData
概念_同步异步
目标
-
掌握哪些是同步代码
-
掌握哪些是异步代码
讲解
请先阅读如下代码的执行顺序并说出打印结果
<script src="https://cdn.jsdelivr.net/npm/axios@0.27.2/dist/axios.min.js"></script> <script> console.log(1) if (true) { console.log(2); } else { console.log(3); } setTimeout(() => { console.log(4); }, 1000) const fn = () => { console.log(5); } for (let i = 0; i < 3; i++) { console.log(6); } console.log(7); fn() document.addEventListener('click', () => { console.log(8); }) axios({ url: 'http://ajax-api.itheima.net/api/province', method: 'GET' }).then(() => { console.log(9); }) console.log(10); </script>
-
打印的结果是
1 2 666 7 5 10 9 4
-
异步代码有如下, 其他都是同步按上到下逐行执行
-
setTimeout: 一次性定时器
-
setInterval: 定时器
-
Ajax
-
事件
-
小结
-
JS中都有哪些异步代码?
-
定时器, Ajax, 事件
概念_回调函数
目标
掌握回调函数的使用和执行过程
讲解
-
异步一般都需要耗时, 而且不会阻塞主线程代码往下执行
-
那万一异步代码有了结果, 怎么能让某段代码执行呢?
JS中, 只有用函数调用的方式, 才能让某一段代码去执行
-
所以你发现前面异步的代码, 都有一个函数体, 这个函数体里代码都是等待这个异步任务有结果后执行
所以这个函数体, 会被回调执行
-
把一个函数当成实参传递, 将来特定的时机调用, 这个函数体就叫回调函数
-
我们可以自己定义一个回调函数, 看看代码执行顺序, 体验下回调的感觉
function theFn(fn) { // fn = () => { console.log('回调函数执行') } fn() // 代码执行到这句话, 会回调theFn()里函数体执行, 这个过程就叫回调函数 } theFn(() => { // 此箭头函数体作为实参值传递给了thenFn的形参变量fn上 console.log('回调函数执行'); }) // 小结: 所以以后在调用一个函数时, 把函数体作为实参值传入到另一个函数内, 它应该就是一个回调函数应用了 // 例如forEach等数组方法都是回调函数的运行, 还有我们所有的异步任务都有回调函数的应用
-
所以回调函数应用非常广泛, 不只是接收异步的结果, 可以恰当的流程中进行调用, 其他场景, 例如数组的forEach方法
小结
-
什么是回调函数?
-
把一个函数当做实参传递, 将来特定的时机调用, 这个函数体就叫回调函数
概念_回调地狱
目标
掌握回调地狱概念的理解
讲解
这里我们以一个, 请求省市区列表, 最终获取地区列表为例, 来体验下回调地狱, 直接复制代码查看
// 目标: 获取所有省市区数据, 随便获取 // 1. 获取所有省份列表 axios.get('http://ajax-api.itheima.net/api/province').then(res => { // 2. 获取某个省, 对应的城市列表 let pname = res.data.data[5]; axios.get(`http://ajax-api.itheima.net/api/city?pname=${pname}`).then(res => { // 3. 获取某个市, 对应的地区列表 let cname = res.data.data[0] axios.get(`http://ajax-api.itheima.net/api/area?pname=${pname}&cname=${cname}`).then(res => { console.log(res); }) }) }) // 上面的代码就出现了回调地狱 // 概念: 在回调函数内, 再嵌套回调函数, 一直嵌套下去形成了回调地狱
小结
-
什么是回调地狱?
-
在回调函数内, 再嵌套回调函数, 一直嵌套下去形成了回调地狱
Promise_语法学习
目标
掌握Promise相关语法使用
讲解
Promise在设计之初, 就是为了解决回调地狱
新建html文件, 来熟悉下Promise的相关语法
// 语法: /* let Promise对象变量名 = new Promise((resolve, reject) => { // resolve和reject是Promise内提供的2个函数, 用于回调返回结果到外面 resolve(成功结果) // 触发.then()小括号里函数体执行 reject(失败结果) // 触发.catch()小括号里函数体执行 }) Promise对象变量名.then((成功结果变量名) => { }).catch((失败结果变量名) => { }) */ let p = new Promise((resolve, reject) => { // resolve和reject是Promise内提供的2个函数, 用于回调返回结果到外面 // resolve('成功了') reject('失败了') }) p.then(res => { console.log(res) }).catch(err => { console.error(err) })
小结
-
Promise对象内的resolve和reject函数作用?
-
回调外部的then/catch, 传递成功/失败的动作和结果
Promise_配合异步
目标
掌握用Promise来管理异步代码
讲解
新建Promise_配合异步.html
来使用Promise包裹一个异步任务代码, 来看看效果
// 例1 配合定时器使用 let p = new Promise((resolve, reject) => { setTimeout(() => { resolve('成功结果') }, 2000) }) p.then(res => { console.log(res); // 2秒后返回'成功结果' })
小结
-
Promise是否必须和异步代码使用?
-
不一定, 看情况
Promise_三种状态
目标
了解Promise的三种状态
讲解
新建Promise_三种状态.html
来讲解, 在上个代码的例子来说明为何代码会这样执行
// 1. 创建Promise对象并返回此对象在原地, 并立刻执行函数内代码, 交给浏览器去做倒计时了(异步, 不会阻塞主线程代码往下走, 所以继续走2) let p = new Promise((resolve, reject) => { setTimeout(() => { // 3. 等2秒后, resolve触发, 影响p对象状态 resolve('成功结果') // resolve => fulfilled状态 => .then reject('失败结果') // reject => rejected状态 => .catch }, 2000) }) // 2. 立刻给p对象添加.then函数并传入函数体等待被调用接收成功结果(此是.then()小括号里函数体不会执行) p.then(res => { console.log(res); // 2秒后返回'成功结果' }).catch(err => { console.error(err) }) // 总结: 每个Promise对象有状态凝固特性, 就是Promise对象确定是成功/失败, 就不能再备更改了
-
Promise有三种状态
-
new实例化后, Promise对象(pending准备状态) -> 也是在原地给个承诺
-
当Promise内代码执行了resolve, 会导致所在的Promise对象(fulfilled成功状态) -> 兑现状态 -> 把值传给then
-
当Promise内代码执行了reject, 会导致所在的Promise对象(rejected失败状态) -> 拒绝状态 -> 把失败值传给catch
-
小结
-
Promise对象内部有几种状态?
-
pending, fulfilled, rejected
-
Promise对象的状态如何切换?
-
刚创建会立刻执行new Promise函数内代码, 此时为pending准备状态
-
当Promise对象内异步代码有结果触发resolve, 会导致Promise对象状态为fulfilled兑现成功状态
-
当Promise对象内异步代码有结果触发reject, 会导致Promise对象状态为rejected失败状态, 拒绝兑现承诺
-
什么是Promise的状态凝固?
-
当Promise对象状态改变后, 无法再次改变
Promise_链式调用
目标
掌握Promise的链式调用
讲解
编写代码进行讲解
let p = new Promise((resolve, reject) => { setTimeout(() => { resolve('成功1') }, 2000) }) let p2 = new Promise((resolve, reject) => { setTimeout(() => { resolve('成功2') }, 2000) }) p.then(res => { console.log(res); return p2 // 如果return一个Promise对象,它的结果, 会被下个then接收 }).then(res => { console.log(res); })
小结
-
then里return的Promise对象结果, 交到哪里执行?
-
下一个then接收成功结果
Promise_解决回调地狱
目标
使用Promise来解决回调地狱
讲解
-
新建html文件, 用axios方法原地返回Promise对象特性
-
用.then()里return Promise对象结果会被链式调用下一个then接收特性
把回调函数嵌套的写法, 变成链式调用then的形式
// 目标: 使用Promise的链式调用解决问题 // 前提: axios函数在原地返回的就是一个Promise对象 let pname = '' axios.get('http://ajax-api.itheima.net/api/province').then(res => { // 2. 获取某个省, 对应的城市列表 pname = res.data.data[5]; return axios.get(`http://ajax-api.itheima.net/api/city?pname=${pname}`) }).then(res => { // 3. 获取某个市, 对应的地区列表 let cname = res.data.data[0] return axios.get(`http://ajax-api.itheima.net/api/area?pname=${pname}&cname=${cname}`) }).then(res => { console.log(res); })
小结
-
Promise是如何解决回调地狱的问题的?
-
根据.then的链式调用把Promise对象串联起来
Promise的静态方法_all
目标
能说出Promise.all方法的作用
Promise的all方法
// 目标: 讲解Promise的all方法 // 静态(类)方法: 直接用Promise类来调用 // 1. Promise.all() // 作用: 合并多个Promise对象, 等待所有成功后, 返回结果 // 语法: Promise.all([promise对象1, promise对象2, ...]).then() // 特点: 返回最终结果是个数组, 值是按顺序对应小Promise对象的成功结果 // 注意: 如果有1个Promise失败, 则整个Promise对象则失败 let p1 = new Promise((resolve, reject) => { setTimeout(() => { resolve('成功1') }, 2000) }) let p2 = new Promise((resolve, reject) => { setTimeout(() => { resolve('成功2') }, 2000) }) let p3 = new Promise((resolve, reject) => { setTimeout(() => { resolve('成功3') }, 2000) }) Promise.all([p1, p2, p3]).then(res => { console.log(res); })
小结
-
Promise.all方法有什么作用?
-
合并并发多个Promise对象, 等待所有人成功, 按顺序接收结果
Promise的静态方法_race
目标
-
能说出Promise.race方法的作用
讲解
-
讲解Promise的race方法
// 目标: 讲解Promise的race方法 // 静态(类)方法: 直接用Promise类来调用 // 1. Promise.race() - 赛跑机制 // 作用: 发起并行多个Promise对象, 等待只要任何一个成功, 返回结果执行then // 语法: Promise.race([promise对象1, promise对象2, ...]).then() // 特点: 返回某个promise对象成功的结果 let p1 = new Promise((resolve, reject) => { setTimeout(() => { resolve('成功1') }, 2000) }) let p2 = new Promise((resolve, reject) => { setTimeout(() => { resolve('成功2') }, 2000) }) let p3 = new Promise((resolve, reject) => { setTimeout(() => { resolve('成功3') }, 2000) }) Promise.race([p1, p2, p3]).then(res => { console.log(res); })
Promise.race([p1, p2, p3]).then(res => { console.log(res); })
小结
-
Promise.race方法有什么作用?
-
合并并发多个Promise对象, 接收最先成功的结果
Promise的静态方法resolve和reject
目标
能说出Promise的静态方法resolve和reject方法作用
讲解
了解Promise的resolve和reject方法即可
// 目标: 讲解Promise静态方法的resolve和reject // 1. 先看如下效果, 封装求2个数的和函数 function sumFn(numA, numB) { if (numA && numB) { return numA + numB } else { return '请传递参数' } } let result1 = sumFn(10, 20) let result2 = sumFn() // 返回的结果要么是和要么是字符串, 不太方便判断是否成功/失败 // 2. 所以这个时候, 我们可以用Promise的静态方法, 把结果包装成Promise对象 // 3. Promise.resolve() // 作用: 把现有的值包装成成功状态的Promise对象并返回 // 返回值: 一个成功状态的Promise对象和结果 function myFn(numA, numB) { if (numA && numB) { return Promise.resolve(numA + numB) // 现在返回的Promise对象 } else { return '报错' } } myFn(10, 20).then(res => { console.log(res); }) // 但是myFn().then() 这样写会报错, 因为返回的字符串无法调用then // 解决, 也得把错误提示字符串包装成Promise对象 // 4. Promise.reject() // 作用: 把现有的值包装成失败状态的Promise对象并返回 // 返回值: 一个失败状态的Promise对象和结果 function theFn(numA, numB) { if (numA && numB) { return Promise.resolve(numA + numB) // 现在返回的Promise对象 } else { return Promise.reject('报错') } } theFn().then(res => { console.log(res); }).catch(err => { console.error(err); }) // 一般在封装库的时候常用, 所以了解其作用即可
小结
Promise.resolve和Promise.reject有什么作用?
-
把值转成Promise对象,并设置状态和返回结果
案例封装Ajax函数查询参数
目标
了解封装原生Ajax请求的代码
讲解
新建html文件, 封装itheimaAxios函数, 并复习axios的GET和params传参使用, 暂时不考虑请求体传参
function itheimaAxios({ url: url, method: method = 'GET', params: params }) { // 1. 外面需要Promise对象, 所以创建一个并返回 return new Promise((resolve, reject) => { // 2. 解构配置对象参数使用 let xhr = new XMLHttpRequest() // 3. 做判断, 如果params有值, 则需要携带在url?后面 if (params) { // 把对象转成key=value&key=value格式字符串 let arr = [] for (let key in params) { // 先转成数组[key=value, key=value]这样格式 arr.push(`${key}=${params[key]}`) } url = url + '?' + arr.join('&') // 高端写法 // url += '?' + Object.keys(params).map(key => `${key}=${params[key]}`).join('&') } xhr.open(method, url) xhr.send() xhr.addEventListener('load', function () { let result = JSON.parse(xhr.response) console.log(result); }) }) } // 目标: 封装Ajax函数 // 我们知道axios就是封装了原生的Ajax的代码, 我们也模拟下, 先回顾下axios方法使用 // 然后封装完上面的函数后, 把axios名字换我们自己的发现也ok, 封装成功 itheimaAxios({ url: 'http://ajax-api.itheima.net/api/area', // method: 'GET', params: { pname: '辽宁省', cname: '大连市' } }).then(res => { console.log(res) }).catch(err => { console.error(err) })
小结
-
为何使用axios的配置项key名固定的?
-
因为函数内部会使用这些属性名
-
axios内是如何处理params参数的?
-
把对象转成key=value&key=value字符串格式拼接在url?后面带给服务器
案例封装Ajax函数请求体
目标
能了解Ajax函数内对请求体的处理
讲解
在上个代码基础上继续完成
function itheimaAxios({ url: url, method: method = 'GET', params: params, data: data }) { // 1. 外面需要Promise对象, 所以创建一个并返回 return new Promise((resolve, reject) => { // 2. 解构配置对象参数使用 let xhr = new XMLHttpRequest() // 3. 做判断, 如果params有值, 则需要携带在url?后面 if (params) { // 把对象转成key=value&key=value格式字符串 let arr = [] for (let key in params) { // 先转成数组[key=value, key=value]这样格式 arr.push(`${key}=${params[key]}`) } url = url + '?' + arr.join('&') // 高端写法 // url += '?' + Object.keys(params).map(key => `${key}=${params[key]}`).join('&') } xhr.open(method, url) // 4. 判断如果data有值则携带 if (data) { // 5. 接着判断值的3种类型 if (data instanceof FormData) { xhr.send(data) } else if (data instanceof Object) { xhr.setRequestHeader('Content-Type', 'application/json') xhr.send(JSON.stringify(data)) } else if (typeof data === 'string') { xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') xhr.send(data) } } else { // 如果没有请求体正常发请求 xhr.send() } // 6. 处理成功/失败返回结果 // 成功 // xhr.addEventListener('load', () => { // resolve(xhr.responseText ? JSON.parse(xhr.responseText) : null) // }) // 失败 // xhr.addEventListener('error', () => { // reject(new Error('请检查网络/各项参数的值')) // }) // axios源码逻辑 xhr.addEventListener('loadend', () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(xhr.responseText ? JSON.parse(xhr.responseText) : null) } else { reject(new Error('请检查网络/各项参数的值')) } }) }) } // 目标: 封装Ajax函数_支持POST和请求体 // 先尝试调用, 看着需求写实现 itheimaAxios({ url: 'http://ajax-api.itheima.net/api/books', method: 'POST', data: { "bookname": "黑马程序员", "author": "小马", "publisher": "北京出版社" } }).then(res => { console.log(res) }).catch(err => { console.error(err) })
小结
-
我们传递的data参数请求体, 内部会如何处理?
-
判断值的类型, 然后携带不同的请求头告诉服务器请求体内容类型