主题
Ajax总结回顾与promise介绍(Day16 1/3)
本页对应 Day16_Promise,共 5 段视频,约 1 小时 22 分。
学习目标
- 能按四步写出 XMLHttpRequest 请求流程,并说明监听事件位置灵活、请求体格式与
Content-Type设置、load与error的触发条件。 - 能解释同源策略的协议、域名、端口三要素,并说明 CORS 响应头与 JSONP 的实现步骤及前后端配合点。
- 能说明 Promise 是 ES6 异步编程语法,列举
pending、resolved、rejected三种状态及状态不可逆特点。 - 能使用
new Promise创建 Promise 对象,验证执行器函数同步执行、接收resolve和reject两个函数参数,并输出pending状态。 - 能调用
then设置成功与失败回调,验证回调异步执行、通过形参获取 Promise 结果,并用随机成功/失败案例处理状态。 - 能封装
setTimeout为返回 Promise 的函数,使用then处理延时结果,并对比传统回调写法说明参数与回调分离的差异。
知识点
从 Ajax 回顾到 Promise:先分清“异步”与“回调”
为什么需要:前面用 Ajax 发请求时,核心是 XMLHttpRequest 对象:创建对象、监听事件、初始化请求、发送请求;响应成功与否再通过事件处理。定时器、DOM 事件、Ajax 都是异步场景,传统写法都离不开回调函数。
怎么想:回调函数不是“马上执行”,而是满足条件后执行:定时器等时间到,DOM 事件等动作触发,Ajax 等拿到响应。问题在于回调一多,代码容易层层嵌套,形成回调地狱。Promise 是 ES6 新增的语法方案,用来改造异步编程写法。
怎么做:先记住 Promise 是“承诺/许诺”的意思,是一个内置类;通过 new Promise(...) 得到 Promise 对象。它有三种状态:pending(悬而未决)、resolved(成功,也可说 fulfilled)、rejected(失败)。状态一旦改变就不再变。
易错点:不要把 Promise 当成新功能,它没有新增业务能力,主要是语法写法变化;也不要把 pending 当成失败,它只是还没决定走向。
创建 Promise 对象:构造函数的回调是同步执行的
为什么需要:要使用 Promise,先得创建 Promise 实例,并让它内部执行一个异步操作,再由这个操作决定成功或失败。
怎么想:Promise 是类,new Promise(...) 得到的是 Promise 对象/实例。构造函数必须接收一个函数作为参数,这个函数通常叫执行器;它会被自动调用,并接收 resolve、reject 两个函数参数。
怎么做:
js
const p = new Promise((resolve, reject) => {
const r = Math.random();
if (r >= 0.5) {
resolve(r); // 改成成功状态,并携带结果
} else {
reject(r); // 改成失败状态,并携带结果
}
});调试时会发现:new Promise() 不传函数会报 Promise resolver ... is not a function;传 100 也同样报错,因为参数必须是函数。只创建不调用 resolve/reject 时,输出对象状态仍是 pending。
易错点:构造函数的执行器是同步自动执行的,不是异步;resolve/reject 的名字可以改,但位置和含义不能乱;状态只能改一次,成功后再失败不行。
用 then 设置回调:处理成功与失败,且回调异步执行
为什么需要:Promise 状态改变后,需要分别处理成功和失败,否则失败状态可能产生未处理错误。
怎么想:Promise 对象有 then 方法,接收两个函数参数:第一个在成功时执行,第二个在失败时执行;回调可以通过形参拿到 resolve/reject 传入的结果。
怎么做:
js
p.then(
res => {
console.log('成功,结果是', res);
},
err => {
console.log('失败,结果是', err);
}
);调试时先 console.log('hello') 再调用 then,会发现 hello 先输出,说明 then 的两个回调是异步执行的;而构造函数里的执行器是同步的,要分清。
易错点:then 第一个回调处理成功,第二个处理失败;不要和构造函数的同步执行混淆;失败不处理时容易看到报错。
封装定时器 Promise:把“参数”和“回调”分开
为什么需要:传统 setTimeout 把回调和时间写在同一个调用里;Promise 改造后,可以先得到 Promise 对象,再用 then 处理后续逻辑。
怎么想:定时器只有“时间到了”这一种结果,所以 Promise 版定时器只可能成功,不需要 reject。
怎么做:
js
function setTimeoutPromise(delay) {
return new Promise(resolve => {
setTimeout(resolve, delay);
});
}
setTimeoutPromise(3000).then(() => {
console.log('Promise 版定时器执行了');
});调试时注意:执行 setTimeoutPromise(3000) 后立刻输出返回值,它仍是 pending,因为 3 秒后才调用 resolve。
易错点:不要以为 new Promise 一创建就成功;定时器 Promise 没必要强行写失败分支;它主要是语法差异,真正优势在后面链式调用中体现。
本节回顾的定位
这节笔记不是新知识,而是把前面两天做的 Ajax 相关内容串起来:XMLHttpRequest 对象、同源策略与跨域、Ajax 封装、Ajax 记账本。转写稿里反复强调:步骤要理解,不要只背“第一步、第二步”。因为真正写项目时,请求怎么发、响应怎么处理、跨域为什么失败,都要靠理解来判断。
本节笔记大纲如下,保留原代码块:
1. XMLHttpRequest 对象
1.1 ajax 基本流程步骤
1.2 发送请求
① url 携带参数
② 请求体 字符串
② 请求体 FormData
1.3 处理响应
1.4 响应超时
1.5 进度事件
1.6 同步和异步
2. 同源策略
打开页面的URL和ajax请求的URL 协议、域名(ip地址)、端口 必须相同
2.1 跨域方案一: 后端做出响应的时候,设置响应头
2.2 跨域方案二: josnp
3. 封装 ajax 函数
4. ajax记账本XMLHttpRequest 基本流程
为什么需要:Ajax 的本质是让页面在不刷新的情况下发请求、拿响应,所以必须先掌握 XMLHttpRequest 的基本流程。
怎么想:流程可以理解为创建对象、监听事件、初始化请求、发送请求。转写稿特别说明:因为发送请求是异步的,监听事件放在创建后、初始化后、甚至发送后,语法上都行;但一般习惯先监听,避免响应太快时漏掉。
怎么做:先 new XMLHttpRequest(),再监听 load、error 等事件,然后 open() 初始化,最后 send() 发送。调试时先看请求有没有发出去,再看响应有没有回来。
易错点:不要以为监听事件必须固定在某个位置;也不要以为所有失败都进 error。只要拿到了后端响应,哪怕是 404、500,也会触发 load;请求根本发不出去或违反同源策略,才更容易落到 error。
发送请求的三种组织方式
为什么需要:请求要携带数据,数据放在 URL 还是请求体、用什么格式,后端处理方式不同。
怎么想:URL 携带参数就是把查询字符串拼到地址后面;请求体字符串常见有三种写法:不规定内容、查询字符串形式、JSON 形式;另外还可以用 FormData。
怎么做:URL 参数直接拼接;字符串请求体要按约定设置请求头,例如 JSON 要告诉后端请求体类型;FormData 创建后直接作为请求体,XMLHttpRequest 会自动设置合适的请求头。
易错点:发 JSON 字符串却不设置请求头,后端默认可能按文本处理,无法自动解析;FormData 不要自己乱设请求头。转写稿说得很直白:按开发规范把自己该做的做好,减少联调问题。
处理响应、超时与进度事件
为什么需要:请求发出去后要拿到状态码、响应头、响应体,还要处理超时和下载进度。
怎么想:响应行、响应头、响应体都能从 XMLHttpRequest 对象上取。响应体可以用 responseType 配合 response,提前指定类型后拿到处理好的结果。超时用 timeout 属性加 timeout 事件:时间到了请求流程还没结束,就触发超时。
怎么做:在 load 里读响应;设置 timeout 并监听超时;进度相关事件里,loadstart 在请求刚开始时触发,loadend 不管成功失败最后都会触发,progress 在接收响应过程中可能触发多次,可用事件对象里的已加载数据做下载进度。
易错点:progress 触发次数取决于响应体大小,不要假设只触发一次;loadend 不是“成功结束”,而是最终收尾;loadstart 时状态还在变化过程中,不要拿它当响应完成。
同步与异步
为什么需要:Ajax 默认异步,不阻塞主线程;同步请求会占住主线程,直到拿到结果才继续。
怎么想:open() 第三个参数默认是异步,传 false 才是同步。同步看起来“等结果”,但会卡住页面流程,实际开发一般不推荐。
怎么做:默认不传或传 true 保持异步;理解异步回调会进入回调队列,等主线程空闲再执行。
易错点:不要因为“想按顺序拿结果”就改成同步;同步请求会阻塞主线程,体验差。
同源策略与跨域方案
为什么需要:浏览器用同源策略限制页面脚本随意读取别的源的数据,保护服务器和用户。
怎么想:比较两个 URL:打开页面的 URL 和 Ajax 请求的 URL,协议、域名(或 IP)、端口必须完全相同。不同就是跨域。跨域不是后端不响应,而是浏览器不让前端接收响应。
怎么做:方案一是后端在响应头里设置允许的页面源,例如 Access-Control-Allow-Origin,可以指定页面 URL,也可以放开为 *;方案二是 JSONP,利用 script 标签天生可跨域的特点,动态创建 script,设置 src 为请求地址,插入页面后请求发出,后端返回“函数调用”形式的数据,前端提前声明回调函数接收数据,最后删除 script。
易错点:响应头里允许的是“打开页面的 URL”,不是 Ajax 请求地址;JSONP 需要前后端配合,不是前端单方面能完成;动态 script 要插入页面才会发请求,用完要删除,避免越积越多。
封装 Ajax 与记账本
为什么需要:原生 XMLHttpRequest 写法散,封装后调用更统一;记账本则把增删改查串成完整业务。
怎么想:封装时把创建对象、初始化、发送、监听响应收进函数,把成功、失败、超时等按需求分流。记账本要体会页面不跳转、只在一个页面上操作的变化。
怎么做:按项目需求封装请求函数,调用时传地址、方法、数据、成功/失败处理;记账本里每个操作对应一次 Ajax 请求,响应回来再更新页面。
易错点:封装时不要把 404、500 一律当“请求发不出去”;它们仍是响应,只是状态码不同。转写稿也提醒:后续会学更常用的 Ajax 库,原生写法要理解,不必死背。
课堂画面





