在 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
//JS
// 用 location.href:需要手动拼接 URL,易出错(比如特殊字符、空格)
goSearch() {
const inputVal = this.$refs.input.value; // 假设获取输入框值
location.href = `/search?key=${inputVal}`; // 手动拼参数,需处理编码
}

// 用 $router.push:直接传参数,Vue Router 自动处理
goSearch() {
const inputVal = this.$refs.input.value;
this.$router.push({
name: 'search',
query: { key: inputVal } // 优雅传递参数,无需手动拼URL
});
}

而且你的代码里还用了「命名路由」(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
//JS
// 全局路由守卫:拦截所有路由跳转
router.beforeEach((to, from, next) => {
// 比如:跳转到search页必须传key参数,否则拦截
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 完全不能用,它的适用场景是:

  1. 跳转到 外部域名(比如 location.href = 'https://www.baidu.com'):Vue Router 只能处理项目内部路由,跳转到外部网站必须用 location.href
  2. 强制刷新当前页面(比如 location.reload(),等价于 location.href = 当前URL):需要重置所有页面状态时才用。

总结

你的代码是 Vue 单页应用的内部路由跳转(从首页跳转到搜索页),用 this.$router.push 而非 location.href 的核心原因:

  1. 无刷新跳转,保持 SPA 流畅的用户体验;
  2. 优雅处理路由参数、命名路由,易维护;
  3. 支持路由守卫,能统一控制跳转规则;
  4. 保留页面 / 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
<!-- 滚动容器:固定高度,overflow:auto 产生滚动条 -->
<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%;
/* 高度会通过JS动态设置为列表总高度 */
}

/* 可视区域容器:绝对定位,宽度100%,top/transform控制偏移 */
.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
// ===================== 1. 初始化参数 =====================
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, // 缓冲项数(上下各多渲染5项,避免快速滚动空白)
viewportHeight: 500 // 可视区域高度(等于滚动容器高度)
};

// 计算可视区域能显示的基础项数
const visibleCount = Math.ceil(config.viewportHeight / config.itemHeight);

// ===================== 2. 初始化渲染 =====================
function init() {
// 1. 设置占位容器高度(总高度),撑开滚动容器,让滚动条正常显示
placeholder.style.height = `${totalData.length * config.itemHeight}px`;
// 2. 渲染初始可视区域内容
renderVisibleItems(0);
}

// ===================== 3. 核心:渲染可视区域列表项 =====================
function renderVisibleItems(startIndex) {
// 计算结束索引:起始索引 + 可视项数 + 缓冲项
const endIndex = Math.min(
startIndex + visibleCount + config.buffer,
totalData.length // 防止超出总数据长度
);
// 截取可视区域的数据
const visibleData = totalData.slice(startIndex, endIndex);

// 渲染DOM:拼接HTML(实际项目可优化为文档碎片,减少重排)
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)`;
}

// ===================== 4. 监听滚动事件 =====================
function handleScroll() {
// 获取滚动容器的滚动偏移量(距离顶部的距离)
const scrollTop = scrollContainer.scrollTop;
// 计算当前可视区域的起始索引
const startIndex = Math.floor(scrollTop / config.itemHeight);
// 渲染可视区域内容
renderVisibleItems(startIndex);
}

// 防抖处理:避免scroll事件频繁触发(优化性能)
function debounce(fn, delay = 16) {
let timer = null;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}

// ===================== 5. 启动 =====================
scrollContainer.addEventListener('scroll', debounce(handleScroll));
init();

效果验证

运行代码后,滚动列表时:

  • DOM 面板中只会看到几十条.list-item节点(而非 10 万条);
  • 滚动流畅,无卡顿;
  • 滚动条长度对应 10 万条数据的总高度(因为 placeholder 撑开了)。

关键细节解释

  1. placeholder 的作用:如果没有这个占位容器,滚动容器的高度只有可视区域高度,滚动条会消失,无法滚动。设置总高度后,滚动条的长度和「真实长列表」一致。
  2. transform 偏移:用translateY代替top,因为 transform 触发 GPU 加速,性能更好,且不会触发重排。
  3. 防抖处理:scroll 事件会高频触发(每秒几十次),防抖能减少 renderVisibleItems 的调用次数,提升性能。
  4. 缓冲项 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>

四、进阶:动态高度虚拟列表(解决固定高度限制)

上面的示例是「固定高度」,但实际项目中列表项高度可能不固定(比如有的项有图片,有的没有),这时候需要调整思路:

核心思路

  1. 预估算高度:先给每个项设置一个「预估高度」,渲染后获取真实高度并缓存;
  2. 动态更新总高度:根据缓存的真实高度,动态更新占位容器的总高度;
  3. 修正偏移量:滚动时根据真实高度重新计算起始索引和偏移量。

关键代码调整(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
//JS
// 在VirtualList.vue中新增响应式数据:
// 缓存每个列表项的真实高度
const itemHeightCache = ref(new Map());
// 预估高度(代替固定高度)
const estimateHeight = props.itemHeight;

// 重写totalHeight计算属性:
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和scroll事件后调用updateItemHeight:
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 = 目标偏移量

六、成熟库推荐(实际项目优先用)

如果不想自己封装,可直接使用成熟的虚拟列表库,功能更全:

  1. vue-virtual-scroller(Vue2/Vue3):支持固定高度、动态高度、横向列表、无限滚动;
  2. @tanstack/virtual(跨框架):React/Vue/ 原生 JS 都能用,轻量且高性能;
  3. vxe-table(Vue):如果是表格类虚拟列表,推荐这个,支持虚拟滚动表格。

总结

虚拟列表的核心是「只渲染可视区域 DOM」,掌握固定高度版的原理后,动态高度只是「高度计算方式」的调整。实际开发中:

  1. 简单场景(固定高度长列表):用自己封装的组件即可;
  2. 复杂场景(动态高度、无限滚动、横向列表):优先用成熟库,减少踩坑。

记住:虚拟列表不是银弹,只有当列表数据量达到「千级以上」且出现性能问题时才需要用;少量数据(几百条)直接渲染即可,没必要过度设计。