j.s异步编程4

async function fileSizes(files) {
  let lines = files.map(async fileName => {
    return fileName + ": " +
      (await textFile(fileName)).length;
  });
  return (await Promise.all(lines)).join("\n");
}

修正后的异步文件大小收集函数解析

这个改进版的 fileSizes 函数解决了之前的竞争条件问题,让我们分析它的工作原理:

代码功能与改进

async function fileSizes(files) {
  // 1. 创建Promise数组(不立即执行拼接)
  let lines = files.map(async fileName => {
    return fileName + ": " +
      (await textFile(fileName)).length; // 返回单个文件的结果
  });
  
  // 2. 等待所有Promise完成并收集结果
  return (await Promise.all(lines)).join("\n");
  // 等价于:
  // const results = await Promise.all(lines);
  // return results.join("\n");
}

核心改进

  1. 避免共享状态修改:不再直接操作共享的 list 变量
  2. 结果隔离:每个异步操作返回独立的字符串结果
  3. 统一合并:使用 Promise.all 等待所有结果完成后,再一次性拼接

执行流程详解

假设有三个文件:

const files = ["a.txt", "b.txt", "c.txt"];
  1. 生成Promise数组

    files.map(...)
    
    • 为每个文件创建一个异步任务
    • 每个任务返回一个 Promise,解析为对应文件的结果字符串
  2. 并行执行异步任务

    await Promise.all(lines)
    
    • 并行读取所有文件
    • 等待所有文件读取完成
    • 结果数组顺序与原始 files 数组一致
  3. 拼接最终结果

    (await Promise.all(lines)).join("\n")
    
    • 将结果数组按原始顺序拼接成字符串
    • 保证输出顺序与输入文件顺序一致

关键技术点

  1. Promise.all 的顺序保证

    • 即使异步任务完成时间不同
    • Promise.all 返回的结果数组顺序始终与输入 Promise 数组一致
  2. 避免竞态条件

    // 之前的错误写法(共享状态修改)
    list += ... // 非原子操作,多任务冲突
    
    // 现在的正确写法(独立结果)
    return ... // 每个任务返回独立值,无共享状态
    
  3. 异步映射与聚合

    files.map(async ...) // 映射为Promise数组
    Promise.all(...)     // 聚合所有结果
    results.join("\n")   // 按序拼接
    

乌鸦的异步智慧应用

在卡拉的LED项目中,类似的模式可以用于并行控制多个屏幕:

async function updateAllScreens(frames) {
  // 1. 为每个屏幕创建更新任务
  const updateTasks = screenAddresses.map((addr, index) => {
    return sendCommand(addr, "update", frames[index])
      .then(() => `${addr} 更新成功`)
      .catch(err => `${addr} 更新失败: ${err.message}`);
  });
  
  // 2. 等待所有任务完成并收集结果
  const results = await Promise.all(updateTasks);
  
  // 3. 生成汇总报告(顺序与屏幕数组一致)
  return results.join("\n");
}

这种设计确保了:

  1. 所有屏幕更新请求并行发送
  2. 无论响应顺序如何,结果都按屏幕原始顺序排列
  3. 避免了多个屏幕更新操作之间的数据竞争

就像乌鸦同时处理多个坚果一样,每个坚果都独立处理,最后按顺序整齐排列。 🐦✨

这类错误很容易犯,尤其是在使用 await 时,你应当留意代码中存在执行间隙的位置。JavaScript 显式的异步机制(无论是通过回调、Promise 还是 await)有一个优势:相对容易发现这些间隙的位置。

总结

异步编程使程序能够在等待长时间操作时不冻结整个系统成为可能。JavaScript 环境通常使用回调函数(即操作完成时被调用的函数)来实现这种编程风格。事件循环会在适当的时候调度这些回调函数依次执行,因此它们的执行不会重叠。

Promise(表示未来可能完成的操作的对象)和 async 函数(允许你像编写同步程序一样编写异步程序)使异步编程变得更加容易。

