简单的封装一个 ajax 请求
类似这样 ajax 请求的封装,promise 的状态扭转取决于当次 ajax 的响应状态来决定
状态的扭转的逻辑在 exec 函数体内,是被动的发生
js
const request = () => {
const exec = (resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", "https://jsonplaceholder.typicode.com/todos/1");
xhr.onload = function () {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(xhr.status);
}
}
};
xhr.addEventListener("loadstart", (e) => {
console.log("请求开始", e.type + " === " + e.loaded);
});
xhr.addEventListener("loadend", (e) => {
console.log("请求结束", e.type + " === " + e.loaded);
});
xhr.addEventListener("load", (e) => {
console.log("请求中", e.type + " === " + e.loaded);
});
xhr.addEventListener("abort", (e) => {
console.log("请求取消", e.type);
});
};
return new Promise(exec);
};
request()
.then((res) => {
console.log("请求成功", res);
})
.catch((err) => {
console.log("请求失败", err);
});
主动修改 promise
务必记着这个场景
可是某些场景我们需要 在 exec 函数 体外来决定这个 promise 的状态
所以可以把这两个参数赋值给外部的变量来 在 exec 函数外部 动态决定 promise 的状态
js
let resolvePromise, rejectPromise;
const exec = (resolve, reject) => {
resolvePromise = resolve;
rejectPromise = reject;
};
const p = new Promise(execuoter);
p.then((res) => {
console.log("1", res);
}).catch((err) => {
console.log("2", err);
});
// p 的状态就取决于何时调用 resolvePromise
setTimeout(() => {
// exec 外部
resolvePromise("aaa");
}, 1000);
实现一个请求取消
xhr 对象提供了取消请求的方法 叫做 abort
所以要实现的核心就是何时调用 abort
js
const request = () => {
// 取消的发布订阅
// 暂时不考虑其他边界场景哈
const myAbortEvent = () => {
const abort = [];
return {
on: (fn) => {
abort.push(fn);
},
emit: (data) => {
abort.forEach((h) => h(data));
},
};
};
const ev = myAbortEvent();
// 这里要使用外部变量的原因就是目前我们请求的 promise 的状态变更的渠道又多了一个
// 之前很单一就是依赖请求的响应结果
// 但是现在我们的场景多了一个,那就是取消的场景
// 取消的场景会是异步也会是同步,所以我们需要使用外部修改的方式来实现
let rp, rj;
const exec = (resolve, reject) => {
let xhr = new XMLHttpRequest();
rp = resolve;
rj = reject;
xhr.open("GET", "https://jsonplaceholder.typicode.com/todos/1");
xhr.send();
xhr.onload = function () {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
rp(JSON.parse(xhr.responseText));
} else {
rj(xhr.status);
}
}
};
xhr.addEventListener("loadstart", (e) => {
console.log("请求开始", e.type + " === " + e.loaded);
});
xhr.addEventListener("loadend", (e) => {
console.log("请求结束", e.type + " === " + e.loaded);
});
xhr.addEventListener("load", (e) => {
console.log("请求中", e.type + " === " + e.loaded);
});
xhr.addEventListener("abort", (e) => {
console.log("请求取消", e.type);
});
// 调用的时机取决于外部的调用
// 也就是说调用的场景可能是同步发生也可能异步发生
// 解决这样的场景的方案那不就是发布订阅么
// 那就内部维护一个只处理取消的简单的发布订阅场景 myAbortEvent
ev.on((msg) => {
xhr.abort();
rj(msg);
xhr = null;
});
};
const send = new Promise(exec);
return {
send: new Promise(exec),
ev: ev,
};
};
const { send, ev } = request();
send
.then((res) => {
console.log("请求成功", res);
})
.catch((err) => {
console.log("请求失败", err);
});
ev.emit("取消请求");
优化取消的逻辑
之前实现了请求的取消,但是有很多的缺陷
比如只能取消单个请求,而且用法丑陋
要想满足取消多个请求的场景,那就要考虑如何用一个状态来控制多个请求的 promise 的状态
先推翻下之前的方式,先设计下用法
给 request 函数增加传递一个 cancel 参数,相当于我们把一个统一的状态传递给了多个请求函数
现在的节奏是倒着往里推,用法推实现
js
const source = "未知的有特殊能力的对象,有 token 和 cancel";
let request1 = request({
cancel: source.token,
});
let request2 = request({
cancel: source.token,
});
source.cancel("取消请求");
开始设计这个特殊能力对象
js
// 这样的方式满足不了需求
// 需求 执行 cancel 修改 source.token 状态
// request 内部订阅这个状态,状态变更了就执行 abort
// const source = {
// cancel: (msg) => { // 执行 cancel 后就能修改 token 的状态 },
// token: new Promise(() => {}),
// };
// 直接换函数,简单的来个自执行
const source = (function () {
let cancel;
let token = new Promise((resolve, reject) => {
cancel = resolve;
});
return {
token: token,
cancel: cancel,
};
})();
// 用法
let request1 = request({
cancel: source.token,
});
let request2 = request({
cancel: source.token,
});
request1
.then((res) => {
console.log("request1", res);
})
.catch((err) => {
console.log("request1 err", err);
});
request2
.then((res) => {
console.log("request2", res);
})
.catch((err) => {
console.log("request2 err", err);
});
source.cancel("取消所有请求");
好,开始修改 request 内部方法
js
const request = (config = {}) => {
let rp, rj;
const exec = (resolve, reject) => {
let xhr = new XMLHttpRequest();
rp = resolve;
rj = reject;
xhr.open("GET", "https://jsonplaceholder.typicode.com/todos/1");
xhr.send();
// 每个 request 返回的 promise 内部对这个 cancel 状态进行监听
// 如果状态扭转了就执行 abort
if (config.cancel) {
config.cancel.then((res) => {
xhr.abort();
rj(res);
xhr = null;
});
}
xhr.onload = function () {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
rp(JSON.parse(xhr.responseText));
} else {
rj(xhr.status);
}
}
};
xhr.addEventListener("loadstart", (e) => {
console.log("请求开始", e.type + " === " + e.loaded);
});
xhr.addEventListener("loadend", (e) => {
console.log("请求结束", e.type + " === " + e.loaded);
});
xhr.addEventListener("load", (e) => {
console.log("请求中", e.type + " === " + e.loaded);
});
xhr.addEventListener("abort", (e) => {
console.log("请求取消", e.type);
});
};
return new Promise(exec);
};
整合下代码
js
const request = (config = {}) => {
let rp, rj;
const exec = (resolve, reject) => {
let xhr = new XMLHttpRequest();
rp = resolve;
rj = reject;
xhr.open("GET", "https://jsonplaceholder.typicode.com/todos/1");
xhr.send();
if (config.cancel) {
config.cancel.then((res) => {
xhr.abort();
rj(res);
xhr = null;
});
}
xhr.onload = function () {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
rp(JSON.parse(xhr.responseText));
} else {
rj(xhr.status);
}
}
};
xhr.addEventListener("loadstart", (e) => {
console.log("请求开始", e.type + " === " + e.loaded);
});
xhr.addEventListener("loadend", (e) => {
console.log("请求结束", e.type + " === " + e.loaded);
});
xhr.addEventListener("load", (e) => {
console.log("请求中", e.type + " === " + e.loaded);
});
xhr.addEventListener("abort", (e) => {
console.log("请求取消", e.type);
});
};
return new Promise(exec);
};
const source = (function () {
let cancel;
let token = new Promise((resolve, reject) => {
cancel = resolve;
});
return {
token: token,
cancel: cancel,
};
})();
let request1 = request({
cancel: source.token,
});
let request2 = request({
cancel: source.token,
});
request1
.then((res) => {
console.log("request1", res);
})
.catch((err) => {
console.log("request1 err", err);
});
request2
.then((res) => {
console.log("request2", res);
})
.catch((err) => {
console.log("request2 err", err);
});
source.cancel("取消所有请求");
现在就实现了取消多个请求的场景 并且还优化了我们的用法 想取消哪个就在哪个请求里传递这个状态就可以了
AbortController 取消控制器
js 提供了一个控制器对象,可以按照需求中止一个或者多个请求
但是这个只对 fetch 这种现代化的请求方案有用
我想我们写的请求里也能支持取消控制器的方式来取消请求
简单的了解下 AbortController
js
const ab = new AbortController();
console.log("控制器的是否是取消状态", ab.signal.aborted);
ab.abort(); // 用来修改 ab.signal.aborted 状态
// ab.signal 可以绑定一个 abort 事件来查询当前实例的取消状态,用这个钩子可以区分出同步和异步的场景
ab.signal.addEventListener("abort", () => {
console.log("取消");
});
开始扩展支持这种取消请求的方式在我们写好的基础上
js
const request = (config = {}) => {
let rp, rj;
const exec = (resolve, reject) => {
let xhr = new XMLHttpRequest();
rp = resolve;
rj = reject;
xhr.open("GET", "https://jsonplaceholder.typicode.com/todos/1");
xhr.send();
if (config.cancel) {
config.cancel.then((res) => {
xhr.abort();
rj(res);
xhr = null;
});
}
// 判断下是否传递了 config.signal
if (config.signal) {
// 查询 config.signal 的取消状态
if (config.signal.aborted) {
xhr.abort();
rj("请求取消");
xhr = null;
} else {
// 监听 abort 事件
config.signal.addEventListener("abort", () => {
// 触发了取消事件就执行取消逻辑
if (config.signal.aborted) {
xhr.abort();
rj(config.signal.reason);
xhr = null;
}
});
}
}
xhr.onload = function () {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
rp(JSON.parse(xhr.responseText));
} else {
rj(xhr.status);
}
}
};
xhr.addEventListener("loadstart", (e) => {
console.log("请求开始", e.type + " === " + e.loaded);
});
xhr.addEventListener("loadend", (e) => {
console.log("请求结束", e.type + " === " + e.loaded);
});
xhr.addEventListener("load", (e) => {
console.log("请求中", e.type + " === " + e.loaded);
});
xhr.addEventListener("abort", (e) => {
console.log("请求取消", e.type);
});
};
return new Promise(exec);
};
const source = new AbortController();
let request1 = request({
signal: source.signal,
});
let request2 = request({
signal: source.signal,
});
request1
.then((res) => {
console.log("request1", res);
})
.catch((err) => {
console.log("request1 err", err);
});
request2
.then((res) => {
console.log("request2", res);
})
.catch((err) => {
console.log("request2 err", err);
});
source.abort("取消所有请求");
基本上实现了,但是代码有点些许重复
提取下可复用代码
js
const request = (config = {}) => {
let rp, rj;
const exec = (resolve, reject) => {
let xhr = new XMLHttpRequest();
rp = resolve;
rj = reject;
xhr.open("GET", "https://jsonplaceholder.typicode.com/todos/1");
xhr.send();
let cb;
if (config.cancel || config.signal) {
cb = (msg) => {
xhr.abort();
rj(msg);
xhr = null;
};
}
if (config.cancel) {
config.cancel.then(cb);
}
if (config.signal) {
config.signal.aborted
? cb("请求取消")
: config.signal.addEventListener("abort", () => {
if (config.signal.aborted) {
cb(config.signal.reason);
}
});
}
xhr.onload = function () {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
rp(JSON.parse(xhr.responseText));
} else {
rj(xhr.status);
}
}
};
xhr.addEventListener("loadstart", (e) => {
console.log("请求开始", e.type + " === " + e.loaded);
});
xhr.addEventListener("loadend", (e) => {
console.log("请求结束", e.type + " === " + e.loaded);
});
xhr.addEventListener("load", (e) => {
console.log("请求中", e.type + " === " + e.loaded);
});
xhr.addEventListener("abort", (e) => {
console.log("请求取消", e.type);
});
};
return new Promise(exec);
};
const source = new AbortController();
let request1 = request({
signal: source.signal,
});
let request2 = request({
signal: source.signal,
});
request1
.then((res) => {
console.log("request1", res);
})
.catch((err) => {
console.log("request1 err", err);
});
request2
.then((res) => {
console.log("request2", res);
})
.catch((err) => {
console.log("request2 err", err);
});
source.abort("取消所有请求");
回答如何实现取消请求
使用的请求的技术方案不同,取消的方式不同
fetch 取消
通过 AbortController 控制器取消
js
const controller = new AbortController();
const signal = controller.signal;
fetch("http://www.baidu.com", { signal })
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log(err);
});
controller.abort();
axios 也就是 xhr 的取消
方案 1
通过 CancelToken 类创建一个取消控制器
js
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios
.get("http://www.baidu.com", {
cancelToken: source.token,
})
.catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log("Request canceled", thrown.message);
} else {
// 处理错误
}
});
source.cancel("取消");
方案 2
通过 AbortController 控制器取消
js
const controller = new AbortController();
axios
.get("http://www.baidu.com", {
signal: controller.signal,
})
.catch(function (thrown) {
// 处理错误
});
controller.abort("取消");
原理,我们已经敲过了啊