在 Vue 项目(尤其是单页应用 SPA)中,不用 location.href 而用 this.$router.push 跳转,核心原因是:location.href 是「整页刷新式跳转」,而 Vue Router 的 $router.push 是「前端路由无刷新跳转」 ,完全适配 Vue 单页应用的设计理念。下面结合你的代码和场景,拆解具体原因:
一、先搞懂:两者的本质差异
特性
location.href
this.$router.push
跳转原理
操作浏览器地址栏,发起新的 HTTP 请求,整页刷新 (重新加载 HTML/CSS/JS 所有资源)
仅修改 URL 路径(hash/history 模式),不刷新页面 ,仅替换 <router-view> 里的组件
页面状态
刷新后 Vue 实例、全局状态(Vuex)、组件数据全部丢失,需重新初始化
保留当前 Vue 实例、全局状态,仅更新目标组件,状态不丢失
路由能力
无路由参数、守卫、命名路由等能力,仅能拼字符串 URL
支持 query/params 参数、命名路由、路由守卫等 Vue Router 核心功能
二、为什么你的场景绝对不适合用 location.href? 结合你代码里的「搜索页面跳转」场景,具体分析:
1. 核心问题:location.href 会导致「整页刷新」,体验极差 Vue 项目是 单页应用(SPA) :整个项目只有一个 HTML 文件,所有页面都是通过「替换组件」实现切换,而非加载新页面。
若用 location.href = '/search':浏览器会重新请求 /search 路径的 HTML 文件(实际还是项目的 index.html),重新创建 Vue 实例、加载所有资源,页面会出现「白屏→重新渲染」的过程,用户体验像回到了传统多页应用;
而 this.$router.push:仅修改 URL,Vue Router 匹配到「search 路由」后,只渲染 <template> 里的 Search 组件,页面无刷新、无白屏,切换流畅,符合 SPA 的核心优势。
2. 路由参数传递:$router.push 更优雅、不易出错 你的场景是「搜索跳转」,必然需要传递「搜索关键字」(比如输入框里的内容),对比两者的写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 goSearch ( ) { const inputVal = this .$refs .input .value ; location.href = `/search?key=${inputVal} ` ; } goSearch ( ) { const inputVal = this .$refs .input .value ; this .$router .push ({ name : 'search' , query : { key : inputVal } }); }
而且你的代码里还用了「命名路由」(name: 'search'),如果路由路径很长(比如 /home/search/list),$router.push({ name: 'search' }) 只需写路由名,比 location.href 拼长字符串更易维护、不易写错。
3. 路由守卫:$router.push 支持拦截 / 控制跳转,location.href 做不到 Vue Router 提供了「路由守卫」(比如 beforeEach),可以在跳转前做拦截(比如登录验证、参数校验):
1 2 3 4 5 6 7 8 9 10 11 router.beforeEach ((to, from , next ) => { if (to.name === 'search' && !to.query .key ) { alert ('请输入搜索关键字!' ); next (false ); } else { next (); } });
但如果用 location.href,这行代码会直接触发浏览器跳转,无法被 Vue 拦截 ,相当于 “绕开了 Vue 的路由规则”,无法做统一的权限 / 参数校验。
4. 状态保留:$router.push 不丢失页面状态 比如你的首页有输入框、滚动位置、Vuex 里的全局数据:
用 location.href 跳转后,页面刷新,输入框内容、滚动位置、Vuex 状态全部丢失;
用 $router.push 跳转后,首页的 Vue 实例还在,只是「隐藏」了,返回时输入框内容、滚动位置都能保留,体验更友好。
三、补充:什么时候才用 location.href? 不是说 location.href 完全不能用,它的适用场景是:
跳转到 外部域名 (比如 location.href = 'https://www.baidu.com'):Vue Router 只能处理项目内部路由,跳转到外部网站必须用 location.href;
强制刷新当前页面(比如 location.reload(),等价于 location.href = 当前URL):需要重置所有页面状态时才用。
总结 你的代码是 Vue 单页应用的内部路由跳转(从首页跳转到搜索页),用 this.$router.push 而非 location.href 的核心原因:
无刷新跳转,保持 SPA 流畅的用户体验;
优雅处理路由参数、命名路由,易维护;
支持路由守卫,能统一控制跳转规则;
保留页面 / Vue 实例状态,不丢失数据。
而 location.href 的整页刷新特性,完全违背了 Vue 单页应用的设计初衷,因此在内部路由跳转中几乎不会使用。
前端虚拟列表怎么使用,你给我写一个教程,给我非常详细的教程
已完成思考
前端虚拟列表超详细教程(从原理到实战) 虚拟列表(Virtual List)是前端解决长列表渲染性能问题 的核心方案 —— 当列表数据量达到万级 / 十万级时,直接渲染所有 DOM 节点会导致页面卡顿、内存溢出,而虚拟列表仅渲染「可视区域」的 DOM 节点,非可视区域用空白占位,滚动时动态更新可视区域内容,从而将 DOM 节点数量控制在几十级,大幅提升性能。
一、先搞懂核心概念(必看) 在写代码前,先明确虚拟列表的核心术语和原理,避免只抄代码不懂逻辑:
1. 核心术语
术语
含义
滚动容器
承载列表的外层容器,设置 overflow: auto 产生滚动条(如<div class="scroll-container">)
可视区域(Viewport)
滚动容器中能看到的区域(比如容器高度 500px,可视区域高度就是 500px)
列表项高度
单个列表项的高度(先讲「固定高度」,这是基础;进阶讲「动态高度」)
总高度
所有列表项的总高度(用于撑开滚动容器,让滚动条显示正常)
偏移量(Offset)
可视区域列表项相对于滚动容器的垂直偏移,用于定位可视区域内容(避免滚动时内容错位)
缓冲项
可视区域上下额外渲染的少量列表项(比如多渲染 5 项),避免快速滚动时出现「空白闪屏」
2. 核心原理(固定高度版) 虚拟列表的核心是「计算 + 动态更新 」,步骤如下:
1 2 3 4 5 6 7 ① 计算可视区域能显示的列表项数量 = Math.ceil(可视区域高度 / 项高度) ② 计算列表总高度 = 列表总数据长度 × 项高度(撑开滚动容器) ③ 监听滚动容器的 `scroll` 事件,获取滚动偏移量 `scrollTop` ④ 计算当前可视区域的起始索引 = Math.floor(scrollTop / 项高度) ⑤ 计算可视区域结束索引 = 起始索引 + 可视区域数量 + 缓冲项数 ⑥ 截取对应范围的列表数据,渲染到可视区域 ⑦ 设置可视区域的偏移量 = 起始索引 × 项高度(让内容和滚动位置对齐)
二、入门:原生 JS 实现固定高度虚拟列表(理解原理) 先从原生 JS 实现,彻底搞懂底层逻辑,再讲 Vue 组件封装。
步骤 1:搭建 HTML 结构 1 2 3 4 5 6 7 <div id ="scrollContainer" class ="scroll-container" > <div id ="placeholder" class ="placeholder" > </div > <div id ="viewport" class ="viewport" > </div > </div >
步骤 2:编写基础样式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 .scroll-container { width : 400px ; height : 500px ; border : 1px solid #e5e7eb ; overflow : auto; position : relative; margin : 20px auto; } .placeholder { position : absolute; top : 0 ; left : 0 ; width : 100% ; } .viewport { position : absolute; top : 0 ; left : 0 ; width : 100% ; } .list-item { height : 50px ; line-height : 50px ; padding : 0 16px ; border-bottom : 1px solid #f0f0f0 ; box-sizing : border-box; }
步骤 3:编写核心 JS 逻辑(带详细注释) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 const scrollContainer = document .getElementById ('scrollContainer' );const placeholder = document .getElementById ('placeholder' );const viewport = document .getElementById ('viewport' );const totalData = Array .from ({ length : 100000 }, (_, index ) => ({ id : index + 1 , content : `列表项 ${index + 1 } ` })); const config = { itemHeight : 50 , buffer : 5 , viewportHeight : 500 }; const visibleCount = Math .ceil (config.viewportHeight / config.itemHeight );function init ( ) { placeholder.style .height = `${totalData.length * config.itemHeight} px` ; renderVisibleItems (0 ); } function renderVisibleItems (startIndex ) { const endIndex = Math .min ( startIndex + visibleCount + config.buffer , totalData.length ); const visibleData = totalData.slice (startIndex, endIndex); viewport.innerHTML = visibleData.map (item => ` <div class="list-item" data-id="${item.id} "> ${item.content} </div> ` ).join ('' ); viewport.style .transform = `translateY(${startIndex * config.itemHeight} px)` ; } function handleScroll ( ) { const scrollTop = scrollContainer.scrollTop ; const startIndex = Math .floor (scrollTop / config.itemHeight ); renderVisibleItems (startIndex); } function debounce (fn, delay = 16 ) { let timer = null ; return (...args ) => { clearTimeout (timer); timer = setTimeout (() => fn.apply (this , args), delay); }; } scrollContainer.addEventListener ('scroll' , debounce (handleScroll)); init ();
效果验证 运行代码后,滚动列表时:
DOM 面板中只会看到几十条.list-item节点(而非 10 万条);
滚动流畅,无卡顿;
滚动条长度对应 10 万条数据的总高度(因为 placeholder 撑开了)。
关键细节解释
placeholder 的作用 :如果没有这个占位容器,滚动容器的高度只有可视区域高度,滚动条会消失,无法滚动。设置总高度后,滚动条的长度和「真实长列表」一致。
transform 偏移 :用translateY代替top,因为 transform 触发 GPU 加速,性能更好,且不会触发重排。
防抖处理 :scroll 事件会高频触发(每秒几十次),防抖能减少 renderVisibleItems 的调用次数,提升性能。
缓冲项 buffer :如果不设置缓冲项,快速滚动时会看到短暂的空白,多渲染 5 项能解决这个问题。
三、实战:Vue3 封装可复用的虚拟列表组件 在原生原理基础上,封装 Vue3 组件(Vue2 写法类似,仅组合式 API 和选项式 API 的区别),适配实际项目开发。
步骤 1:创建 VirtualList.vue 组件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 <template> <!-- 滚动容器 --> <div class="virtual-list-container" ref="containerRef" @scroll="handleScroll" :style="{ height: containerHeight + 'px', width: containerWidth + 'px' }" > <!-- 占位容器:撑开滚动条 --> <div class="placeholder" :style="{ height: totalHeight + 'px' }"></div> <!-- 可视区域容器 --> <div class="viewport" :style="{ transform: `translateY(${offsetY}px)` }" > <!-- 渲染可视区域列表项 --> <div class="list-item" v-for="item in visibleData" :key="item.id" :style="{ height: itemHeight + 'px', lineHeight: itemHeight + 'px' }" > <!-- 插槽:让父组件自定义列表项内容 --> <slot :item="item"></slot> </div> </div> </div> </template> <script setup> import { ref, computed, onMounted, watch } from 'vue'; // ===================== 1. 定义Props(可配置) ===================== const props = defineProps({ // 列表总数据 data: { type: Array, required: true, default: () => [] }, // 列表项固定高度 itemHeight: { type: Number, required: true, default: 50 }, // 滚动容器高度 containerHeight: { type: Number, default: 500 }, // 滚动容器宽度 containerWidth: { type: Number, default: 400 }, // 缓冲项数 buffer: { type: Number, default: 5 } }); // ===================== 2. 定义响应式数据 ===================== // 滚动容器DOM引用 const containerRef = ref(null); // 滚动偏移量(scrollTop) const scrollTop = ref(0); // 可视区域起始索引 const startIndex = ref(0); // 列表总高度 const totalHeight = computed(() => props.data.length * props.itemHeight); // 可视区域能显示的基础项数 const visibleCount = computed(() => Math.ceil(props.containerHeight / props.itemHeight)); // ===================== 3. 计算可视区域数据和偏移量 ===================== // 可视区域数据 const visibleData = computed(() => { // 结束索引:防止超出总数据长度 const endIndex = Math.min( startIndex.value + visibleCount.value + props.buffer, props.data.length ); // 截取可视区域数据 return props.data.slice(startIndex.value, endIndex); }); // 可视区域偏移量(Y轴) const offsetY = computed(() => startIndex.value * props.itemHeight); // ===================== 4. 滚动事件处理 ===================== const handleScroll = (e) => { // 更新滚动偏移量 scrollTop.value = e.target.scrollTop; // 计算起始索引 startIndex.value = Math.floor(scrollTop.value / props.itemHeight); }; // ===================== 5. 防抖优化(可选) ===================== // 如果你觉得scroll触发还是太频繁,可以给handleScroll加防抖: // import { debounce } from 'lodash'; // const handleScroll = debounce((e) => { // scrollTop.value = e.target.scrollTop; // startIndex.value = Math.floor(scrollTop.value / props.itemHeight); // }, 16); // ===================== 6. 监听数据变化,重置起始索引 ===================== watch(() => props.data.length, () => { startIndex.value = 0; if (containerRef.value) { containerRef.value.scrollTop = 0; } }); // ===================== 7. 初始化 ===================== onMounted(() => { // 初始渲染可视区域内容 startIndex.value = 0; }); </script> <style scoped> /* 滚动容器 */ .virtual-list-container { border: 1px solid #e5e7eb; overflow: auto; position: relative; margin: 0 auto; } /* 占位容器 */ .placeholder { position: absolute; top: 0; left: 0; width: 100%; pointer-events: none; /* 防止占位容器遮挡点击事件 */ } /* 可视区域容器 */ .viewport { position: absolute; top: 0; left: 0; width: 100%; } /* 列表项基础样式 */ .list-item { padding: 0 16px; box-sizing: border-box; border-bottom: 1px solid #f0f0f0; } </style>
步骤 2:使用虚拟列表组件 在父组件中引入并使用,支持自定义列表项内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 //vue <template> <div class="demo"> <h3>Vue3虚拟列表示例(10万条数据)</h3> <!-- 使用虚拟列表组件 --> <VirtualList :data="totalData" :item-height="50" :container-height="600" :container-width="500" > <!-- 插槽:自定义列表项内容 --> <template #default="{ item }"> <div> <span>ID:{{ item.id }}</span> <span style="margin-left: 20px;">内容:{{ item.content }}</span> </div> </template> </VirtualList> </div> </template> <script setup> import VirtualList from './components/VirtualList.vue'; // 模拟10万条数据 const totalData = Array.from({ length: 100000 }, (_, index) => ({ id: index + 1, content: `Vue虚拟列表项 ${index + 1}` })); </script> <style scoped> .demo { padding: 20px; } </style>
四、进阶:动态高度虚拟列表(解决固定高度限制) 上面的示例是「固定高度」,但实际项目中列表项高度可能不固定(比如有的项有图片,有的没有),这时候需要调整思路:
核心思路
预估算高度 :先给每个项设置一个「预估高度」,渲染后获取真实高度并缓存;
动态更新总高度 :根据缓存的真实高度,动态更新占位容器的总高度;
修正偏移量 :滚动时根据真实高度重新计算起始索引和偏移量。
关键代码调整(Vue3 组件为例) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 const itemHeightCache = ref (new Map ());const estimateHeight = props.itemHeight ;const totalHeight = computed (() => { let height = 0 ; for (let i = 0 ; i < props.data .length ; i++) { height += itemHeightCache.value .get (i) || estimateHeight; } return height; }); const updateItemHeight = ( ) => { const items = containerRef.value .querySelectorAll ('.list-item' ); items.forEach ((item, index ) => { const realHeight = item.offsetHeight ; const dataIndex = startIndex.value + index; itemHeightCache.value .set (dataIndex, realHeight); }); }; onMounted (() => { startIndex.value = 0 ; nextTick (() => { updateItemHeight (); }); }); const handleScroll = (e ) => { scrollTop.value = e.target .scrollTop ; calculateStartIndex (); nextTick (() => { updateItemHeight (); }); }; const calculateStartIndex = ( ) => { let sum = 0 ; let targetIndex = 0 ; for (let i = 0 ; i < props.data .length ; i++) { const height = itemHeightCache.value .get (i) || estimateHeight; sum += height; if (sum >= scrollTop.value ) { targetIndex = i; break ; } } startIndex.value = Math .max (0 , targetIndex - props.buffer ); };
注意:动态高度虚拟列表复杂度更高,若项目中列表项高度差异不大,优先用「固定高度 + 缓冲项」,性能更好;只有高度差异极大时,才用动态高度方案。
五、优化技巧 & 常见问题 1. 性能优化
防抖 / 节流 :scroll 事件必须加防抖(推荐 16ms,对应 60 帧),避免高频触发渲染;
缓存 DOM :避免每次滚动都重新创建 DOM(Vue 的 v-for+key 已做优化,无需额外处理);
减少重排 :用 transform 代替 top/left 定位,避免触发浏览器重排;
懒加载数据 :如果数据是十万级以上,可结合接口分页,滚动到底部时加载下一页数据,而非一次性生成所有数据。
2. 常见问题解决
问题
解决方案
快速滚动出现空白
增加缓冲项(buffer 设为 5-10);优化 scroll 防抖时间(不要太长)
列表项点击事件失效
占位容器添加pointer-events: none;确保列表项的 key 唯一且稳定
动态高度计算不准
渲染后用nextTick获取真实高度;缓存每个项的高度,避免重复计算
滚动到指定位置
计算目标项的偏移量,设置containerRef.value.scrollTop = 目标偏移量
六、成熟库推荐(实际项目优先用) 如果不想自己封装,可直接使用成熟的虚拟列表库,功能更全:
vue-virtual-scroller (Vue2/Vue3):支持固定高度、动态高度、横向列表、无限滚动;
@tanstack/virtual (跨框架):React/Vue/ 原生 JS 都能用,轻量且高性能;
vxe-table (Vue):如果是表格类虚拟列表,推荐这个,支持虚拟滚动表格。
总结 虚拟列表的核心是「只渲染可视区域 DOM 」,掌握固定高度版的原理后,动态高度只是「高度计算方式」的调整。实际开发中:
简单场景(固定高度长列表):用自己封装的组件即可;
复杂场景(动态高度、无限滚动、横向列表):优先用成熟库,减少踩坑。
记住:虚拟列表不是银弹,只有当列表数据量达到「千级以上」且出现性能问题时才需要用;少量数据(几百条)直接渲染即可,没必要过度设计。