异步编程的陷阱与核心要点解析

这类错误很容易犯,尤其是在使用 await 时,你需要清楚代码中哪些地方存在执行间隙。JavaScript 显式的异步机制(无论是通过回调、Promise 还是 await)有一个优点:相对容易发现这些间隙的位置。

核心总结

1. 异步编程的本质

异步编程使得程序在等待耗时操作时不会冻结整个系统。JavaScript 环境通常通过回调函数实现这种编程模式——回调函数会在操作完成时被调用。事件循环(Event Loop)负责调度这些回调,确保它们按顺序执行,不会重叠。

2. 异步机制的演进

  • 回调函数(Callbacks):传统异步处理方式,但容易导致“回调地狱”(Callback Hell)。
  • Promise:用对象封装未来可能完成的操作,通过 .then().catch() 处理结果,避免回调嵌套。
  • async/await:以同步代码的写法实现异步逻辑,使异步流程更直观,可读性更高。

3. 关键优势与注意事项

  • 非阻塞特性:异步操作在等待时释放主线程,允许程序处理其他事件(如用户输入、UI 渲染)。
  • 显式间隙管理:通过 Promise 和 await,可以清晰识别代码中的异步间隙,避免共享状态导致的竞态条件(Race Conditions)。
  • 错误处理:Promise 的 .catch()try/catch 结合 await,能更优雅地捕获异步操作中的异常。

乌鸦的异步哲学

在卡拉的LED灯光系统中,异步编程是实现复杂动画的核心。例如,当她需要让九个屏幕依次显示不同的像素图案时:

async function animateCrowFlight() {
  for (const frame of flightFrames) {
    // 并行更新所有屏幕(非阻塞)
    await Promise.all(screenAddresses.map(addr => 
      displayFrameOnScreen(addr, frame[addr])
    ));
    
    // 等待动画间隔,期间可响应其他事件(如紧急停止信号)
    await wait(100); 
  }
}

这里的 await 并非阻塞主线程,而是让事件循环在等待时处理其他任务(如传感器输入)。这种机制让卡拉的程序既能实现精密的动画同步,又能保持对外部事件的实时响应——就像乌鸦在飞行中同时观察周围环境,随时调整方向。

异步编程的智慧,本质上是对“时间间隙”的掌控。正如JavaScript用事件循环调度回调,乌鸦用代码间隙处理突发情况,两者都在无序中创造出有序的节奏。 🐦💻✨

练习题:活动时间分析

卡拉实验室附近有一个由运动传感器激活的安全摄像头。它连接到网络,激活时会发送视频流。为避免被发现,卡拉设置了一个系统,该系统会检测这种无线网络流量,并在室外有活动时在她的巢穴中点亮一盏灯,以便她知道何时需要保持安静。

她还记录了摄像头被触发的时间,并希望利用这些信息来可视化显示平均一周中哪些时间段通常安静,哪些时间段通常繁忙。日志存储在文件中,每行包含一个时间戳数字(由 Date.now() 返回)。

1695709940692
1695701068331
1695701189163

camera_logs.txt 文件包含一个日志文件列表。编写一个异步函数 activityTable(day),它接收一周中的某一天作为参数,并返回一个包含24个数字的数组,数组中的每个数字表示该天对应小时内观察到的摄像头网络流量次数。天数使用 Date.getDay 方法的编号系统(星期日为0,星期六为6)。

沙箱环境提供的 activityGraph 函数可以将这样的数组汇总为一个字符串。

要读取文件,请使用前面定义的 textFile 函数——给定一个文件名,它返回一个解析为文件内容的 Promise。请记住,new Date(timestamp) 会创建该时间的 Date 对象,该对象具有返回星期几和小时的 getDaygetHours 方法。

两种类型的文件——日志文件列表和日志文件本身——都将每条数据单独放在一行,用换行符(\n)分隔。

