第一篇:别再被 “异步” 搞懵了!用奶茶店逻辑学会 JS 异步编程,从此告别回调地狱

内容分享2个月前发布
1 0 0

第一篇:别再被 “异步” 搞懵了!用奶茶店逻辑学会 JS 异步编程,从此告别回调地狱

你有没有过这样的经历?写 JS 代码时,明明顺序排得整整齐齐,结果运行起来却 “不按常理出牌”—— 比如刚请求完数据,想立刻用却发现是 undefined,或者循环里的定时器全挤在最后一起执行。别慌,这不是你代码写错了,而是没搞懂 JS 里最核心的 “异步逻辑”。今天咱们就用奶茶店的日常操作当例子,把异步编程讲得明明白白,再教你怎么用 Promise 和 async/await 彻底摆脱回调地狱。
第一篇:别再被 “异步” 搞懵了!用奶茶店逻辑学会 JS 异步编程,从此告别回调地狱

一、先搞懂:为什么 JS 会有 “异步” 这种东西?

首先得明确一个关键点:JS 是 “单线程” 语言。啥意思?就好比一家奶茶店只有一个做奶茶的师傅,同一时间只能做一杯奶茶。如果师傅非要做完一杯再接下一杯,那后面排队的人早就等疯了 —— 这就是 “同步” 的问题。

为了解决这个问题,奶茶店会搞个 “取餐号” 系统:你点单后拿个号,师傅先做你的奶茶,但在等奶茶煮好的间隙,他可以先接下一个人的订单。等你的奶茶做好了,广播喊号你再来取 —— 这就是 “异步” 的逻辑。JS 里的异步操作,比如定时器、网络请求、读取文件,本质上都是 “师傅在等食材熟的间隙,先处理其他活”。

举个最简单的例子:


console.log("1. 点一杯珍珠奶茶");

setTimeout(() => {

  console.log("2. 奶茶做好了,取餐");

}, 2000);

console.log("3. 拿完取餐号,去玩手机");

运行结果会是 1→3→2,而不是 1→2→3。因为 setTimeout 就像 “等奶茶煮好”,JS 会先把这个任务放到 “异步队列” 里,继续执行后面的 “拿号玩手机”,等主线程的活干完了,再回头执行异步队列里的 “取奶茶”。

这里要记住两个核心概念:

调用栈:JS 执行代码的地方,就像师傅正在做的那杯奶茶,同一时间只有一个任务。

任务队列:异步任务排队的地方,就像取餐号的列表,等调用栈空了才会按顺序执行。

二、从 “回调函数” 到 “Promise”:异步代码的进化史

早期的 JS 异步编程全靠 “回调函数”,比如用 AJAX 请求数据:


// 早期AJAX代码,回调嵌套噩梦的开始
$.get("/user", function(userData) {
  $.get("/user/" + userData.id + "/orders", function(ordersData) {
    $.get("/orders/" + ordersData[0].id + "/details", function(detailData) {
      console.log("终于拿到订单详情了", detailData);
    }, function(err) {
      console.log("订单详情请求失败", err);
    });
  }, function(err) {
    console.log("订单列表请求失败", err);
  });
}, function(err) {
  console.log("用户信息请求失败", err);
});

这段代码看起来就像 “俄罗斯套娃”,如果再嵌套几层,代码缩进能歪到屏幕外面 —— 这就是传说中的 “回调地狱”。不仅写起来费劲,调试的时候找错也得一层一层扒,简直是开发者的噩梦。

为了解决这个问题,ES6 推出了 “Promise”,把嵌套的回调改成了 “链式调用”,就像奶茶店点单时 “加珍珠→加奶盖→少糖” 这样一步一步选,逻辑清晰多了。

咱们用 Promise 重写上面的 AJAX 请求:


// 先把AJAX封装成返回Promise的函数
function getRequest(url) {
  return new Promise((resolve, reject) => {
    $.get(url, function(data) {
      resolve(data); // 请求成功,把数据传出去
    }, function(err) {
      reject(err); // 请求失败,把错误传出去
    });
  });
}

// 链式调用,再也没有嵌套
getRequest("/user")
  .then(userData => {
    return getRequest("/user/" + userData.id + "/orders"); // 返回下一个Promise
  })
  .then(ordersData => {
    return getRequest("/orders/" + ordersData[0].id + "/details");
  })
  .then(detailData => {
    console.log("拿到订单详情了", detailData);
  })
  .catch(err => {
    console.log("某个步骤失败了", err); // 所有错误在这里统一处理
  });

是不是清爽多了?Promise 就像一个 “承诺”:我现在去做异步任务,成功了就告诉你结果(resolve),失败了也告诉你原因(reject)。然后用.then () 处理成功的情况,用.catch () 处理所有失败的情况,不用再每个回调里都写错误处理了。

这里要注意 Promise 的三个状态:

pending(等待中):刚点完单,奶茶还在做。

fulfilled(已成功):奶茶做好了,调用 resolve。

rejected(已失败):原料没了做不了,调用 reject。

一旦状态从 pending 变成另外两种,就再也改不了了 —— 就像奶茶做好了不能再变回原料,没原料也不能突然变出奶茶。

三、终极方案:用 async/await 写 “同步风格” 的异步代码

虽然 Promise 解决了回调地狱,但链式调用写多了,还是得写一堆.then (),不够直观。ES2017 推出的 async/await,直接把异步代码写成了同步的样子,堪称 “异步编程的语法糖天花板”。

还是刚才的请求案例,用 async/await 改写:


// 封装getRequest的代码不变
function getRequest(url) {
  return new Promise((resolve, reject) => {
    $.get(url, resolve, reject); // 简化写法,$.get的成功回调直接传resolve
  });
}

// 关键:用async声明函数,里面可以用await
async function getOrderDetail() {
  try {
    const userData = await getRequest("/user"); // 等这个Promise成功,再继续
    const ordersData = await getRequest("/user/" + userData.id + "/orders");
    const detailData = await getRequest("/orders/" + ordersData[0].id + "/details");
    console.log("拿到订单详情了", detailData);
  } catch (err) {
    console.log("某个步骤失败了", err); // 所有错误用try/catch捕获
  }
}

getOrderDetail();

这段代码看起来就像 “先拿用户数据→再拿订单列表→最后拿订单详情” 的同步逻辑,但实际上每一步都是异步的,不会阻塞主线程。这就是 async/await 的魔力:用同步的写法,实现异步的功能。

不过用的时候要记住两个规则:

await 只能在 async 函数里用:就像只有在奶茶店(async 函数)里才能用取餐号(await),你在大街上用人家不认。

错误要用 try/catch 处理:Promise 的 reject 会变成 await 的错误,必须用 try/catch 捕获,不然会报未处理的错误。

四、实战避坑:异步编程里那些 “反常识” 的坑

学会了基础用法,咱们再聊聊实际开发中容易踩的坑,帮你少走弯路。

坑 1:forEach 里用 await,结果 “并行执行” 变 “串行执行”?

比如你想遍历一个数组,每个元素都发一个异步请求:


async function fetchAllData(arr) {
  arr.forEach(async item => {
    const data = await getRequest(`/data/${item}`);
    console.log(data);
  });
  console.log("所有请求完成");
}

fetchAllData([1,2,3]);

你以为会是 “请求 1→完成 1→请求 2→完成 2→请求 3→完成 3→所有请求完成”,但实际结果是 “同时发 3 个请求→随机顺序打印 data→先打印‘所有请求完成’”。

为什么?因为 forEach 会立刻执行所有回调函数,每个 async 回调里的 await 都是独立的,不会阻塞 forEach 的循环。就像师傅同时接了 3 个订单,三个奶茶一起做,谁先好谁先出。

如果想让请求 “串行执行”(一个做完再做下一个),应该用 for…of 循环:


async function fetchAllData(arr) {
  for (const item of arr) {
    const data = await getRequest(`/data/${item}`);
    console.log(data);
  }
  console.log("所有请求完成"); // 现在会等所有请求做完再打印
}

如果想 “并行执行”(同时做,效率更高),但要等所有请求都完成再处理,用 Promise.all ():


async function fetchAllData(arr) {
  // 先把所有请求变成Promise数组
  const promiseArr = arr.map(item => getRequest(`/data/${item}`));
  // 等所有Promise都成功,才会返回结果数组
  const allData = await Promise.all(promiseArr);
  console.log(allData); // 按数组顺序返回结果,不管谁先完成
  console.log("所有请求完成");
}

坑 2:Promise.resolve () 和 new Promise (resolve) 的区别?

有时候会看到两种写法:


// 写法1

const p1 = Promise.resolve("hello");

// 写法2

const p2 = new Promise(resolve => resolve("hello"));


这两种写法看起来一样,结果也一样,但在处理 “thenable 对象”(有 then 方法的对象)时会有区别。比如:



```bash
const thenable = {
  then(resolve) {
    setTimeout(() => resolve("延迟1秒"), 1000);
  }
};

// 写法1:会等待thenable的then方法执行完
Promise.resolve(thenable).then(data => console.log(data)); // 1秒后打印

// 写法2:不会等待,直接把thenable作为结果返回
new Promise(resolve => resolve(thenable)).then(data => console.log(data)); // 立刻打印thenable对象

简单说,Promise.resolve () 会 “穿透” thenable 对象,等待它的异步操作完成;而 new Promise (resolve) 只会直接把 thenable 当成普通值返回。日常开发中,用 Promise.resolve () 更安全,除非有特殊需求。

坑 3:async 函数永远返回 Promise,哪怕你 return 一个普通值

比如:


async function add(a, b) {
  return a + b; // 看似返回数字
}

const result = add(1, 2);
console.log(result); // 输出Promise { 3 },而不是3

所以如果想拿到 async 函数的返回值,必须用 await 或者.then ():


add(1,2).then(res => console.log(res)); // 3

// 或者在另一个async函数里用await
async function getResult() {
  const res = await add(1,2);
  console.log(res); // 3
}

五、总结:异步编程的 “学习路径”

看到这里,你应该对 JS 异步编程有了清晰的认识。最后给你总结一个学习路径,帮你巩固知识:

理解基础:先搞懂单线程、调用栈、任务队列的概念,知道异步任务为什么会 “插队”。

掌握 Promise:熟悉 Promise 的三种状态、resolve/reject 的用法,以及.then ()/.catch () 的链式调用。

用好 async/await:在 Promise 的基础上,学会用 async/await 简化代码,记住 try/catch 处理错误。

实战避坑:搞清楚 forEach 和 for…of 的区别、Promise.all () 的用法、async 函数的返回值特性,避免踩常见的坑。

其实异步编程并不难,关键是把 “奶茶店逻辑” 和代码对应起来,多写多练。下次再遇到异步相关的 bug,先想想 “师傅现在在做什么?哪些任务在排队?”,思路就会清晰很多。

第二篇:JS 数组方法从入门到精通:10 个常用方法让你少写 80% 循环,菜鸟也能变大神

数组是 JS 里最常用的数据结构,没有之一。不管是处理列表数据、渲染页面,还是做数据筛选,都离不开数组。但很多新手还在用 for 循环一遍一遍遍历数组,代码又长又容易出错。其实 JS 数组自带了几十种方法,用好这些方法能让你的代码简洁又高效,还能少写很多 bug。今天咱们就挑 10 个最常用的数组方法,从基础用法到实战技巧,用大白话讲明白,让你看完就能上手用。

一、先搞懂:数组方法的 “共性” 和 “区别”

在学具体方法之前,先明确几个重要概念,帮你避免混淆:

是否改变原数组

「改变原数组」的方法( mutable ):比如 push、pop、splice,调用后原数组会被修改。

「不改变原数组」的方法( immutable ):比如 map、filter、slice,调用后会返回一个新数组,原数组不变。

开发中尽量多用不改变原数组的方法,避免不小心修改了原始数据,导致后续 bug。

是否有返回值

有的方法返回新数组(如 map、filter),有的返回新元素(如 pop、shift),有的返回布尔值(如 every、some),有的没有返回值(如 forEach)。

别以为 “没返回值” 就没用,比如 forEach 就是专门用来遍历数组执行操作的。

回调函数的参数

大部分数组方法(如 map、filter、forEach)都需要传一个回调函数,回调函数通常有三个参数:


[1,2,3].forEach((item, index, arr) => {
  console.log("当前元素:", item);
  console.log("当前索引:", index);
  console.log("原数组:", arr);
});

currentValue:当前正在处理的数组元素(必填)。

index:当前元素的索引(可选)。

array:调用方法的原数组(可选)。

比如 forEach 的回调:

接下来,咱们逐个讲解 10 个常用方法,每个方法都包含 “基础用法 + 实战场景 + 注意事项”,确保你学完就能用。

二、10 个常用数组方法详解(附实战案例)

1. forEach:遍历数组的 “万能工具”

作用:遍历数组,对每个元素执行回调函数,没有返回值(返回 undefined)。

是否改变原数组:不改变,但可以在回调里手动修改原数组(不推荐)。

基础用法


const fruits = ["苹果", "香蕉", "橙子"];

// 遍历数组,打印每个水果
fruits.forEach(fruit => {
  console.log(fruit); // 依次输出:苹果、香蕉、橙子
});

// 带索引的写法
fruits.forEach((fruit, index) => {
  console.log(`第${index+1}个水果:${fruit}`); // 第1个水果:苹果...
});

实战场景:渲染列表数据到页面。

比如后端返回一个商品列表,需要渲染到页面的 ul 里:


const products = [
  { id: 1, name: "手机", price: 3999 },
  { id: 2, name: "电脑", price: 5999 },
  { id: 3, name: "平板", price: 2999 }
];

const ul = document.querySelector("#productList");

// 遍历商品数组,创建li元素并添加到ul
products.forEach(product => {
  const li = document.createElement("li");
  li.innerHTML = `
    <span>商品名:${product.name}</span>
    <span>价格:${product.price}元</span>
  `;
  ul.appendChild(li);
});

注意事项

forEach 不能中断循环(比如用 break 或 return 没用),如果需要中断循环,应该用 for 循环或 some 方法。

空数组不会执行回调函数,所以不用担心遍历到 undefined。

© 版权声明

相关文章

暂无评论

none
暂无评论...