脱离jQuery实现自定义ajax后链式调用无法等待异步请求完成如何解决
更新:新增简化演示jsfiddle,https://jsfiddle.net/47sfj3Lv/3/,用极少量代码复现问题。
我正在尝试脱离jQuery重构代码,原先填充表格的代码如下:
var hb = new hbLister(url: '#attributes.listURL#') .getData(#url.page#, #url.per#) .search(searchColumn, searchParam) .render();
hbLister负责初始化相关配置getData执行$.ajax请求search执行console.log('filtering data')并对JavaScript对象应用搜索条件render将结果渲染到页面
核心要求是search必须等getData内的ajax请求完成后再执行。
我目前写的ajax构造器简化代码如下:
let ajax = function (options, hooks, headers) { let that = this; // enforce parameter types // copy parameters to this.base this.base = { options: options, hooks: hooks, headers: headers } return function (url, options, data, hooks, headers) { // enforce variable types // merge options and hooks with their base.counterparts headers = new Headers(Object.assign({}, that.base.headers, headers)); options.headers = headers; return fetch(url, options) .then(response => { return response.json().then(json => { console.log('processing'); if (response.ok) { // it's omitted here but the Base functions are defined // in the constructor parameters hooks.successBase(json, response.status, response.headers); hooks.success(response.json, response.status, response.headers) } else { hooks.failureBase(json, response.status, response.headers); hooks.failure(response.json, response.status, response.headers) } }) }); } }
预期用法是:
let tableLister = new ajax()
之后getData内调用:
tableLister = tableLister(hb.url, // url { type: "GET" }, // options config.data, // data { success: successFn } // hooks, success being the callback )
原先jQuery版本执行顺序为:先输出processing,再输出filtering data,符合预期。但现在基于fetch的版本执行顺序为先输出filtering data、报错,之后才输出processing,链式调用.search(...).render()不会等待ajax请求完成。
我判断解决方案应该用到async和await,但尝试后无法生效,尝试的代码如下:
return await (async function () { console.log('fetching') let fetcher = await fetch(url, options); console.log('getting json'); return await fetcher.json().then((json) => { console.log('have json, doing hooks'); if (fetcher.ok) { let argXHR = { json: json} hooks.successBase(argXHR, hooks.params); hooks.success.forEach(v => v(argXHR, hooks.params)); hooks.afterSend(argXHR, hooks.params); } else { let argXHR = { json: json,} hooks.failureBase(argXHR, hooks.params); hooks.failure.forEach(v => v(argXHR, hooks.params)); hooks.afterError(argXHR, hooks.params); } console.log('finished hooks') }) }())
无论怎么调整,链式调用都会在await执行完成前继续向下执行。
我目前用XMLHttpRequest实现的同步版本可以正常支持链式调用.getData().search(...).render(),因为请求完成、回调执行前函数不会返回,但我更希望用fetch实现需求,相关代码如下:
let xhr = new XMLHttpRequest() let urlParams = []; Object.keys(data).forEach((v) => urlParams.push(v + '=' + encodeURIComponent(data[v]))); urlParams = urlParams.join('&') xhr.open(options.method, options.url, false); xhr.onreadystatechange = function(state) { if (this.readyState == 4) { let json = JSON.parse(xhr.responseText) hooks.successBase(json, xhr.status, xhr.getResponseHeader); hooks.success.forEach(v => v(json, xhr.status, xhr.getResponseHeader)); } } xhr.onerror = function() { let json = JSON.parse(xhr.responseText) hooks.failureBase(json, xhr.status, xhr.getResponseHeader); hooks.failure.forEach(v => v(json, xhr.status, xhr.getResponseHeader)); } for (h in headers) { xhr.setRequestHeader(h, headers[h]) } xhr.send(urlParams)
请问如何基于fetch实现支持等待异步完成的链式调用?
你之前的async/await尝试没生效,核心原因是普通函数内return await返回的本质还是Promise实例,你调用时没有等待这个Promise状态变更就直接执行后续的search方法,才会出现顺序错误。同步XHR虽然能实现需求,但会阻塞主线程,体验很差,不符合现代前端开发规范。
推荐用内部维护Promise队列的封装方案,完全保留你之前的链式调用写法,自动按顺序执行异步操作:
class TableLister { constructor(config) { this.url = config.url; // 内部维护执行队列,初始为已完成状态 this.taskQueue = Promise.resolve(); // 存储请求返回的原始数据 this.rawData = null; } getData(page, per) { // 把请求逻辑追加到队列末尾 this.taskQueue = this.taskQueue.then(async () => { const requestUrl = `${this.url}?page=${page}&per=${per}`; const res = await fetch(requestUrl, { method: 'GET' }); this.rawData = await res.json(); console.log('processing'); }); // 始终返回实例本身,支持链式调用 return this; } search(column, param) { this.taskQueue = this.taskQueue.then(() => { console.log('filtering data'); // 对已拿到的rawData执行筛选逻辑 this.rawData = this.rawData.filter(item => item[column]?.includes(param)); }); return this; } render() { this.taskQueue = this.taskQueue.then(() => { // 你的渲染逻辑,把rawData插入到页面表格中 console.log('rendering', this.rawData); }); // 可选:添加全局错误捕获 this.taskQueue.catch(err => console.error('执行错误:', err)); return this; } }
调用方式和你原先的写法完全一致:
const hb = new TableLister({url: '#attributes.listURL#'}) .getData(#url.page#, #url.per#) .search(searchColumn, searchParam) .render();
如果不想修改现有封装逻辑,也可以调整调用方式,用async/await等待异步完成:
async function loadTableData() { const hb = new hbLister({url: '#attributes.listURL#'}); // 等待getData的异步请求完成 await hb.getData(#url.page#, #url.per#); hb.search(searchColumn, searchParam); hb.render(); } loadTableData();
内容的提问来源于stack exchange,提问作者Regular Jo