async function activityTable(day) {
  let logFileList = await textFile("camera_logs.txt");
  // Your code here
}

activityTable(1)
  .then(table => console.log(activityGraph(table)));

异步活动时间表函数解析

这个 activityTable 函数的任务是统计特定星期几(如星期一)每小时的摄像头活动次数。让我们逐步解析如何实现它:

代码框架与核心逻辑

async function activityTable(day) {
  // 1. 读取日志文件列表
  let logFileList = await textFile("camera_logs.txt");
  
  // 2. 初始化24小时计数器数组
  const hourlyCounts = Array(24).fill(0);
  
  // 3. 解析日志文件列表并并行处理每个日志文件
  const logFiles = logFileList.trim().split('\n');
  await Promise.all(logFiles.map(async logFileName => {
    // 4. 读取单个日志文件内容
    const logContent = await textFile(logFileName);
    
    // 5. 解析每行时间戳并过滤指定星期几的记录
    logContent.trim().split('\n').forEach(timestamp => {
      const date = new Date(Number(timestamp));
      if (date.getDay() === day) {
        const hour = date.getHours();
        hourlyCounts[hour]++; // 增加对应小时的计数
      }
    });
  }));
  
  // 6. 返回统计结果
  return hourlyCounts;
}

关键步骤详解

  1. 读取日志文件列表

    let logFileList = await textFile("camera_logs.txt");
    
    • 调用 textFile 异步获取日志文件列表内容
    • 返回类似:"log1.txt\nlog2.txt\nlog3.txt"
  2. 初始化计数器数组

    const hourlyCounts = Array(24).fill(0);
    
    • 创建长度为24的数组,初始值全为0
    • 索引0-23对应00:00-23:00小时
  3. 解析并并行处理日志文件

    const logFiles = logFileList.trim().split('\n');
    await Promise.all(logFiles.map(...));
    
    • 将文件列表按行分割为数组
    • 使用 Promise.all 并行处理所有日志文件
  4. 处理单个日志文件

    const logContent = await textFile(logFileName);
    logContent.trim().split('\n').forEach(timestamp => { ... });
    
    • 读取日志文件内容(多行时间戳)
    • 逐行解析每个时间戳
  5. 过滤并统计特定星期的活动

    const date = new Date(Number(timestamp));
    if (date.getDay() === day) {
      const hour = date.getHours();
      hourlyCounts[hour]++;
    }
    
    • 将时间戳转换为 Date 对象
    • 检查是否为目标星期(getDay() === day
    • 提取小时信息并更新对应计数器

执行流程示例

假设 camera_logs.txt 包含:

monday.log
tuesday.log

执行流程

  1. 读取 camera_logs.txt 得到两个日志文件名
  2. 并行读取 monday.logtuesday.log
  3. 解析每个日志文件中的时间戳:
    1695709940692 → 2023-09-26 13:32:20 (星期二, 13点)
    1695701068331 → 2023-09-26 11:24:28 (星期二, 11点)
    1695701189163 → 2023-09-25 11:26:29 (星期一, 11点)
    
  4. 若查询 day=1(星期一):
    • 仅统计 1695701189163 这条记录
    • hourlyCounts[11] = 1,其他小时为0

优化与注意事项

  1. 错误处理

    • 应添加对 textFile 可能失败的处理
    • 示例:
      try {
        const logContent = await textFile(logFileName);
      } catch (err) {
        console.error(`读取日志文件 ${logFileName} 失败:`, err);
      }
      
  2. 内存优化

    • 对于极大的日志文件,可考虑流式处理而非一次性加载
  3. 性能考量

    • 并行处理所有日志文件可能导致资源耗尽
    • 可限制并发数:
      await Promise.all(chunkArray(logFiles, 5).map(async chunk => {
        await Promise.all(chunk.map(handleFile));
      }));
      

乌鸦的数据分析智慧

在卡拉的安全系统中,这种时间分析可以帮助她:

  1. 识别最安全的活动时段(如凌晨2-4点)
  2. 预测人类巡逻的高峰期
  3. 优化自己的作息以避开监控密集时段

例如,通过分析 activityTable(1)(星期一)的结果,她可能发现:

  • 11:00-13:00 活动频繁 → 对应人类午餐时间
  • 03:00-05:00 几乎无活动 → 理想的实验室工作时段

这种数据分析能力让她在人类世界的夹缝中建立起自己的数字王国,就像乌鸦利用城市垃圾桶的规律来规划觅食路线一样精准。 🐦💻✨

练习题:原生 Promise 实现

题目要求
不使用 async/await 语法,而是使用原生的 Promise 方法(如 .then().catch()Promise.all())重写之前的 activityTable 函数。

参考答案

function activityTable(day) {
  return textFile("camera_logs.txt")
    .then(logFileList => {
      const logFiles = logFileList.trim().split('\n');
      const hourlyCounts = Array(24).fill(0);
      
      return Promise.all(logFiles.map(logFileName => {
        return textFile(logFileName)
          .then(logContent => {
            logContent.trim().split('\n').forEach(timestamp => {
              const date = new Date(Number(timestamp));
              if (date.getDay() === day) {
                hourlyCounts[date.getHours()]++;
              }
            });
          })
          .catch(err => {
            console.error(`Error reading log file ${logFileName}:`, err);
          });
      }))
      .then(() => hourlyCounts);
    })
    .catch(err => {
      console.error("Error reading camera_logs.txt:", err);
      throw err; // 可选:向上传播错误
    });
}

关键点解析

  1. Promise 链式调用

    • 不再使用 await,而是通过 .then() 方法连接异步操作
    • 每个 .then() 接收前一个 Promise 的结果
  2. 并行处理

    • 使用 Promise.all() 并行读取所有日志文件
    • 每个日志文件的处理结果(hourlyCounts 的更新)会被合并
  3. 错误处理

    • 使用 .catch() 捕获读取文件时可能的错误
    • 可选:通过 throw err 将错误继续向上传播
  4. 状态管理

    • hourlyCounts 数组在闭包中被共享和修改
    • 所有日志文件处理完成后,最终返回该数组

与 async/await 的对比

特性 原生 Promise 实现 async/await 实现
代码结构 链式调用(扁平化但嵌套仍存在) 同步风格(更线性)
错误处理 需要多个 .catch() 或最终统一处理 单一的 try/catch
调试难度 调用栈可能更复杂 更接近同步代码的调试体验
状态共享 通过闭包隐式共享 显式变量声明
异步间隙可见性 更明显(通过 .then() 分隔) 隐藏在 await 之后

乌鸦的编程哲学

卡拉可能会选择原生 Promise 实现,因为它更贴近底层机制,让她能更精确地控制异步流程。在她的 LED 控制系统中,这种细粒度控制可能意味着:

  1. 并行与顺序的精确平衡

    // 先获取配置文件,再并行控制屏幕
    getConfig()
      .then(config => Promise.all(
        screens.map(screen => sendCommand(screen, config))
      ))
      .then(results => updateStatus(results));
    
  2. 优雅降级策略

    fetchData()
      .catch(() => useFallbackData())
      .then(data => process(data));
    
  3. 资源限流

    // 控制并发数,避免网络过载
    function processWithLimit(items, limit) {
      let index = 0;
      const next = () => index < items.length 
        ? process(items[index++]).then(next) 
        : Promise.resolve();
      return Promise.all(Array(limit).fill().map(next));
    }
    

就像乌鸦用不同的喙部动作处理不同类型的坚果一样,掌握多种异步编程范式能让开发者在不同场景下选择最合适的工具。 🐦🔧✨

实现Promise_all函数

如我们所见,给定一个Promise数组,Promise.all会返回一个新的Promise,它等待数组中的所有Promise完成。当所有Promise都成功时,新Promise以包含所有结果的数组成功解析;如果其中任何一个Promise失败,新Promise立即失败,并传递第一个失败Promise的原因。

现在需要你自己实现一个类似的函数,命名为Promise_all

注意:Promise一旦成功或失败,就不能再次改变状态,后续对其resolve/reject函数的调用会被忽略。这一特性可以简化我们对Promise失败的处理逻辑。

实现思路

核心逻辑

  1. 初始化状态

    • 记录已成功解析的Promise数量
    • 存储所有Promise的解析结果(按原始顺序)
    • 捕获第一个失败的Promise的原因
  2. 遍历处理每个Promise

    • 为每个Promise绑定成功和失败回调
    • 成功时:将结果存入对应位置,若所有Promise已成功,解析最终结果
    • 失败时:若尚未有失败记录,立即拒绝最终Promise
  3. 处理非Promise值

    • 若数组元素不是Promise,先将其转换为已解析的Promise

参考实现

function Promise_all(promises) {
  return new Promise((resolve, reject) => {
    const results = new Array(promises.length); // 按顺序存储结果
    let completed = 0; // 已成功解析的Promise数量

    // 处理数组中的每个元素(可能是非Promise)
    promises.forEach((promise, index) => {
      // 将非Promise值转换为已解析的Promise
      Promise.resolve(promise)
        .then(value => {
          results[index] = value; // 按索引存储结果
          completed++; // 成功计数+1

          // 所有Promise都成功时,解析最终结果
          if (completed === promises.length) {
            resolve(results);
          }
        })
        .catch(error => {
          // 第一个失败的Promise决定最终结果
          if (!rejectCalled) { // 确保只处理一次失败
            reject(error);
            rejectCalled = true; // 标记已失败
          }
        });
    });

    // 处理空数组的情况(所有Promise已完成)
    if (promises.length === 0) {
      resolve([]);
    }
  });
}

关键点解析

  1. 非Promise元素处理

    Promise.resolve(promise)
    
    • 确保数组中的每个元素都是Promise,统一用.then()处理
  2. 顺序性保证

    • 使用索引index将结果存入对应的位置
    • 最终结果数组的顺序与输入数组一致
  3. 失败处理优化

    • 通过rejectCalled标记确保只处理第一个失败的Promise
    • 后续的失败会被忽略,符合Promise的状态不可变特性
  4. 空数组处理

    • 若输入数组为空,直接解析空数组resolve([]),与原生Promise.all([])行为一致

测试用例

测试1:所有Promise成功

const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
Promise_all([p1, p2]).then(results => {
  console.log(results); // 输出: [1, 2]
});

测试2:包含非Promise值

Promise_all([1, Promise.resolve(2), 3]).then(results => {
  console.log(results); // 输出: [1, 2, 3]
});

测试3:Promise失败

const p1 = Promise.resolve(1);
const p2 = Promise.reject(new Error("Fail"));
Promise_all([p1, p2]).catch(error => {
  console.error(error.message); // 输出: "Fail"
});

测试4:空数组

Promise_all([]).then(results => {
  console.log(results); // 输出: []
});

乌鸦的算法智慧

在卡拉的机器人军团控制程序中,类似Promise_all的机制用于协调多个机器人的动作:

function coordinateRobots(robots) {
  return Promise_all(
    robots.map(robot => 
      robot.moveTo(targetPosition)
        .then(() => `Robot ${robot.id} arrived`)
        .catch(err => `Robot ${robot.id} failed: ${err}`)
    )
  );
}

这种实现确保:

  1. 所有机器人动作并行执行
  2. 任何一个机器人失败都会立即通知主控系统
  3. 最终结果按机器人原始顺序返回状态

就像乌鸦群协作时需要同步行动一样,Promise_all让异步操作在保持独立性的同时,能以整体视角处理成功与失败。这种“统一协调,快速响应”的机制,正是异步编程中处理复杂场景的核心智慧。 🐦🤖✨

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容