代码示例
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>创建 Promise 对象</title>
</head>
<body>
<script>
// 1. new Promise 必须传入一个函数作为参数
const p1 = new Promise(() => {});
console.log('p1', p1);
// 2. Promise 构造函数的回调函数,在实例化时会立即自动执行
const p2 = new Promise((resolve, reject) => {
console.log('Promise 构造函数的回调函数执行了');
});
console.log('p2', p2);
// 3. 回调函数会自动接收两个参数,且两个参数都是函数
const p3 = new Promise((resolve, reject) => {
console.log('第一个参数', resolve);
console.log('第二个参数', reject);
});
console.log('p3', p3);
</script>
</body>
</html>要点解释:
new Promise()必须传入一个函数作为参数;传入100这类非函数会报错。- Promise 构造函数的回调函数在实例化时立即执行,并且是同步执行,不是异步。
- 回调函数自动接收两个函数参数:第一个用于改成成功状态,第二个用于改成失败状态。
- 只创建 Promise 对象、不调用
resolve或reject时,状态仍然是pending。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>为 Promise 对象设置回调函数</title>
</head>
<body>
<div id="books"></div>
<script>
const books = document.querySelector('#books');
const p = new Promise((resolve, reject) => {
const random = Math.random();
if (random >= 0.5) {
// 调用第一个参数,设置为成功状态
resolve(random);
} else {
// 调用第二个参数,设置为失败状态
reject(random);
}
});
// then 方法设置回调函数
p.then(
(res) => {
books.innerHTML = `恭喜您抽中了,中奖码:${res}`;
},
(err) => {
books.innerHTML = `很遗憾,没有抽中,失败码:${err}`;
}
);
console.log('then 后面的同步代码先执行');
</script>
</body>
</html>要点解释:
then的第一个参数是成功状态的回调函数,第二个参数是失败状态的回调函数。resolve(random)和reject(random)传入的值,可以通过then回调函数的形参拿到。then的两个回调函数都是异步执行的,所以后面的同步console.log会先执行。- 如果 Promise 变成失败状态但没有用
then的第二个回调处理,会出现报错;设置失败回调后可以处理掉。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>封装定时器 Promise 函数</title>
</head>
<body>
<script>
// 封装一个返回 Promise 对象的定时器函数
function setTimeoutPromise(delay) {
return new Promise((resolve) => {
setTimeout(resolve, delay);
});
}
const p = setTimeoutPromise(3000);
console.log('输出 p 时状态仍是 pending', p);
p.then(() => {
console.log('promise 版定时器执行了');
});
// 原版定时器作为对比
setTimeout(() => {
console.log('原版定时器执行了');
}, 4000);
</script>
</body>
</html>要点解释:
setTimeoutPromise返回一个 Promise 对象,满足“函数返回 Promise”的改造思路。setTimeout(resolve, delay)把resolve直接作为定时器回调,时间到后调用resolve,状态变为成功。- 定时器没有失败场景,因此这里只写
resolve,不写reject。 - 执行
setTimeoutPromise(3000)时只是创建了 Promise,状态要等 3 秒后调用resolve才改变,所以刚输出p时仍是pending。 then的回调是异步执行的;Promise 版把“创建异步任务”和“处理回调函数”分开写,原版定时器则把回调函数直接写在参数里。
常见错误
new Promise()不传参,控制台报Promise resolver undefined is not a function- 现象:一运行就抛错,代码根本没走到业务逻辑。
- 原因:
Promise构造函数要求必须传入一个 executor 函数作为参数,传数字、字符串、undefined都会报同类错误。 - 修法:写成
new Promise((resolve, reject) => { ... }),形参名可自定义,但必须是函数。
以为 executor 里的代码是异步的,把"延后执行"的逻辑写在 executor 里
- 现象:
new Promise(() => console.log('A')); console.log('B')输出A B,与"Promise 是异步的"直觉冲突。 - 原因:executor 在实例化时同步立即执行;真正异步的是
then的回调(微任务)。 - 修法:需要延后处理的逻辑放进
.then()回调里;executor 内只做"启动异步任务 + 决定状态"这件事。
- 现象:
resolve()之后还继续写代码,或又调用了reject(),以为状态会再变一次- 现象:后面那段的
reject毫无效果,甚至误以为失败分支没被触发。 - 原因:Promise 状态一旦从 pending 变为 fulfilled/rejected 就定格,后续调用一律被忽略。
- 修法:
resolve(...)后直接return,或把成功/失败分支写成if / else互斥结构。
- 现象:后面那段的
只写
.then(成功回调),reject 时控制台报Uncaught (in promise)- 现象:随机抽奖类代码刷新几次就红一片报错。
- 原因:状态变为失败却没有对应的处理函数,错误被抛到全局。
- 修法:补上
.then(null, 失败回调)或.catch(...),把失败分支显式处理掉。
封装 Promise 版定时器时,把
setTimeout写在new Promise外面- 现象:函数返回的 Promise 一直是
pending,then永远不执行。 - 原因:
resolve只在 executor 作用域内可见,写在外部根本拿不到,状态无法被改变。 - 修法:
return new Promise(resolve => { setTimeout(resolve, delay) }),setTimeout必须写在 executor 内部。
- 现象:函数返回的 Promise 一直是
在定时器 Promise 的"启动语句"之后立刻打印对象状态,发现是
pending就以为写错了- 现象:
const p = delay(3000); console.log(p)显示pending。 - 原因:状态要等 3 秒后才由
resolve改写,此刻当然还是 pending;这是执行时机问题,不是语法错误。 - 修法:把观察点放进
.then(v => console.log(v)),或明确区分"创建时刻"和"状态改变时刻"。
- 现象:
练习
输出顺序预测:写出下面代码的完整输出顺序,并说明每一步的理由。
jsconst p = new Promise((resolve, reject) => { console.log(1); resolve(2); console.log(3); }); p.then(v => console.log(v)); console.log(4);抽奖改造:用
new Promise+ 随机数实现"中奖/未中奖",成功时把中奖码(随机数)传给then的第一个回调并渲染到页面,失败时在第二个回调里输出"很遗憾"文案。要求成功与失败分支互斥,且失败时控制台不出现未捕获错误。手写
delay:封装function delay(ms) { ... },返回 Promise,ms毫秒后变为成功状态并把ms作为结果传出。写出调用代码:3 秒后打印"3s 到了",并说明为什么该函数返回的 Promise 不可能进入失败状态。改造 Ajax 封装:把实际中封装的 ajax 函数改造成
ajax({ url, method, data })形式,返回 Promise;请求成功(含 4xx/5xx 能拿到响应的情况)走resolve,请求发不出去或跨域被拦截走reject。写出关键代码并标注resolve/reject分别放在哪个事件回调里。找错题:下面代码有三处问题,指出并改正。
jsfunction wait(ms) { const p = new Promise(resolve => resolve()); setTimeout(() => { p }, ms); return p; } wait(1000).then(console.log('done'));
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | Ajax总结回顾 | 31:32 |
| 2 | promise介绍 | 09:23 |
| 3 | 创建promise对象 | 12:08 |
| 4 | 为promise对象设置回调函数 | 14:37 |
| 5 | 封装定时器Promise函数 | 14:31 |
小结
- Promise 是 ES6 新增的语法结构,属于"写法层面"的解决方案,不新增任何功能;它的价值要到链式调用、回调地狱对比时才会真正显现。
- 构造函数必须接收 executor 函数;executor 同步立即执行,而
.then注册的回调是**异步(微任务)**执行——这两点是最容易搞混、也最常考的地方。 - 三种状态
pending → fulfilled / rejected,方向单一、结果唯一,一旦落定不可逆;这也决定了"一个 Promise 只对应一次异步结果"。 resolve/reject的实参会成为then回调的形参(即PromiseResult),这是把异步结果"传出去"的唯一通道。- 失败分支不处理就会产生未捕获错误,
.then的第二个参数与.catch是必须养成的习惯,而不是可选项。 - Promise 化改造的核心思路是把"参数 + 回调"混在一起的老式 API,拆成"先拿到 Promise 对象,再用
then处理结果"两步。 - 判断某段代码该写在哪:executor 内只负责启动异步任务并决定状态,一切"拿到结果之后要做什么"都放进
then。 - 定时器 Promise 化演示价值大于实用价值;真正值得反复练的是 Ajax 的 Promise 封装,它是后续 Axios 与工程化项目的直接铺垫。