当前位置: 首页 > news >正文

Promise 与 Async Await 深度解析

前言

JavaScript 的异步编程经历了三个时代:回调函数(Callback)、 Promise 、async/await。三者并非相互替代,而是层层构建在同一套运行时基础之上。要真正掌握 async/await,必须先理解 Promise;要真正理解 Promise,必须先理解 JavaScript 的执行模型。本文从运行时底层出发,逐层向上,完整建立这套知识体系。


一、地基:JavaScript 的执行模型

1.1 单线程与调用栈

JavaScript 是单线程语言,同一时刻只能执行一段代码。所有代码的执行通过**调用栈(Call Stack)**管理,遵循后进先出(LIFO)原则:

调用栈
┌────────────────────────┐
│  当前执行的函数(栈顶)  │
├────────────────────────┤
│  调用者函数             │
├────────────────────────┤
│  全局执行上下文(栈底)  │
└────────────────────────┘函数调用 → 压栈
函数返回 → 出栈
调用栈清空 → 可以处理下一个任务

"单线程"意味着 JavaScript 自身不能并行执行代码,但浏览器是多线程的。网络请求、文件 I/O、定时器,都由浏览器的其他线程在后台处理,完成后通过任务队列把回调交还给 JavaScript 线程执行。这是异步机制的物理基础。

1.2 两类任务队列

浏览器维护着两种性质不同的任务队列:

┌─────────────────────────────────────────────┐
│              宏任务队列(Task Queue)         │
│                                             │
│  - setTimeout / setInterval 回调            │
│  - UI 事件回调(click、input...)            │
│  - 网络请求回调(XHR、fetch 底层事件)        │
│  - MessageChannel 回调                      │
└─────────────────────────────────────────────┘┌─────────────────────────────────────────────┐
│              微任务队列(MicroTask Queue)    │
│                                             │
│  - Promise .then / .catch / .finally 回调   │
│  - async/await 的续体(continuation)        │
│  - queueMicrotask()                         │
│  - MutationObserver                         │
└─────────────────────────────────────────────┘

两者的核心区别:微任务优先级高于 宏 任务,每个宏任务执行完毕后,必须把微任务队列彻底清空,才能继续下一个宏任务。

1.3 事件循环(Event Loop )

事件循环是 JavaScript 运行时的调度核心,它不停地执行以下节拍:

┌──────────────────────────────────────────────────────────┐
│                      Event Loop                          │
│                                                          │
│   ┌─────────────────────────────────────────────────┐   │
│   │  ① 从宏任务队列取出 一个 任务,入栈执行           │   │
│   │     直到调用栈清空                               │   │
│   └────────────────────────┬────────────────────────┘   │
│                            │                             │
│   ┌────────────────────────▼────────────────────────┐   │
│   │  ② 清空微任务队列                                │   │
│   │     逐个取出并执行,直到队列为空                  │   │
│   │     (执行中新加入的微任务也在本轮处理)           │   │
│   └────────────────────────┬────────────────────────┘   │
│                            │                             │
│   ┌────────────────────────▼────────────────────────┐   │
│   │  ③ 如有必要,执行 UI 渲染                        │   │
│   └────────────────────────┬────────────────────────┘   │
│                            │                             │
│                            └────────────► 回到 ①        │
└──────────────────────────────────────────────────────────┘

用一段代码验证这个模型:

console.log('A');                       // 同步setTimeout(() => console.log('B'), 0);  // 宏任务Promise.resolve().then(() => {console.log('C');                     // 微任务Promise.resolve().then(() => {console.log('D');                   // 微任务中新增的微任务,本轮一并清空});
});console.log('E');                       // 同步// 输出顺序:A → E → C → D → B

执行分析:

同步阶段:   A → E
微任务阶段: C → D    (D 在 C 执行时才入队,但本轮全部清空)
宏任务阶段: B

二、Promise:一个精确的状态机

2.1 三种状态与不可逆 转换

Promise 规范(Promises/A+)定义了一个严格的状态机:

                    resolve(value)┌─────────────────► fulfilled│                   .value = valuepending          │.value = undefined.reason = undefined│└─────────────────► rejectedreject(reason)     .reason = reason

三条铁律:

  1. 初始状态永远是 pending
  2. 状态只能从 pending 转换,且只能转换一次,不可逆
  3. 状态一旦改变,关联的值(valuereason)永久固定

