map
map()是 JavaScript 中数组的核心高阶函数,核心作用是:遍历数组的每一个元素,对每个元素执行指定的处理逻辑,最后返回一个「长度与原数组相同」的新数组(新数组的元素是原数组元素处理后的结果)。
简单说:map() 是 "数组转换器"------ 不改变原数组,只返回加工后的新数组。
一、基本语法
const 新数组 = 原数组.map((当前元素, 索引, 原数组) => {
// 对当前元素的处理逻辑
return 处理后的新值; // 必须有return,否则新数组元素为undefined
});
当前元素:遍历到的数组单个元素(必选);索引:当前元素的下标(可选);原数组:调用 map 的原数组本身(可选)。
二、核心特点
- 不修改原数组:纯函数特性,原数组保持不变;
- 返回新数组:新数组长度和原数组完全一致;
- 必须 return :如果没有 return,新数组的对应位置为
undefined; - 遍历所有元素 :不会跳过任何元素(包括
undefined/null)。
三、实用场景
场景 1:格式化接口数据(代码里的核心用法)
代码中把接口返回的原始数据,转换成模板需要的标准化格式,是map()最常用的场景:
// 接口返回的原始数据
const realData = [
{ id: 1, title: "原标题", author_name: "张三", create_time: 1735689600000 },
{ id: 2, title: "原标题2", author_name: "李四", create_time: 1735776000000 }
];
// 用map转换成模板需要的格式
const standardData = realData.map((item) => ({
id: item.id, // 保留原ID
title: item.title || "暂无标题", // 处理空值
author: item.author_name || "未知作者", // 字段重命名
uploadTime: item.create_time, // 字段映射
coverUrl: item.cover_path || "默认图片地址" // 兜底处理
}));
最终standardData是新数组,每个元素都是加工后的格式,原realData不变。
场景 2:渲染列表前预处理数据
比如给朋友圈多图数据做兜底、限制数量:
// 给coverUrls数组做兜底,确保是数组且最多9张
const processedData = resultList.map((item) => ({
...item, // 复制原所有属性
coverUrls: Array.isArray(item.coverUrls) ? item.coverUrls.slice(0,9) : [] // 处理非数组/超9张
}));
场景 3:简单数据转换
// 数字数组转字符串数组
const numArr = [1, 2, 3];
const strArr = numArr.map(num => num.toString()); // ["1", "2", "3"]
// 数组元素翻倍
const doubleArr = numArr.map(num => num * 2); // [2, 4, 6]
四、常见误区
-
混淆 map 和 forEach :
map()有返回值(新数组),适合 "转换数据";forEach()无返回值(返回 undefined),仅适合 "遍历执行操作"(如打印、修改 DOM);
-
忘记 return :
const arr = [1,2,3].map(item => { item * 2 }); // [undefined, undefined, undefined]正确写法:
map(item => item * 2)(简写自动 return)或map(item => { return item * 2 }); -
试图修改原数组 :
map()设计为纯函数,即使在回调里修改原元素(如对象属性),也不推荐 ------ 如需修改,建议先复制元素再处理。
可选链操作符?
?.是 JavaScript/TypeScript 中的可选链操作符(Optional Chaining Operator) ,核心作用是:安全地访问嵌套对象的属性 / 方法,避免因中间某一层为undefined/null而抛出Cannot read properties of undefined错误。
一、核心问题:为什么需要 ?.?
假设你要获取文章对象的封面图地址,常规写法会有风险:
// 原始对象(可能缺失 coverUrls 属性)
const article = {
id: 1,
title: "测试文章",
// coverUrls: ["https://xxx.jpg"] // 可能不存在
};
// 常规写法:如果 coverUrls 不存在,会直接报错
const firstCover = article.coverUrls[0];
// ❌ Uncaught TypeError: Cannot read properties of undefined (reading '0')
而用 ?. 可以避免这个错误:
// 可选链写法:coverUrls 不存在时,直接返回 undefined,不报错
const firstCover = article.coverUrls?.[0];
// ✅ 返回 undefined(无报错)
二、?. 的核心语法规则
| 用法 | 含义 |
|---|---|
obj?.prop |
访问对象的属性:如果 obj 是 null/undefined,返回 undefined |
obj?.[prop] |
访问对象的动态属性(比如数组下标、变量名) |
func?.() |
调用函数:如果 func 不是函数(null/undefined),返回 undefined |
obj?.prop?.subProp |
嵌套访问:任意一层为 null/undefined,直接返回 undefined |
三、注意事项(避坑点)
-
?.只判断「当前层级是否为 null/undefined」,不判断空字符串、0、false 等:const obj = { emptyStr: "" }; console.log(obj.emptyStr?.length); // ✅ 返回 0(空字符串不是 null/undefined) -
?.不能用于赋值操作(只能用于 "读取",不能用于 "写入"):// ❌ 错误:可选链不能赋值 article.author?.avatarUrl = "新头像地址"; // ✅ 正确:先判断,再赋值 if (article.author) { article.author.avatarUrl = "新头像地址"; } -
结合空值合并运算符
??更精准(区别于||):-
||:会把 0、空字符串、false 都当成 "假值"; -
??:只把null/undefined当成 "假值"(更适合兜底)。const count = article.readCount ?? 0; // 只有 readCount 是 null/undefined 时,才返回 0
const title = article.title ?? "暂无标题"; // 更精准的兜底
-
forEach
forEach是 JavaScript 数组的核心遍历方法,核心作用是:遍历数组的每一个元素,并对每个元素执行指定的回调函数(无返回值,仅用于 "执行操作" 而非 "转换数据")。
简单说:forEach 是 "数组遍历器"------ 只做事(比如打印、修改 DOM、累加数据),不返回新数组,也不改变原数组(除非手动在回调里修改)。
一、基本语法
数组.forEach((当前元素, 索引, 原数组) => {
// 对当前元素的操作逻辑(无强制 return 要求)
});
当前元素:遍历到的数组单个元素(必选);索引:当前元素的下标(可选,从 0 开始);原数组:调用forEach的原数组本身(可选);- 无返回值:
forEach执行后始终返回undefined。
二、核心特点
- 无返回值 :区别于
map(返回新数组),forEach只执行操作,不产出新数组; - 遍历所有元素 :不会跳过
undefined/null,也无法通过return中断遍历(break也不行); - 不修改原数组(默认):纯遍历,除非手动在回调里修改原数组元素(如对象属性);
- 兼容性好:支持所有现代浏览器,包括 ES5 环境。
三、实用场景(结合你的项目举例)
场景 1:遍历数组执行 "副作用操作"(打印、修改 DOM、累加)
// 你的项目中:遍历文章列表,打印每篇文章的标题(调试用)
const articleList = [
{ id: 1, title: "朋友圈多图测试", author: "张三" },
{ id: 2, title: "单图测试", author: "李四" }
];
// 1. 基础遍历:打印标题
articleList.forEach((item) => {
console.log("文章标题:", item.title);
});
// 输出:
// 文章标题:朋友圈多图测试
// 文章标题:单图测试
// 2. 带索引遍历:打印"索引+标题"
articleList.forEach((item, index) => {
console.log(`第${index+1}篇:${item.title}`);
});
// 输出:
// 第1篇:朋友圈多图测试
// 第2篇:单图测试
// 3. 累加数据:统计所有文章的阅读量
let totalReadCount = 0;
articleList.forEach((item) => {
totalReadCount += item.readCount || 0; // 兜底空值
});
console.log("总阅读量:", totalReadCount);
场景 2:前端渲染前预处理(批量修改对象属性)
// 你的项目中:给所有文章的封面图加默认值(手动修改原数组)
articleList.forEach((item) => {
// 如果没有封面图,赋值默认图
if (!item.coverUrl && !item.coverUrls?.[0]) {
item.coverUrl = "https://picsum.photos/400/300?placeholder";
}
});
// 处理后,articleList 中的每个对象都有了 coverUrl 兜底值
场景 3:绑定事件 / 操作 DOM(前端页面交互)
// 假设前端有多个"点赞按钮",遍历绑定点击事件
const likeButtons = document.querySelectorAll(".like-btn");
// 转换为数组后遍历(NodeList 也支持 forEach)
Array.from(likeButtons).forEach((btn, index) => {
btn.addEventListener("click", () => {
alert(`给第${index+1}篇文章点赞`);
});
});
watch
watch是 Vue 3(组合式 API)中核心的响应式监听工具 ,核心作用是:监听一个或多个响应式数据的变化,当数据改变时自动执行指定的回调函数。
结合 Vue 项目(搜索结果、朋友圈展示),下面从「核心用法、语法、场景、高级特性」全维度解析,贴合你的实际开发场景。
一、为什么需要 watch?
Vue 中响应式数据(ref/reactive)的变化默认只会触发模板重新渲染,但如果需要在数据变化后执行额外逻辑 (比如重新请求接口、更新其他数据、执行异步操作),就需要用 watch 监听。
比如你项目中:
- 搜索关键词变化 → 重新加载搜索结果;
- 排序类型变化 → 重新请求排序后的列表;这些都是
watch的典型应用场景。
二、基本语法(Vue 3 组合式 API)
import { ref, watch } from 'vue';
// 1. 监听单个 ref 数据
const keyword = ref('');
const stopWatch = watch(
keyword, // 要监听的响应式数据
(newVal, oldVal) => { // 数据变化后的回调
console.log('关键词从', oldVal, '变成了', newVal);
// 执行逻辑:比如重新请求接口
},
{ // 可选配置项
immediate: false, // 是否立即执行一次(默认false)
deep: false, // 是否深度监听(默认false)
flush: 'pre' // 回调执行时机(pre/post/sync)
}
);
// 2. 停止监听(可选)
// stopWatch(); // 调用返回值即可停止监听
三、核心参数解析
| 参数 / 配置 | 含义 |
|---|---|
| 第一个参数(源) | 要监听的响应式数据:✅ ref 变量(如 keyword)✅ reactive 对象的属性(如 () => obj.name)✅ 数组(监听多个数据) |
| 第二个参数(回调) | (newVal, oldVal) => {}:- newVal:数据变化后的新值- oldVal:数据变化前的旧值(基本类型可直接获取,引用类型需注意) |
immediate |
布尔值,默认 false。设为 true 时,监听创建时立即执行一次回调(比如页面初始化就执行) |
deep |
布尔值,默认 false。设为 true 时,深度监听引用类型(对象 / 数组)的内部属性变化 |
四、常见用法(结合你的项目场景)
场景 1:监听单个 ref 数据(你的项目核心用法)
对应你代码中 "搜索关键词变化重新加载数据":
import { ref, watch, onMounted } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
const currentKeyword = ref('');
const resultList = ref([]);
// 监听路由中的关键词变化 → 重新加载搜索结果
watch(
() => route.query.keyword, // 监听路由参数(非直接ref,用函数返回)
(newKeyword) => {
currentKeyword.value = newKeyword || '';
resultList.value = []; // 清空旧数据
fetchSearchData(); // 重新请求接口
},
{ immediate: true } // 初始化时立即执行(页面加载就触发)
);
// 接口请求函数
const fetchSearchData = async () => {
// 你的请求逻辑...
};
关键说明:
route.query.keyword不是直接的ref变量,所以用「函数返回值」的方式监听;immediate: true:页面初始化时就执行一次回调,保证首次加载能拿到关键词并请求数据。
场景 2:监听多个数据(比如排序 + 类型同时变化)
对应你项目中 "排序类型 / 内容类型变化重新加载":
const sortType = ref('hot');
const contentType = ref('video');
// 监听多个数据:数组形式
watch(
[sortType, contentType], // 监听排序+内容类型
([newSort, newType], [oldSort, oldType]) => {
console.log('排序从', oldSort, '变', newSort);
console.log('类型从', oldType, '变', newType);
fetchSearchData(); // 任一变化都重新请求
}
);
场景 3:深度监听(监听对象 / 数组内部变化)
比如监听朋友圈文章对象的内部属性(如封面图数组变化):
import { reactive, watch } from 'vue';
// 响应式对象(朋友圈文章)
const article = reactive({
id: 1,
title: '测试文章',
coverUrls: ['https://xxx.jpg'] // 多图数组
});
// 深度监听:coverUrls 数组内部变化
watch(
article, // 监听整个对象
(newVal, oldVal) => {
console.log('封面图数组变化:', newVal.coverUrls);
},
{ deep: true } // 必须开启深度监听
);
// 测试:修改数组内部元素 → 触发监听
article.coverUrls.push('https://yyy.jpg');
注意:
-
监听
reactive对象时,oldVal会和newVal指向同一个对象(因为是引用类型),如需对比旧值,需手动深拷贝; -
仅监听对象的某个属性时,推荐用「函数返回值」+ 无需
deep:// 更高效:只监听 coverUrls 属性,无需深度监听 watch( () => article.coverUrls, (newUrls) => { console.log('封面图变化:', newUrls); } );
五、watch vs watchEffect(易混淆点)
Vue 3 还有 watchEffect,和 watch 核心区别:
| 特性 | watch |
watchEffect |
|---|---|---|
| 监听目标 | 显式指定要监听的数据 | 隐式监听回调中用到的所有响应式数据 |
| 旧值获取 | 支持(newVal/oldVal) | 不支持(只能拿到新值) |
| 执行时机 | 默认数据变化才执行(可配 immediate) | 立即执行一次,之后依赖变化再执行 |
| 适用场景 | 需要对比新旧值、精准监听特定数据 | 简单监听,无需旧值、自动收集依赖 |
ref
在 Vue 中,
ref不仅能创建响应式变量,还能获取 DOM 元素 / 组件实例 ,是前端操作 DOM 的核心方式(替代原生document.querySelector)。结合你的项目场景(比如操作朋友圈列表滚动、图片预览),下面从「核心用法、语法、场景、避坑点」全维度解析:
一、为什么用 ref 获取 DOM?
原生 JS 获取 DOM(document.querySelector)有两个核心问题:
- 时机问题 :Vue 模板渲染是异步的,直接在
setup中调用原生方法可能拿不到 DOM(模板还没渲染); - 耦合问题:原生方法依赖 DOM 结构(比如类名 / ID),模板结构变化后代码需同步修改;
而 ref 获取 DOM 是 Vue 官方推荐方式:✅ 自动等待 DOM 渲染完成后赋值;✅ 与模板解耦(通过 ref 标识绑定,不依赖类名 / ID);✅ 支持组件内局部 DOM 操作,避免全局污染。
二、核心语法(Vue 3 组合式 API)
步骤 1:模板中给 DOM 加 ref 标识
<template>
<!-- 1. 给普通 DOM 加 ref 标识 -->
<div ref="listRef" class="result-list">
<!-- 朋友圈/文章列表 -->
</div>
<!-- 2. 给图片加 ref(数组形式,遍历场景) -->
<img
v-for="(url, idx) in coverUrls"
:key="idx"
:ref="(el) => imgRefs[idx] = el" <!-- 遍历场景特殊写法 -->
:src="url"
>
</template>
步骤 2:脚本中定义 ref 变量并获取 DOM
<script setup>
import { ref, onMounted } from 'vue';
// 1. 定义 ref 变量(初始值为 null)
const listRef = ref(null);
// 2. 遍历场景:定义数组存储多个 DOM 元素
const imgRefs = ref([]);
// 关键:DOM 渲染完成后才能获取(onMounted 钩子)
onMounted(() => {
// 获取单个 DOM 元素
console.log(listRef.value); // <div class="result-list">...</div>
// 操作 DOM:比如设置滚动高度
listRef.value.scrollTop = 0;
// 获取遍历的 DOM 数组
console.log(imgRefs.value[0]); // 第一张图片的 DOM 元素
});
</script>
三、核心规则 & 关键说明
1. 基础用法(单个 DOM)
| 步骤 | 操作 | 示例 |
|---|---|---|
| 模板 | 给 DOM 加 ref="xxx" |
<div ref="listRef">...</div> |
| 脚本 | 定义 const xxx = ref(null) |
const listRef = ref(null) |
| 访问 | 需在 DOM 渲染完成后(onMounted/watch 等),通过 xxx.value 获取 |
listRef.value.scrollTop = 0 |
2. 遍历场景(多个 DOM)
遍历列表时(比如朋友圈多图),直接用 ref="imgRefs" 会只保留最后一个 DOM,需用函数形式绑定:
<template>
<img
v-for="(url, idx) in coverUrls"
:key="idx"
:ref="(el) => {
// el 是当前 DOM 元素,idx 是索引
if (el) imgRefs.value[idx] = el; // 非空判断(避免卸载时 el 为 null)
}"
>
</template>
<script setup>
const imgRefs = ref([]); // 数组存储所有图片 DOM
onMounted(() => {
console.log(imgRefs.value); // [img, img, img...] 所有图片 DOM
});
</script>
3. 时机问题(核心避坑点)
ref 获取 DOM 的值默认是 null,因为 Vue 模板渲染是异步的:
-
❌ 错误:直接在
setup中访问setup() { const listRef = ref(null); console.log(listRef.value); // null(模板还没渲染) return { listRef }; } -
✅ 正确:在 DOM 渲染完成后的钩子 / 回调中访问
onMounted:组件挂载(DOM 渲染完成)后执行;watch+flush: 'post':数据变化且 DOM 更新后执行;nextTick:下一次 DOM 更新循环后执行;
示例:数据变化后操作 DOM
import { ref, watch, nextTick } from 'vue';
const coverUrls = ref([]);
// 监听封面图变化,DOM 更新后操作图片
watch(coverUrls, async () => {
await nextTick(); // 等待 DOM 渲染完成
console.log(imgRefs.value[0]); // 能拿到最新的图片 DOM
});
四、实用场景(结合你的项目)
场景 1:列表滚动到顶部(搜索结果更新后)
<template>
<div ref="listRef" class="article-list">
<!-- 朋友圈/文章列表 -->
</div>
</template>
<script setup>
const listRef = ref(null);
const keyword = ref('');
// 监听关键词变化,更新列表后滚动到顶部
watch(keyword, async () => {
// 1. 请求新的搜索结果
await fetchArticleList();
// 2. 等待 DOM 更新完成
await nextTick();
// 3. 操作 DOM:滚动到顶部
listRef.value.scrollTop = 0;
});
</script>
场景 2:图片预览(获取图片 DOM 绑定点击事件)
<template>
<img
v-for="(url, idx) in coverUrls"
:key="idx"
:ref="(el) => imgRefs[idx] = el"
:src="url"
>
</template>
<script setup>
const imgRefs = ref([]);
const coverUrls = ref([
'https://picsum.photos/150/150?1',
'https://picsum.photos/150/150?2'
]);
onMounted(() => {
// 给每张图片绑定点击预览事件
imgRefs.value.forEach((imgEl, idx) => {
imgEl.addEventListener('click', () => {
console.log(`预览第${idx+1}张图片:`, imgEl.src);
// 你的预览逻辑:比如打开弹窗、放大图片等
});
});
});
</script>
场景 3:获取输入框焦点(搜索框自动聚焦)
<template>
<input ref="searchInputRef" v-model="keyword" placeholder="搜索文章...">
</template>
<script setup>
const searchInputRef = ref(null);
onMounted(() => {
// 页面加载后,搜索框自动获取焦点
searchInputRef.value.focus();
});
</script>
五、避坑点 & 注意事项
-
组件卸载时清理 :如果给 DOM 绑定了事件,需在
onUnmounted中移除,避免内存泄漏:import { onUnmounted } from 'vue'; onMounted(() => { const clickHandler = () => { /* 预览逻辑 */ }; imgRefs.value[0].addEventListener('click', clickHandler); // 组件卸载时移除事件 onUnmounted(() => { imgRefs.value[0].removeEventListener('click', clickHandler); }); }); -
v-if场景 :v-if为false时 DOM 会被销毁,ref.value会变为null,访问前需判断:if (listRef.value) { // 非空判断 listRef.value.scrollTop = 0; } -
组件上的
ref:如果ref加在 Vue 组件上,获取的是组件实例 而非 DOM 元素(需通过expose暴露组件内方法 / 数据):<!-- 子组件 MyImage.vue --> <script setup> const expose({ preview: () => { /* 预览逻辑 */ } }); </script> <!-- 父组件 --> <template> <MyImage ref="imgCompRef" /> </template> <script setup> const imgCompRef = ref(null); onMounted(() => { imgCompRef.value.preview(); // 调用子组件暴露的方法 }); </script>