Lecture03-异步与 Promise(async - await)

文章目录

    [toc]

    单词词性音标含义
    Promisen./ˈprɒmɪs/表示"将来才给结果"的容器对象,用 then 或 await 等它兑现
    asyncadj./əˈsɪŋk/异步关键词,修饰函数后该函数总是返回一个 Promise
    awaitv./əˈweɪt/等待右边的 Promise 完成再执行下一步,把异步写得像同步
    thenv./ðen/Promise 的方法,用来接收内部 resolve 交出来的结果
    resolvev./rɪˈzɒlv/Promise 内部的方法,负责把任务结果"交出去"
    setIntervalv./set ˈɪntəvl/定时器,每隔固定毫秒执行一次回调,本节用它体验异步并行
    try/catchn./traɪ kætʃ/异常捕获结构,配合 await 处理异步任务的失败

    Lecture03-异步与 Promise(async - await)

    1、本节讲什么

    搞懂 JS 异步的核心工具 Promise,以及 async / await 三个关键词各自干什么、怎么配合用。全程用 node 运行 .js 文件,不涉及网络请求(真正的请求后端,将来第 23 节直接用 axios)。

    2、概念速览

    什么是异步任务:有些操作要"等别人"(等网络、等定时器),等的时候不能把程序卡死。异步 = 我先去干别的,结果好了再通知我。

    什么是 Promise:一个"将来才给结果"的容器。你拿到的不是结果本身,而是"会出结果的凭证",用 then 或 await 等它兑现。

    类比 Java:Promise 类似 CompletableFuture——都是"异步结果"的封装。

    正确 vs 误区:异步不是"更快",而是"不用等";不 await 就想拿结果,拿到的是"还没完成的 Promise"。

    3、先看效果

    Promise 能"把要异步执行的任务装进去",任务做完能把结果交出来。建 promise-then.js,node promise-then.js:

    const promise = new Promise((resolve) => {
        const result = 10 + 5;        // Promise 内的任务:算个加法
        resolve(result);              // 把结果"交出去"
    });
    promise.then((taskResult) => {
        console.log("内部打印:taskResult = " + taskResult);   // 15
    });
    

    终端打印 内部打印:taskResult = 15——这就是"Promise 里的结果,能在外面接到"。

    4、照着做(4 个文件,一步步来)

    ① 体验异步:Promise 内外并行执行

    Promise 最基本的作用:把一段代码放进去,让它和外面并行执行(谁也不等谁)。建 promise-async.js,node promise-async.js:

    // 初步体验 Promise 封装异步操作
    // 异步:两边谁都不用等谁,同时并行、交错执行
    // 同步:A 操作需要等 B 操作执行完,才能执行自己的操作
    // 1、在 Promise 外面(主线程)每隔 1 秒打印一句话
    setInterval(() => console.log("●主线程~~~" + Math.random() * 100), 1000);
    
    // 2、在 Promise 里面(子线程)每隔 1 秒打印一句话
    new Promise(() => {
        setInterval(() => console.log("○子线程~~~" + Math.random() * 100), 1000);
    });
    

    你会看到 ● 和 ○ 两行交错出现、互不等——这就是"异步":两边同时进行。

    ② Promise 把结果返回给调用者(then)

    光并行不够,我们还要能拿到 Promise 里算出来的结果。resolve(结果) 负责"把结果交出去",then 负责"在外面接住"。建 promise-then.js,node promise-then.js:

    // 总体说明:Promise 执行任务之后,会有一个最终的结果,可以返回给调用者
    // 设定:Promise 要执行的任务就是 10 和 5 相加
    let taskResultOutter = 0;
    
    const promise = new Promise((resolve) => {
        // 1、Promise 内执行数据计算,得到结果
        const result = 10 + 5;
    
        // 2、把结果返回给外部调用者
        resolve(result);
    });
    
    // 提问:为啥要舍近求远,在 then() 里面接收任务结果?
    // 回答:因为我们需要模拟将来使用框架的场景,Promise 内部的代码在框架内部我们摸不到,但是可以获取到 Promise 对象
    // 模拟:Promise 里面的任务将来就是 Axios 给后端发送请求;任务结果就是后端返回的响应
    promise.then((taskResult) => {
        console.log("内部打印:taskResult = " + taskResult);
        taskResultOutter = taskResult;
    });
    
    // 外部打印 taskResultOutter 是在子线程赋值操作之前执行的
    // 原因是外部主线程和内部子线程是异步执行的,外部不会等内部完成赋值
    // 所以从效果上,我们看到外部变量仍然是初始值,没有被赋值
    console.log("外部打印:taskResultOutter = " + taskResultOutter);
    

    两个要点:

    • 结果靠 then 接收,不靠外部变量——所以下面外部变量 taskResultOutter 打印出来还是 0(异步执行,外部不等人)。
    • 为什么"舍近求远"用 then 接? 因为将来用框架时,Promise 内部的代码我们摸不到,只能拿到 Promise 对象(比如 axios 发请求,任务结果就是后端响应),所以必须通过 then 拿。

    ③ await:把异步变成同步

    then 接结果还是有点绕,await 更直接:等右边 Promise 做完,再执行下一步——把异步"翻译"成同步的样子,逻辑更顺、代码更优雅。建 promise-await.js,node promise-await.js:

    // 在 Promise 中封装子任务
    // await 关键词的作用:等右边的 Promise 对象把子任务执行完,然后再执行下一步操作
    // 效果:把异步任务转换为了同步任务
    // 好处1:逻辑方面更加顺畅
    // 好处2:代码变得更加优雅
    const taskResult = await new Promise((resolve) => {
        setTimeout(() => {
            console.log("出门之前先化个妆~~~[子任务]");
            resolve("任务执行的结果~~~[子任务]");
        }, 3000);
    });
    
    // 获取到任务执行的结果之后,打印出来
    console.log("[外部主线程]taskResult = " + taskResult);
    

    你会看到:先打印"化妆"(3 秒后),再打印结果——await 让外部停下来等子任务完成。这段 await 写在模块顶层(Vite 项目的 .js 默认就是模块,顶层可以用 await);如果 await 写在函数内部,这个函数必须 async——看下一步。

    ④ 函数内用 await,函数必须 async

    规则一句话:一个函数内部用了 await,函数本身就必须用 async 修饰。建 promise-async.js,node promise-async.js:

    // 一个函数内部使用了 await 关键词,那么函数本身就必须使用 async 修饰
    async function love() {
        const value = await new Promise((resolve) => {
            resolve("task result ...");
        });
        console.log("value = " + value);
    }
    
    love();
    

    把 async 去掉试试——node 直接报语法错误。为什么必须,见下面的补充。

    补充:为什么 await 必须配 async?

    image-20260929150345901

    一句话:await 是"函数内部的暂停点",async 是"给整个函数装上可等待外壳"——暂停点要成立,前提是外壳先存在。

    1. await = 函数在这里"等一个将来的结果",等于让函数在某处暂停。

    2. 一暂停就引出两个问题:

      1. 函数结果要等一会儿才出来;
      2. 调用方必须知道要等,否则会把"没完成的 Promise"当"完成的值"用。
    3. async 一次性解决:声明后这个函数总是返回一个 Promise,调用方就该 await / .then 拿结果、try/catch / .catch 接错误。

    普通函数async 函数
    调用直接返回"值"调用返回 Promise(将来完成信号)
    内部不能 await(写了就报错)内部能 await
    调用方直接拿结果调用方必须 await / .then
    错误靠外层接错误靠 try/catch / .catch 接

    比喻:await = 车在某站停下来等人;async = 这辆车整体被登记成"异步班车、到终点会通知你"。你要停下来等人,前提是车本身是异步班车且有完成信号;普通车停下来等人,没人登记、没人知道、没处通知,所以 JS 直接不让写。

    一句话记法:await 是"我停下来等别人",async 是"我这个函数整体也是个要等的异步任务、并对外发完成信号"——后者是前者的容器。

    5、常见疑问

    • Q:await 到底在等什么? A:等一个 Promise 有结果。类似 Java 里 CompletableFuture.get()。await 只能写在 async 函数里(或模块顶层)。
    • Q:什么时候用 .then,什么时候用 await? A:都能用,但 await 更像同步代码、更好读,本教程后面一律用 await。.then 你能看懂就行。
    • Q:Promise 里的结果,外部变量为什么拿不到(还是 0)? A:因为异步:外部主线程不会等内部子线程赋值。要拿结果就用 then 接收或 await,别靠外部变量。
    • Q:.catch 和 try/catch 区别? A:.catch 是 Promise 自带的错误处理;await 写法配合 try/catch 捕获错误。推荐 await + try/catch:
    async function run() {
      try {
        const msg = await wait
        console.log(msg)
      } catch (err) {
        console.log('出错了:', err)   // 失败一定处理,别静默
      }
    }
    

    6、规范要点

    • 异步结果用 then / await 接收,不要靠外部变量(靠外部变量接,拿到的还是初始值)。
    • 异步逻辑一定要处理失败(try/catch 或 .catch),别静默失败难排查。
    • 将来写异步请求时,记住"先 await 拿结果,再 console.log 确认数据长什么样,再上页面"。

    7、本节验证

    • 能用 new Promise((resolve) => { ... }) 封装一段异步任务并 resolve 结果 ✅
    • 能用 .then() 或 await 接到 Promise 里的结果,知道不能靠外部变量接 ✅
    • 明白 await 只能写在 async 函数里(或模块顶层),去掉 async 会报语法错误 ✅
    • 能说清 await 与 async 的分工:一个是"暂停等待点"、一个是"可等待外壳" ✅
    • 知道异步失败要用 try/catch 或 .catch 处理,别静默失败 ✅
    end