多次调用 resolve / reject 只有第一次生效,其余被静默忽略。

2.2 内部结构——用伪代码还原

下面用 Class 语法还原 Promise 的核心实现逻辑(符合 Promises/A+ 规范精神):

class MyPromise {#state = 'pending';#value = undefined;#reason = undefined;#onFulfilledCallbacks = [];   // 等待 fulfilled 的回调队列#onRejectedCallbacks  = [];   // 等待 rejected  的回调队列constructor(executor) {// executor 同步立即执行// 内部异常被捕获并自动转为 rejecttry {executor(this.#resolve.bind(this),this.#reject.bind(this));} catch (err) {this.#reject(err);}}#resolve(value) {if (this.#state !== 'pending') return;   // 幂等保护this.#state = 'fulfilled';this.#value = value;// 将所有等待成功的回调推入微任务队列this.#onFulfilledCallbacks.forEach(fn =>queueMicrotask(() => fn(value)));}#reject(reason) {if (this.#state !== 'pending') return;   // 幂等保护this.#state = 'rejected';this.#reason = reason;this.#onRejectedCallbacks.forEach(fn =>queueMicrotask(() => fn(reason)));}then(onFulfilled, onRejected) {// .then() 永远返回新 Promise,构成链式调用return new MyPromise((resolve, reject) => {const handleFulfilled = (value) => {try { resolve(onFulfilled(value)); }catch (e) { reject(e); }};const handleRejected = (reason) => {try { resolve(onRejected(reason)); }catch (e) { reject(e); }};if (this.#state === 'fulfilled') {// 已经 fulfilled:直接放入微任务队列queueMicrotask(() => handleFulfilled(this.#value));} else if (this.#state === 'rejected') {queueMicrotask(() => handleRejected(this.#reason));} else {// 还在 pending:先存起来,等 resolve/reject 调用时再触发this.#onFulfilledCallbacks.push(handleFulfilled);this.#onRejectedCallbacks.push(handleRejected);}});}catch(onRejected) {return this.then(undefined, onRejected);}finally(onFinally) {return this.then(value  => Promise.resolve(onFinally()).then(() => value),reason => Promise.resolve(onFinally()).then(() => { throw reason; }));}
}

从这段伪代码可以提炼出四个关键结论:

结论一.then() 的回调永远是异步的(通过 queueMicrotask),即使 Promise 已经是 fulfilled 状态也不例外,这保证了行为的一致性和可预测性。

结论二.then() 永远返回一个新的 Promise,新 Promise 的状态由回调的返回值或抛出的异常决定,这是链式调用的基础。

结论三:当 Promise 处于 pending 时,.then() 注册的回调被暂存到内部队列,等待 resolve / reject 调用后才被触发。

结论四:executor 内部抛出的同步异常被 try/catch 捕获,自动转换为 reject,Promise 不会因此崩溃。

2.3 new Promise 的执行时序

console.log('1');const p = new Promise((resolve, reject) => {console.log('2');        // executor 同步执行setTimeout(() => {console.log('4');      // 宏任务resolve('result');     // 在宏任务中 resolve}, 0);console.log('3');        // executor 继续同步执行
});p.then(val => {console.log('5', val);   // resolve 后放入微任务队列
});// 输出:1 → 2 → 3 → 4 → 5 result

精确执行节拍:

① 同步阶段(调用栈)print '1'new Promise → executor 立即入栈print '2'setTimeout → 注册定时器,推入宏任务队列(不执行回调)print '3'executor 出栈,Promise 状态 = pendingp.then(cb) → cb 存入 #onFulfilledCallbacks调用栈清空② 微任务队列(此时为空,跳过)③ 宏任务队列:取出 setTimeout 回调print '4'resolve('result') 被调用→ #state: pending → fulfilled→ #value = 'result'→ 将 .then 的 cb 推入微任务队列④ 当前宏任务执行完毕,清空微任务队列取出 cb,执行print '5 result'

三、.then() 链:数据如何在 Promise 链中流动

3.1 链式调用的本质

每个 .then() 返回一个新 Promise,新 Promise 的状态由回调的返回值决定:

Promise.resolve(1).then(val => {console.log(val);            // 1return val + 1;              // 返回普通值 → 下一个 Promise fulfilled(2)}).then(val => {console.log(val);            // 2return Promise.resolve(val * 10); // 返回 Promise → 等待其落定}).then(val => {console.log(val);            // 20throw new Error('oops');     // 抛出异常 → 下一个 Promise rejected}).then(val => {console.log('不会执行');      // 跳过,因为上一个是 rejected}).catch(err => {console.log(err.message);    // 'oops'return 'recovered';          // catch 返回普通值 → 链恢复为 fulfilled}).then(val => {console.log(val);            // 'recovered',链已恢复});

返回值决定下一个 Promise 状态的完整规则:

回调的行为 下一个 Promise 的状态
return 普通值(含 undefined fulfilled,值为该返回值
return 一个 Promise 等待该 Promise 落定,状态与之同步
抛出异常 rejected,reason 为该异常
onFulfilled(跳过) 透传上一个 Promise 的状态和值

3.2 错误透传机制

.then() 只传入 onFulfilled 时,onRejected 默认为 reason => { throw reason },错误沿链向下传递,直到被 .catch() 捕获:

fetchUser()         // 假设此处 rejected.then(processA)   // 跳过(透传 rejection).then(processB)   // 跳过(透传 rejection).then(processC)   // 跳过(透传 rejection).catch(handle);   // 在这里统一捕获

这使得错误处理可以集中在链的末尾,而不必每一步都单独处理。


四、async/await:Promise 的语法糖

4.1 async 函数的三个隐式行为

async 关键字赋予函数三个隐式能力:

// 隐式行为一:返回值自动包装成 fulfilled Promise
async function f1() { return 42; }
// 等价于:
function f1() { return Promise.resolve(42); }// 隐式行为二:未捕获的异常自动转为 rejected Promise
async function f2() { throw new Error('crash'); }
// 等价于:
function f2() { return Promise.reject(new Error('crash')); }// 隐式行为三:函数体内可以使用 await 暂停执行
async function f3() {const result = await somePromise();return result;
}

4.2 await 的本质——脱糖(Desugaring)

await.then() 的语法糖,JavaScript 引擎会把 async 函数转换为 Promise 链:

// 你写的代码:
async function processData() {console.log('start');const a = await stepA();console.log('got a:', a);const b = await stepB(a);return b;
}// 引擎等价处理为:
function processData() {console.log('start');return stepA().then(a => {console.log('got a:', a);return stepB(a);}).then(b => {return b;});
}

await expr 精确地做了以下三件事:

  1. expr 求值,若不是 Promise 则用 Promise.resolve() 包装
  2. 向该 Promise 注册 .then(continuation),其中 continuationawait 之后的剩余代码
  3. 暂停当前 async 函数,将控制权交还给调用栈上层(函数从调用栈退出,不阻塞主线程)

4.3 "暂停"与"阻塞"的本质区别

async function main() {console.log('1: main 开始');const result = await longRunningAsync(); // 暂停 main,不阻塞主线程console.log('3: main 恢复,结果:', result);
}main();
console.log('2: main 暂停后,主线程继续执行这里');// 输出:
// 1: main 开始
// 2: main 暂停后,主线程继续执行这里
// 3: main 恢复,结果: ...

await 暂停的只是当前 async 函数自身,主线程照常继续执行后续代码。这是与同步阻塞的根本区别。

4.4 await 的恢复时机

await 恢复执行通过微任务队列调度,即使等待的是一个已经 fulfilled 的 Promise,恢复也是异步的:

async function demo() {await Promise.resolve();             // 等待一个已 fulfilled 的 Promiseconsole.log('async 恢复');           // 依然是微任务,异步执行
}demo();
console.log('同步代码');setTimeout(() => console.log('宏任务'), 0);// 输出:
// 同步代码
// async 恢复     ← 微任务,先于宏任务
// 宏任务

4.5 async/await 与 new Promise 的选择原则

// 场景一:包装回调式 API → 必须使用 new Promise
// setTimeout、XMLHttpRequest、Node.js fs 等没有原生 Promise 的 API
function delay(ms) {return new Promise(resolve => setTimeout(resolve, ms));
}function readFileAsync(path) {return new Promise((resolve, reject) => {fs.readFile(path, 'utf8', (err, data) => {if (err) reject(err);else resolve(data);});});
}// 场景二:组合多个已有的 Promise → 用 async/await,更清晰
async function composedFlow() {const user    = await fetchUser(id);const orders  = await fetchOrders(user.id);return summarize(orders);
}// 场景三:反模式——async executor(危险,务必避免)
// ❌ executor 内部 await 之后抛出的异常,Promise 无法捕获
async function dangerous() {return new Promise(async (resolve, reject) => {const result = await riskyOperation(); // 抛出时 Promise 不会 rejectresolve(result);});
}// ✅ 正确写法:直接 return await,让 async 函数本身处理异常
async function safe() {return await riskyOperation();
}

async executor 的危险本质:new Promisetry/catch 只能捕获同步异常async executorawait 之后抛出的异常发生在独立的微任务中,已超出 try/catch 的保护范围,变成未处理的 rejection,导致静默失败。


五、完整的执行时序——综合示例

以一个"获取用户数据并写入本地存储"的场景,完整追踪微任务与宏任务的流转:

async function run() {console.log('[1] run 开始');const user = await fetchUser(1);          // 暂停点 Aconsole.log('[4] 拿到用户:', user.name);const orders = await fetchOrders(user.id); // 暂停点 Bconsole.log('[6] 写入完成');return orders;
}run();
console.log('[2] run 已暂停,同步代码继续');
setTimeout(() => console.log('[3] 宏任务标记'), 0);

完整执行时序:

════════════════ ① 同步阶段 ════════════════print '[1]'fetchUser(1) 调用 → 发起网络请求(浏览器后台线程处理)返回 pending Promise Aawait Promise A → run() 暂停,退出调用栈print '[2]'setTimeout → 注册宏任务调用栈清空════════════════ ② 微任务(此时为空)════════════════════════════════ ③ 宏任务:setTimeout 回调 ════════════════print '[3]'════════════════ ④ 后台线程:网络请求完成 ════════════════浏览器将 fetchUser 的响应回调推入宏任务队列════════════════ ⑤ 宏任务:fetchUser 响应处理 ════════════════Promise A: pending → fulfilled,value = user将 run() 的续体推入微任务队列════════════════ ⑥ 微任务:恢复 run() ════════════════run() 从 await fetchUser() 处恢复const user = { id: 1, name: '...' }print '[4]'fetchOrders(user.id) 调用 → 再次发起网络请求返回 pending Promise Bawait Promise B → run() 再次暂停════════════════ ⑦ 宏任务:fetchOrders 响应处理 ════════════════Promise B: pending → fulfilled,value = orders将 run() 的续体推入微任务队列════════════════ ⑧ 微任务:恢复 run() ════════════════run() 从 await fetchOrders() 处恢复print '[6]'run() 执行完毕,返回 fulfilled Promise

六、错误处理的完整机制

6.1 try/catch 与 await 的配合

try/catch 配合 await 能捕获 rejected Promise,是因为 await 在 Promise 是 rejected 状态时,会把 reason 作为异常重新抛出,被外层 try/catch 捕获:

async function safeRun() {try {const user   = await fetchUser(id);      // 若 reject → catchconst orders = await fetchOrders(user.id); // 若 reject → catchreturn summarize(orders);} catch (err) {// 捕获所有 rejected Promise 和同步异常console.error('操作失败:', err);return null;} finally {cleanup(); // 无论成功失败都执行}
}

6.2 未处理的 rejection

Promise rejected 但没有任何 .catch()try/catch 处理时,浏览器触发 unhandledrejection 事件:

window.addEventListener('unhandledrejection', (event) => {console.error('未处理的 Promise rejection:', event.reason);event.preventDefault();
});

6.3 常见的错误处理陷阱

// ❌ 陷阱一:async 函数调用后忽略 rejection
async function risky() { throw new Error('lost'); }
risky();              // rejection 未处理,触发 unhandledrejection// ✅ 正确:
risky().catch(console.error);// ❌ 陷阱二:Promise 链末尾没有 .catch()
fetchData().then(process).then(save);        // process 或 save 抛出,错误消失// ✅ 正确:
fetchData().then(process).then(save).catch(handleError);// ❌ 陷阱三:误以为 resolve 之后 executor 停止执行
new Promise((resolve, reject) => {resolve('first');console.log('这行仍然会执行'); // resolve 不是 returnreject('ignored');             // 被幂等保护忽略,但代码继续运行
});// ✅ 正确:需要停止执行时,显式 return
new Promise((resolve, reject) => {if (condition) return resolve('ok'); // return 阻止后续执行reject('fail');
});

七、性能维度:并发控制

7.1 串行 vs 并发

await 的常见误用是将可以并发的操作串行化:

// ❌ 串行执行,总耗时 = t(A) + t(B) + t(C)
async function serial() {const a = await fetchA();   // 等 A 完成const b = await fetchB();   // 再等 B 完成const c = await fetchC();   // 再等 C 完成return [a, b, c];
}// ✅ 并发执行,总耗时 = max(t(A), t(B), t(C))
async function concurrent() {const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);return [a, b, c];
}

Promise.all 并发的关键:三个 Promise 在传入时同时创建(即同时发起请求),Promise.all 只是等待它们全部落定后返回结果数组。

7.2 四种并发工具对比

方法 语义 适用场景
Promise.all(arr) 全部 fulfilled 则成功;任一 rejected 则立即失败 所有结果都必须成功时
Promise.allSettled(arr) 等所有落定,无论成功失败,返回每个的状态结果 需要知道每个的结果,失败不中止
Promise.race(arr) 第一个落定(不论成功失败)即返回 超时控制、取最快响应
Promise.any(arr) 第一个 fulfilled 则返回;全部 rejected 才失败 多源请求取最快成功的

实际运用示例:

// 超时控制:请求 vs 定时器竞速
function withTimeout(promise, ms) {const timeout = new Promise((_, reject) =>setTimeout(() => reject(new Error(`超时:${ms}ms`)), ms));return Promise.race([promise, timeout]);
}const result = await withTimeout(fetchData(), 5000);// 多源降级:优先用快的,全失败才报错
const data = await Promise.any([fetchFromCDN1(url),fetchFromCDN2(url),fetchFromOrigin(url),
]);

八、知识体系总览

┌───────────────────────────────────────────────────────────────┐
│                    JavaScript 运行时                           │
│                                                               │
│  ┌─────────────┐    ┌──────────────────────────────────────┐  │
│  │  调用栈      │    │           任务队列                    │  │
│  │  (同步执行)  │    │                                      │  │
│  │             │    │  宏任务队列          微任务队列        │  │
│  │  main()     │    │  ┌─────────────┐  ┌──────────────┐  │  │
│  │  func()     │    │  │ setTimeout  │  │ Promise.then │  │  │
│  │  ...        │    │  │ I/O 回调    │  │ await 续体   │  │  │
│  │             │    │  │ UI 事件     │  │              │  │  │
│  └─────────────┘    │  └─────────────┘  └──────────────┘  │  │
│                     │      优先级:微任务 > 宏任务            │  │
│                     └──────────────────────────────────────┘  │
│                                                               │
│  ┌─────────────────────────────────────────────────────────┐  │
│  │                  Promise 状态机                          │  │
│  │                                                         │  │
│  │  pending ──resolve()──► fulfilled                       │  │
│  │     └──────reject()───► rejected                        │  │
│  │                                                         │  │
│  │  .then(fn)  → fn 放入微任务队列,返回新 Promise          │  │
│  │  .catch(fn) → .then(undefined, fn) 的语法糖             │  │
│  └─────────────────────────────────────────────────────────┘  │
│                                                               │
│  ┌─────────────────────────────────────────────────────────┐  │
│  │                  async / await                          │  │
│  │                                                         │  │
│  │  async function → 返回值自动包装 Promise                 │  │
│  │  await expr     → .then() 语法糖                        │  │
│  │                   暂停函数,不阻塞主线程                  │  │
│  │                   通过微任务队列恢复执行                  │  │
│  └─────────────────────────────────────────────────────────┘  │
└───────────────────────────────────────────────────────────────┘

总结

概念 本质一句话
调用栈 同步代码的执行容器,清空才能处理异步回调
宏任务队列 浏览器 I/O 完成后把回调放这里,一次 Event Loop 处理一个
微任务队列 Promise 回调放这里,每个宏任务后全部清空,优先级更高
new Promise(executor) 创建状态机,executor 同步执行,通过 resolve/reject 驱动状态转换
resolve(value) 推进状态至 fulfilled,将 .then 回调推入微任务队列
reject(reason) 推进状态至 rejected,将 .catch 回调推入微任务队列
.then(fn) fn 异步(微任务)执行,永远返回新 Promise
async function 返回值自动 Promise 化,内部可用 await
await expr 向 expr 的 Promise 注册续体,暂停 async 函数,不阻塞主线程

Promise 与 async/await 的设计哲学是一致的:把异步操作的结果抽象为一个值,让异步代码的组织方式尽可能接近同步代码的线性结构。掌握它的关键不在于 记忆 API,而在于理解事件循环的调度节拍、Promise 状态机的转换规则,以及 await 暂停与恢复的微任务机制。三者合而为一,构成了 JavaScript 现代异步编程的完整心智模型。

http://www.jsqmd.com/news/931852/

相关文章:

  • 2026衡阳卫生间免砸砖防水、外墙、地下室、楼顶渗漏+彩钢瓦、阳光房漏水 本地专业防水公司TOP5权威推荐(2026年6月本地最新深度调研) - 企业资讯
  • 04|精准测试平台的数据存储:MySQL、Redis、Elasticsearch 怎么分工?
  • 收藏!AI时代,这10类工作将越来越香,普通人如何避开内卷安稳立足?
  • 深度解析PanoHead:如何实现360度全头部3D生成的技术突破
  • 如何快速掌握围棋AI分析:LizzieYzy完整使用指南
  • 基于PHP的抖音无水印视频解析技术实现与架构解析
  • 2026柳州卫生间免砸砖防水、外墙、地下室、楼顶渗漏+彩钢瓦、阳光房漏水 本地专业防水公司TOP5权威推荐(2026年6月本地最新深度调研) - 企业资讯
  • ctf-git篇
  • 2026襄阳卫生间免砸砖防水、外墙、地下室、楼顶渗漏+彩钢瓦、阳光房漏水 本地专业防水公司TOP5权威推荐(2026年6月本地最新深度调研) - 企业资讯
  • 机组电涡流传感器TR-81
  • Chinese-LLaMA-2-7B与原始Llama-2对比:中文理解能力提升分析
  • 逆势承压!2026汽车活塞市场分析:行业发展趋势与未来前景预判
  • 2026 时尚家庭选购四件套5大宝藏家纺品牌完整盘点 - qiqi1113
  • 如何将ArcMenu集成到现有项目:迁移与适配完整指南
  • ASP.NET Core .NET 10 错误响应体系全景:从 BadRequest 到编译器基础设施
  • 5倍提速!用Fast-GitHub突破国内访问GitHub的技术瓶颈
  • 外星人将在 2026 年台北电脑展发布多款游戏显示器,7 月及秋季陆续上市
  • Sora 2珠宝展示不卡顿?揭秘底层NeRF-Transformer混合架构与实时LOD调度机制
  • 2026青岛卫生间免砸砖防水、外墙、地下室、楼顶渗漏+彩钢瓦、阳光房漏水 本地专业防水公司TOP5权威推荐(2026年6月本地最新深度调研) - 企业资讯
  • BigBird-Pegasus-large-arxiv常见问题解答:从安装到使用的全面排错指南 [特殊字符]
  • 抖音下载器终极指南:三步实现无水印视频批量下载,免费构建你的内容收藏库
  • 2026广州卫生间免砸砖防水、外墙、地下室、楼顶渗漏+彩钢瓦、阳光房漏水 本地专业防水公司TOP5权威推荐(2026年6月本地最新深度调研) - 企业资讯
  • VisualCppRedist AIO:Windows系统运行库问题的终极解决方案
  • 2026桂林卫生间免砸砖防水、外墙、地下室、楼顶渗漏+彩钢瓦、阳光房漏水 本地专业防水公司TOP5权威推荐(2026年6月本地最新深度调研) - 企业资讯
  • AMD Ryzen系统管理单元调试工具:硬件级电源管理与超频优化终极指南
  • 复古外壳智能改造:Echo Dot移植与3D打印适配全指南
  • PP-FormulaNet-L实战应用:在教育、科研、出版领域的7个创新案例
  • Umi-OCR终极指南:免费离线OCR如何彻底改变你的数字工作流
  • 2026淄博卫生间免砸砖防水、外墙、地下室、楼顶渗漏+彩钢瓦、阳光房漏水 本地专业防水公司TOP5权威推荐(2026年6月本地最新深度调研) - 企业资讯
  • 网上购物|基于SprinBoot+vue的网上购物系统(源码+数据库+文档)