点击按钮不重复发送请求

一个按钮,用户点击会发送请求,如何做才能在用户反复点击按钮时不重复发送请求?

要防止用户在反复点击按钮时重复发送请求,可以使用以下几种策略:

禁用按钮

在发送请求时禁用按钮,直到请求完成。这样可以防止用户在请求未完成前再次点击按钮。

使用节流(Throttle)

节流函数在特定时间段内只允许触发一次操作,无论用户点击按钮多少次。

使用防抖(Debounce)

防抖函数可以确保在一段时间内只执行一次函数。

使用请求状态管理

维护请求状态,防止重复发送请求。

总结

  • 禁用按钮:简单直接,适合多数情况。
  • 节流:限制函数调用频率,适用于频繁点击。
  • 防抖:确保函数在最后一次点击后执行一次,适用于短时间内多次点击的情况。
  • 请求状态管理:跟踪请求状态,避免重复请求。

控制请求并发的数量,例如有8个请求需要发送,如何控制同时只能并行发送两个请求

要控制请求的并发数量,比如限制同时只能并行发送两个请求,可以使用以下几种方法:

使用并发限制库

使用现成的库,如 p-limitasync,可以很方便地限制并发请求的数量。

使用 p-limit

1
npm install p-limit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import pLimit from 'p-limit';

// 创建一个并发限制器,限制同时只能运行2个任务
const limit = pLimit(2);

// 模拟请求函数
const fetchRequest = async (id) => {
console.log(`Request ${id} started`);
return new Promise((resolve) => setTimeout(() => {
console.log(`Request ${id} finished`);
resolve(id);
}, 1000));
};

// 请求数组
const requests = Array.from({ length: 8 }, (_, i) => () => fetchRequest(i + 1));

// 使用限制器执行请求
const executeRequests = async () => {
await Promise.all(requests.map((request) => limit(request)));
console.log('All requests completed');
};

executeRequests();

使用 async 库的 queue 功能

1
npm install async
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
import async from 'async';

// 创建一个并发限制队列,限制同时只能运行2个任务
const queue = async.queue(async (task, callback) => {
await task();
callback();
}, 2);

// 模拟请求函数
const fetchRequest = async (id) => {
console.log(`Request ${id} started`);
return new Promise((resolve) => setTimeout(() => {
console.log(`Request ${id} finished`);
resolve(id);
}, 1000));
};

// 添加请求到队列
const requests = Array.from({ length: 8 }, (_, i) => () => fetchRequest(i + 1));
requests.forEach((request) => queue.push(request));

// 处理队列完成后的回调
queue.drain(() => {
console.log('All requests completed');
});

手动控制并发

如果不想使用外部库,可以使用自定义逻辑来控制并发数量。以下是一个示例实现:

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
// 创建一个控制并发的函数
const limitConcurrency = (tasks, concurrency) => {
let index = 0;
const results = [];
const executing = [];

const enqueue = async () => {
if (index === tasks.length) {
return Promise.resolve();
}

const task = tasks[index++];
const p = Promise.resolve().then(() => task());
results.push(p);

const e = p.finally(() => {
executing.splice(executing.indexOf(e), 1);
});
executing.push(e);

const res = Promise.race(executing);
return res.then(() => enqueue());
};

return Promise.all(results);
};

// 模拟请求函数
const fetchRequest = async (id) => {
console.log(`Request ${id} started`);
return new Promise((resolve) => setTimeout(() => {
console.log(`Request ${id} finished`);
resolve(id);
}, 1000));
};

// 请求数组
const requests = Array.from({ length: 8 }, (_, i) => () => fetchRequest(i + 1));

// 使用并发限制函数执行请求
const executeRequests = async () => {
await limitConcurrency(requests, 2);
console.log('All requests completed');
};

executeRequests();

总结

  • 使用 p-limitasync:提供了简单易用的接口来限制并发请求数量。
  • 手动控制并发:可以更细粒度地控制并发,但需要更多代码和逻辑处理。

排查网络连接

如果一个用户不能访问你的网站,但其他网站都可以正常访问,这可能涉及多种原因。下面是一些排查步骤和方法来帮助你找到问题的根源:

检查网站是否正常运行

  1. 检查服务器状态

    • 确保服务器正在运行且没有崩溃。
    • 使用服务器监控工具(如 UptimeRobot、Pingdom)检查服务器的健康状态。
  2. 检查应用日志

    • 查看网站的服务器日志(例如 Nginx、Apache、Node.js、Python 等)来查找错误或异常情况。
  3. 检查部署和代码

    • 确保最近的代码部署没有引入错误。
    • 确保所有服务(如数据库、API)都正常运行。

检查用户的网络环境

  1. 检查用户的网络连接

    • 确保用户的网络连接正常。
    • 尝试让用户使用不同的网络环境(如切换到手机热点)来访问网站。
  2. 检查DNS解析

    • 让用户尝试访问网站的 IP 地址,而不是域名,以排除 DNS 问题。
    • 可以使用 nslookupdig 工具检查域名解析是否正确。
  3. 清除浏览器缓存

    • 让用户清除浏览器缓存和 cookie,并尝试重新访问网站。
  4. 使用 VPN

    • 如果用户在某些国家或地区,可能会遇到网络限制。建议用户尝试使用 VPN 访问网站,看看是否能解决问题。

检查网站的访问控制

  1. IP 黑名单

    • 检查是否有 IP 黑名单或防火墙规则阻止了用户的 IP 地址。
    • 确保没有误将用户的 IP 地址列入黑名单。
  2. 地理位置限制

    • 确认是否有地理位置限制(如 IP 地址范围限制)导致用户无法访问。

检查用户的设备

  1. 浏览器兼容性

    • 确保用户使用的浏览器版本与网站兼容。
    • 尝试让用户使用不同的浏览器访问网站。
  2. 浏览器插件

    • 检查是否有浏览器插件或扩展(如广告拦截器)干扰网站的访问。
  3. 安全软件

    • 有些安全软件或防火墙可能会阻止特定网站的访问。建议用户暂时禁用这些软件,检查是否能访问网站。

使用网络调试工具

  1. 浏览器开发者工具

    • 让用户打开浏览器的开发者工具(F12),查看控制台是否有错误信息。
    • 在网络面板中检查请求和响应是否正常。
  2. 在线网络工具

    • 使用在线工具(如 PingdomGTmetrix)检查网站的响应情况。

使用命令行工具

  1. Ping

    • 使用 ping 命令检查服务器是否可达:
      1
      ping yourwebsite.com
  2. Traceroute

    • 使用 traceroute (或 tracert 在 Windows 上)命令检查网络路径:
      1
      traceroute yourwebsite.com
  3. Curl

    • 使用 curl 命令检查服务器响应:
      1
      curl -I http://yourwebsite.com

总结

通过这些步骤,你可以排查出用户无法访问你的网站的原因。通常需要从服务器状态、用户网络环境、访问控制、用户设备、网络工具等多个方面进行检查,找到并解决问题。

如何更快的让用户看到渲染的页面

让用户更快地看到渲染的页面涉及到多个方面的优化,涵盖了前端和后端的策略。以下是一些有效的优化方法:

优化页面加载速度

  1. 压缩和优化资源

    • 图像优化:使用现代图像格式(如 WebP)和压缩工具减少图像大小。
    • 压缩文件:压缩 CSS 和 JavaScript 文件(如使用 gzipbrotli)。
    • 合并文件:合并多个 CSS 和 JavaScript 文件减少 HTTP 请求数量。
  2. **使用 Content Delivery Network (CDN)**:

    • 将静态资源(如图像、CSS、JavaScript)托管在 CDN 上,以加快全球用户的访问速度。
  3. 启用浏览器缓存

    • 配置缓存策略,使浏览器缓存静态资源,减少重复加载时间。
  4. 延迟加载

    • 图像和资源的懒加载:使用 loading="lazy" 属性或 JavaScript 延迟加载图像和其他资源。
    • 异步加载 JavaScript:使用 asyncdefer 属性异步加载 JavaScript 文件,避免阻塞渲染。
  5. 优化 HTML、CSS 和 JavaScript

    • Critical CSS:将关键 CSS 内嵌到 HTML 中,确保在初始页面加载时立即渲染样式。
    • 代码拆分:将 JavaScript 代码拆分成多个小块,仅加载当前页面所需的部分。

提升服务器响应速度

  1. 服务器性能优化

    • 使用高效的服务器配置和硬件,确保服务器能够快速响应请求。
    • 数据库优化:优化数据库查询,使用索引提高数据库访问速度。
  2. 启用 HTTP/2

    • HTTP/2 支持多路复用,减少延迟,提升加载速度。确保服务器和客户端都支持 HTTP/2。
  3. 使用服务器端缓存

    • 缓存动态内容:使用缓存机制(如 Redis、Memcached)存储频繁请求的动态内容。
    • 页面缓存:缓存静态页面或渲染后的页面,减少对后端的请求。

优化用户体验

  1. 显示占位符内容

    • 在内容加载之前,显示占位符(如骨架屏、加载动画)给用户反馈,减少感知的加载时间。
  2. 优先加载关键内容

    • 优先加载和渲染用户最关注的内容,确保关键内容在页面上快速显示。
  3. 优化交互和响应时间

    • 确保用户界面(UI)响应快速,减少用户的等待时间。

前端性能优化

  1. 利用 Service Workers

    • 使用 Service Workers 实现离线缓存和后台同步,提高页面加载速度和离线体验。
  2. 减少重绘和重排

    • 减少浏览器重绘和重排的次数,优化页面性能。
  3. 使用 Web Performance API

    • 监测和分析页面性能,使用 Web Performance API (如 Performance 接口)进行性能分析和优化。

技术选型和框架优化

  1. 前端框架优化

    • React:使用 React 的 React.lazySuspense 进行组件懒加载。
    • Vue:使用 Vue 的 async 组件和 vue-router 的懒加载功能。
  2. SSR(服务器端渲染)和 SSG(静态站点生成)

    • 使用服务器端渲染(如 Next.js、Nuxt.js)生成快速响应的页面,或者使用静态站点生成器(如 Gatsby)预生成页面。

示例

HTML 优化示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Optimized Page</title>
<style>
/* Critical CSS */
body { margin: 0; font-family: Arial, sans-serif; }
.header { background: #333; color: white; padding: 1em; text-align: center; }
</style>
</head>
<body>
<div class="header">Header Content</div>
<script src="app.js" defer></script>
</body>
</html>

JavaScript 异步加载示例:

1
2
3
4
5
6
// main.js
document.addEventListener('DOMContentLoaded', () => {
import('./feature-module.js').then(module => {
module.initializeFeature();
});
});

总结

  • 前端优化:压缩、缓存、懒加载、异步加载和优化资源。
  • 服务器优化:提升服务器性能、使用 CDN、启用 HTTP/2 和缓存。
  • 用户体验:显示占位符内容、优先加载关键内容、减少感知等待时间。
  • 技术选型:选择适合的前端框架和使用 SSR 或 SSG。

通过这些优化策略,可以显著提升页面加载速度和用户体验。

不同标签页或窗口间的通信的方式

在不同标签页或窗口间实现主动推送消息机制(不借助服务端),可以通过以下几种方式:

localStorage 事件

浏览器的 localStorage 提供了一种简单的跨标签页或窗口的消息传递方式。localStorage 的变动会触发 storage 事件,这个事件会在所有同源的其他窗口中触发。

优点

  • 简单易用
  • 广泛支持

缺点

  • 容量限制(通常为 5MB)
  • 非实时(可能有轻微延迟)

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 发送消息
function sendMessage(message) {
localStorage.setItem('message', JSON.stringify(message));
localStorage.removeItem('message'); // 清除存储项,以便多次触发 storage 事件
}

// 接收消息
window.addEventListener('storage', (event) => {
if (event.key === 'message') {
const message = JSON.parse(event.newValue);
console.log('Received message:', message);
}
});

// 发送消息示例
sendMessage({ text: 'Hello from another tab!' });

BroadcastChannel API

BroadcastChannel API 允许同源的不同浏览上下文(如标签页、iframe 等)通过一个发布/订阅模型进行消息传递。

优点

  • 实时消息传递
  • 简单易用
  • 不受 localStorage 的容量限制

缺点

  • 仅现代浏览器支持

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建频道
const channel = new BroadcastChannel('my_channel');

// 发送消息
function sendMessage(message) {
channel.postMessage(message);
}

// 接收消息
channel.onmessage = (event) => {
console.log('Received message:', event.data);
};

// 发送消息示例
sendMessage({ text: 'Hello from another tab!' });

Shared Worker

Shared Worker 允许多个浏览上下文(如多个标签页、iframe 等)共享一个 worker,可以用来在这些上下文之间共享数据和消息。

优点

  • 强大的功能
  • 可以实现复杂的消息传递和数据共享

缺点

  • 相对复杂
  • 仅现代浏览器支持

示例代码

shared-worker.js

1
2
3
4
5
6
7
8
9
10
// 共享 worker 的消息处理
onconnect = function (event) {
const port = event.ports[0];
port.onmessage = function (e) {
// 向所有连接的端口广播消息
for (let i = 0; i < ports.length; i++) {
ports[i].postMessage(e.data);
}
};
};

主页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建共享 worker
const sharedWorker = new SharedWorker('shared-worker.js');

// 发送消息
function sendMessage(message) {
sharedWorker.port.postMessage(message);
}

// 接收消息
sharedWorker.port.onmessage = (event) => {
console.log('Received message:', event.data);
};

// 发送消息示例
sendMessage({ text: 'Hello from another tab!' });

Service Workers

虽然 Service Workers 通常用于处理网络请求和缓存,但它们也可以用于标签页之间的消息传递,通过 clients.matchAllclient.postMessage 方法来实现。

优点

  • 强大的功能
  • 可以结合推送通知和后台同步

缺点

  • 相对复杂
  • 需要 HTTPS
  • 仅现代浏览器支持

示例代码

service-worker.js

1
2
3
4
5
6
7
self.addEventListener('message', (event) => {
self.clients.matchAll().then(clients => {
clients.forEach(client => {
client.postMessage(event.data);
});
});
});

主页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 注册 service worker
navigator.serviceWorker.register('/service-worker.js').then((registration) => {
// 发送消息
function sendMessage(message) {
if (registration.active) {
registration.active.postMessage(message);
}
}

// 接收消息
navigator.serviceWorker.addEventListener('message', (event) => {
console.log('Received message:', event.data);
});

// 发送消息示例
sendMessage({ text: 'Hello from another tab!' });
});

总结

这些方法都可以在不借助服务端的情况下,实现不同标签页或窗口间的消息传递。根据具体需求和浏览器支持情况,可以选择适合的方法来实现这一功能。

一次性渲染十万条数据保证页面不卡顿

虚拟列表加载

虚拟滚动(Virtual Scrolling)的原理是只渲染可视区域内的 DOM 元素,而将不可见的元素移除或不渲染,以此来优化性能,避免因为大量 DOM 元素渲染导致的卡顿问题。

虚拟滚动的原理:

  1. 可视区域和缓冲区:只渲染可视区域内的元素,同时在可视区域上下增加一定数量的缓冲区元素。
  2. 元素位置计算:通过元素的高度和滚动位置来计算哪些元素应该被渲染。
  3. 滚动事件监听:监听滚动事件,根据滚动位置动态更新渲染的元素。

下面是一个用纯 JavaScript 实现虚拟滚动的简单示例。假设我们有一个包含 10 万条数据的列表,并且每个元素的高度是固定的。

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Virtual Scrolling Example</title>
<style>
#container {
height: 500px;
overflow-y: auto;
position: relative;
border: 1px solid #ccc;
}
#content {
position: absolute;
width: 100%;
}
.item {
height: 30px;
box-sizing: border-box;
border-bottom: 1px solid #ddd;
padding: 5px;
}
</style>
</head>
<body>

<div id="container">
<div id="content"></div>
</div>

<script>
const container = document.getElementById('container');
const content = document.getElementById('content');

const totalItems = 100000; // 总条数
const itemHeight = 30; // 每个元素的高度
const buffer = 10; // 缓冲区元素数

// 计算容器总高度
content.style.height = `${totalItems * itemHeight}px`;

// 创建可视区域内的元素
function createItems(start, end) {
const fragment = document.createDocumentFragment();
for (let i = start; i < end; i++) {
const item = document.createElement('div');
item.className = 'item';
item.textContent = `Item ${i + 1}`;
item.style.top = `${i * itemHeight}px`;
fragment.appendChild(item);
}
return fragment;
}

// 更新渲染的元素
function updateItems() {
const scrollTop = container.scrollTop;
const viewportHeight = container.clientHeight;
const start = Math.floor(scrollTop / itemHeight) - buffer;
const end = Math.ceil((scrollTop + viewportHeight) / itemHeight) + buffer;

// 清空当前的元素
content.innerHTML = '';

// 添加新的元素
content.appendChild(createItems(Math.max(0, start), Math.min(totalItems, end)));
}

// 监听滚动事件
container.addEventListener('scroll', updateItems);

// 初始化渲染
updateItems();
</script>

</body>
</html>

解释

  1. HTML 和 CSS 部分

    • #container 是滚动容器,设置了固定高度和垂直滚动条。
    • #content 是用于放置所有列表项的容器,设置 position: absolute 以便根据需要移动元素位置。
    • .item 是每个列表项,设置了固定高度。
  2. JavaScript 部分

    • 计算容器的总高度以使滚动条正确显示。
    • createItems 函数根据起始和结束索引创建新的列表项,并返回一个文档片段(DocumentFragment)。
    • updateItems 函数根据滚动位置计算当前可视区域内需要渲染的元素,并更新 #content 容器内的元素。
    • 监听滚动事件,在滚动时调用 updateItems 更新渲染的元素。
    • 初始化时调用 updateItems 渲染初始的元素。

这个简单的虚拟滚动示例展示了如何通过只渲染可视区域内的元素来优化性能。在实际项目中,可以使用更复杂的逻辑来处理动态高度的元素、惰性加载数据等情况。

分页加载

将数据分成多个小块,每次只加载和渲染一部分数据。用户滚动到页面底部时,动态加载更多数据。这种方法适合用户不需要一次性查看所有数据的场景。

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
<template>
<div class="data-list">
<div v-for="item in visibleItems" :key="item.id" class="item">
{{ item.text }}
</div>
<button v-if="hasMore" @click="loadMore">Load More</button>
</div>
</template>

<script>
export default {
data() {
return {
items: [], // 全部数据
visibleItems: [], // 可见数据
pageSize: 100, // 每次加载的数据量
currentPage: 1, // 当前页码
};
},
computed: {
hasMore() {
return this.visibleItems.length < this.items.length;
}
},
created() {
// 生成数据
for (let i = 0; i < 100000; i++) {
this.items.push({ id: i, text: `Item ${i}` });
}
this.loadMore();
},
methods: {
loadMore() {
const start = (this.currentPage - 1) * this.pageSize;
const end = this.currentPage * this.pageSize;
this.visibleItems = this.visibleItems.concat(this.items.slice(start, end));
this.currentPage++;
}
}
};
</script>

<style>
.data-list {
max-height: 100vh;
overflow-y: auto;
}
.item {
height: 30px;
padding: 10px;
border-bottom: 1px solid #ccc;
}
</style>

惰性加载

在用户滚动时检测元素是否进入视口,只有进入视口的元素才会被渲染。可以使用 Intersection Observer API 来实现这种效果。

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
<template>
<div class="lazy-list">
<div
v-for="item in items"
:key="item.id"
v-intersect="loadItem"
class="item"
>
<span v-if="item.loaded">{{ item.text }}</span>
</div>
</div>
</template>

<script>
export default {
data() {
return {
items: []
};
},
created() {
for (let i = 0; i < 100000; i++) {
this.items.push({ id: i, text: `Item ${i}`, loaded: false });
}
},
directives: {
intersect: {
inserted(el, binding) {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
binding.value(entry.target);
observer.unobserve(entry.target);
}
});
});
observer.observe(el);
}
}
},
methods: {
loadItem(item) {
const index = this.items.findIndex(i => i.id === parseInt(item.innerText.split(' ')[1]));
if (index !== -1) {
this.$set(this.items, index, { ...this.items[index], loaded: true });
}
}
}
};
</script>

<style>
.lazy-list {
max-height: 100vh;
overflow-y: auto;
}
.item {
height: 30px;
padding: 10px;
border-bottom: 1px solid #ccc;
}
</style>

虚拟 DOM 节点回收

将不在视口内的 DOM 节点从文档流中移除,并在需要时重新添加。这样可以减少 DOM 节点的数量,从而提升性能。

按需渲染

使用 v-ifv-show 根据用户交互或滚动位置来动态渲染内容。例如,只渲染当前可见的部分内容,当用户滚动时动态加载并渲染新的内容。

浏览器原生优化

利用浏览器提供的优化技术,例如 requestAnimationFrame,来批量处理渲染操作,使得渲染过程更加平滑。

结论

每种方法都有其适用的场景和优缺点。根据具体需求和数据量的大小,可以选择合适的方法来优化大数据渲染,确保页面不卡顿。虚拟列表技术(Virtual Scrolling)依然是处理大量数据最常用和高效的方法之一,但在某些特定情况下,分页加载、惰性加载等方法也能提供有效的解决方案。

虚拟列表

在前端开发中,虚拟列表(Virtual List)是一种优化技术,用于处理大量数据时提高性能。通常,虚拟列表会仅渲染视口内的可见元素,而非一次性渲染所有元素,从而减少 DOM 操作和内存使用。

要在不固定高度情况下实现虚拟列表,有几种常见的方式可以选择。以下是一些方法和实现步骤:

方法一:使用第三方库

许多第三方库可以帮助你实现虚拟列表,并且支持不固定高度的情况。例如,react-windowvue-virtual-scroller 是两个常用的库。

Vue.js 示例:vue-virtual-scroller

  1. 安装 vue-virtual-scroller

    1
    npm install vue-virtual-scroller
  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
    38
    39
    <template>
    <div>
    <RecycleScroller
    :items="items"
    :item-size="getItemSize"
    key-field="id"
    >
    <template #default="{ item, index }">
    <div :key="item.id" :style="{ height: item.height + 'px' }">
    {{ item.content }}
    </div>
    </template>
    </RecycleScroller>
    </div>
    </template>

    <script>
    import { RecycleScroller } from 'vue-virtual-scroller'

    export default {
    components: {
    RecycleScroller
    },
    data() {
    return {
    items: [
    { id: 1, content: 'Item 1', height: 50 },
    { id: 2, content: 'Item 2', height: 100 },
    // 更多项...
    ]
    }
    },
    methods: {
    getItemSize(item) {
    return item.height
    }
    }
    }
    </script>

方法二:手动实现虚拟列表

如果你不想依赖第三方库,可以手动实现一个简单的虚拟列表。以下是一个基本思路:

  1. 创建容器和列表项:使用 div 容器包裹列表,并通过 v-for 渲染列表项。
  2. 计算可见区域和偏移:监听滚动事件,计算可见区域的起始和结束索引。
  3. 仅渲染可见项:根据可见区域的起始和结束索引,只渲染可见的列表项。

Vue.js 示例:手动实现

  1. 模板部分:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <template>
    <div ref="container" class="container" @scroll="onScroll">
    <div class="spacer" :style="{ height: totalHeight + 'px' }"></div>
    <div
    v-for="item in visibleItems"
    :key="item.id"
    class="item"
    :style="{ top: itemTop(item) + 'px', position: 'absolute' }"
    >
    {{ item.content }}
    </div>
    </div>
    </template>
  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
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    <script>
    export default {
    data() {
    return {
    items: [
    { id: 1, content: 'Item 1', height: 50 },
    { id: 2, content: 'Item 2', height: 100 },
    // 更多项...
    ],
    containerHeight: 0,
    scrollTop: 0
    }
    },
    computed: {
    totalHeight() {
    return this.items.reduce((sum, item) => sum + item.height, 0)
    },
    visibleItems() {
    let start = 0, end = 0
    let accumulatedHeight = 0

    // 计算起始索引
    for (let i = 0; i < this.items.length; i++) {
    if (accumulatedHeight >= this.scrollTop) {
    start = i
    break
    }
    accumulatedHeight += this.items[i].height
    }

    // 计算结束索引
    for (let i = start; i < this.items.length; i++) {
    accumulatedHeight += this.items[i].height
    if (accumulatedHeight >= this.scrollTop + this.containerHeight) {
    end = i
    break
    }
    }

    return this.items.slice(start, end + 1)
    }
    },
    mounted() {
    this.containerHeight = this.$refs.container.clientHeight
    },
    methods: {
    onScroll(event) {
    this.scrollTop = event.target.scrollTop
    },
    itemTop(item) {
    let top = 0
    for (let i = 0; i < this.items.length; i++) {
    if (this.items[i].id === item.id) break
    top += this.items[i].height
    }
    return top
    }
    }
    }
    </script>
  3. 样式部分:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    .container {
    position: relative;
    height: 400px; /* 或其他适合的高度 */
    overflow-y: auto;
    }
    .spacer {
    width: 100%;
    }
    .item {
    width: 100%;
    }

总结

使用第三方库如 vue-virtual-scroller 是一种方便且高效的方式,因为它们已经解决了大部分复杂问题。手动实现则提供了更多的灵活性,适合需要定制化需求的场景。根据你的具体需求选择适合的方法。

路由守卫如何实现

在 Vue.js 应用中,路由守卫(Router Guards)用于控制导航行为,确保用户只有在满足特定条件时才能访问某些路由。Vue Router 提供了三种类型的路由守卫:

  1. 全局守卫:适用于所有路由的导航。
  2. 路由级守卫:仅适用于特定路由。
  3. 组件内守卫:定义在组件内,只影响该组件的导航。

全局守卫

全局守卫可以在路由实例上使用 beforeEachafterEach 方法来定义。

示例:全局前置守卫

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
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'

const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About, meta: { requiresAuth: true } }
]

const router = createRouter({
history: createWebHistory(),
routes
})

// 全局前置守卫
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
// 假设这里有一个方法 `isLoggedIn` 来检查用户是否已登录
if (!isLoggedIn()) {
// 如果用户未登录,重定向到登录页面
next({ path: '/login' })
} else {
next()
}
} else {
next()
}
})

export default router

路由级守卫

路由级守卫在路由配置中定义,使用 beforeEnter

示例:路由级前置守卫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const routes = [
{
path: '/about',
component: About,
beforeEnter: (to, from, next) => {
// 路由级守卫逻辑
if (!isLoggedIn()) {
next('/login')
} else {
next()
}
}
}
]

组件内守卫

组件内守卫直接在组件内定义,使用 beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave

示例:组件内守卫

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
<template>
<div>
<h1>Profile</h1>
</div>
</template>

<script>
export default {
beforeRouteEnter(to, from, next) {
// 在导航到该组件的路由之前调用
if (!isLoggedIn()) {
next('/login')
} else {
next()
}
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但仍然渲染该组件时调用
if (to.params.id !== from.params.id) {
// 做一些额外的处理
}
next()
},
beforeRouteLeave(to, from, next) {
// 在导航离开该组件的路由之前调用
if (this.hasUnsavedChanges) {
const answer = window.confirm('You have unsaved changes. Do you really want to leave?')
if (answer) {
next()
} else {
next(false)
}
} else {
next()
}
},
methods: {
isLoggedIn() {
// 检查用户登录状态的逻辑
return true // 或者返回实际的登录状态
}
}
}
</script>

总结

路由守卫提供了强大的工具来控制导航行为。全局守卫适用于全局范围的检查,路由级守卫适用于特定路由的导航控制,组件内守卫适用于需要与组件生命周期紧密结合的导航逻辑。根据实际需求选择合适的守卫类型。

深拷贝是指完全复制一个对象及其嵌套的所有子对象,确保新对象和原对象在内存中是完全独立的。浅拷贝只复制对象的第一层属性,对于嵌套的子对象依旧是引用。

原生路由 history api 和 react-router 的差距是什么

原生路由 History API 和 React Router、Vue Router 之间的差距主要体现在功能、使用的简便性以及社区支持上。以下是详细的对比:

1. 原生 History API

概述

  • 原生的 History API 是浏览器自带的,提供了基本的路由功能。
  • 通过 window.history.pushStatewindow.history.replaceState 方法,可以在不重新加载页面的情况下更新 URL。
  • 使用 window.onpopstate 监听浏览器的前进和后退按钮事件。

优点

  • 不依赖外部库,纯原生实现,减少了依赖和包的体积。
  • 完全掌控路由行为和历史记录,灵活性高。

缺点

  • 功能有限,只提供了基础的 URL 操作和历史记录管理。
  • 需要手动管理路由状态和 URL 解析,代码量较大。
  • 没有提供高级功能,如路由守卫、嵌套路由、动态加载等。

示例

1
2
3
4
5
6
7
// 更新 URL 并添加一条历史记录
window.history.pushState({ page: 1 }, "Title", "/page1");

// 监听浏览器的前进和后退按钮
window.onpopstate = function(event) {
console.log("location: " + document.location + ", state: " + JSON.stringify(event.state));
};

2. React Router

概述

  • React Router 是 React 生态系统中最流行的路由库。
  • 提供了丰富的 API 和组件来管理应用的路由。
  • 支持动态路由、嵌套路由、路由守卫等高级功能。

优点

  • 集成了 React 的组件化开发模式,使用简单直观。
  • 提供了丰富的功能和灵活的路由配置。
  • 社区支持广泛,文档齐全,生态系统丰富。

缺点

  • 需要额外的依赖,增加了包的体积。
  • 学习曲线较原生 API 略高,需要了解 React Router 的用法和概念。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom';

function App() {
return (
<Router>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</nav>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
</Switch>
</Router>
);
}

function Home() {
return <h2>Home</h2>;
}

function About() {
return <h2>About</h2>;
}

3. Vue Router

概述

  • Vue Router 是 Vue.js 官方推荐的路由库,专为 Vue.js 设计。
  • 提供了丰富的功能,如嵌套路由、命名视图、路由守卫等。

优点

  • 深度集成 Vue.js,使用方便,符合 Vue 的开发习惯。
  • 提供了直观的路由配置和灵活的导航守卫。
  • 社区支持广泛,文档齐全,生态系统丰富。

缺点

  • 需要额外的依赖,增加了包的体积。
  • 学习曲线较原生 API 略高,需要了解 Vue Router 的用法和概念。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { createRouter, createWebHistory } from 'vue-router';
import Home from './components/Home.vue';
import About from './components/About.vue';

const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About }
];

const router = createRouter({
history: createWebHistory(),
routes
});

export default router;

总结

  • 原生 History API 适合简单的应用或希望完全掌控路由行为的场景,但需要更多手动操作和代码。
  • React RouterVue Router 提供了丰富的功能和更高层次的抽象,适合中大型应用,提升了开发效率和代码可维护性。
  • 选择使用哪个取决于项目需求、开发者的熟悉程度以及所使用的前端框架。

实现深拷贝的方法

方法一:使用 JSON 序列化和反序列化

这种方法适用于简单对象(不包含函数、Date 对象、undefined 等)。

1
2
3
4
5
6
7
8
9
10
11
12
13
const original = {
name: 'Alice',
age: 25,
address: {
city: 'Wonderland',
zip: '12345'
}
}

const deepCopy = JSON.parse(JSON.stringify(original))

console.log(deepCopy)
// { name: 'Alice', age: 25, address: { city: 'Wonderland', zip: '12345' } }

方法二:递归遍历

递归遍历对象的每一层,进行深拷贝。

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
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj
}

if (obj instanceof Date) {
return new Date(obj)
}

if (obj instanceof Array) {
const copy = []
obj.forEach((_, i) => (copy[i] = deepClone(obj[i])))
return copy
}

if (obj instanceof Object) {
const copy = {}
Object.keys(obj).forEach(key => (copy[key] = deepClone(obj[key])))
return copy
}

throw new Error('Unable to copy obj! Its type isn\'t supported.')
}

const original = {
name: 'Alice',
age: 25,
address: {
city: 'Wonderland',
zip: '12345'
}
}

const deepCopy = deepClone(original)

console.log(deepCopy)
// { name: 'Alice', age: 25, address: { city: 'Wonderland', zip: '12345' } }

方法三:使用 Lodash 的 cloneDeep

如果你的项目中使用了 Lodash 库,可以直接使用其提供的 cloneDeep 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const _ = require('lodash')

const original = {
name: 'Alice',
age: 25,
address: {
city: 'Wonderland',
zip: '12345'
}
}

const deepCopy = _.cloneDeep(original)

console.log(deepCopy)
// { name: 'Alice', age: 25, address: { city: 'Wonderland', zip: '12345' } }

深拷贝注意事项

  1. 循环引用:如果对象包含循环引用,上述方法一和方法二将会失败。需要特殊处理循环引用。
  2. 特殊类型:对象中包含特殊类型(如函数、DateMapSetRegExp 等)时,方法一不适用,需考虑其他方法。
  3. 性能:递归实现的深拷贝在处理大对象时性能可能不佳,需要权衡性能和代码复杂度。

处理循环引用的深拷贝示例

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
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') {
return obj
}

if (hash.has(obj)) {
return hash.get(obj)
}

if (obj instanceof Date) {
return new Date(obj)
}

if (obj instanceof Array) {
const arr = []
hash.set(obj, arr)
obj.forEach((_, i) => (arr[i] = deepClone(obj[i], hash)))
return arr
}

if (obj instanceof Object) {
const cloneObj = {}
hash.set(obj, cloneObj)
Object.keys(obj).forEach(key => (cloneObj[key] = deepClone(obj[key], hash)))
return cloneObj
}

throw new Error('Unable to copy obj! Its type isn\'t supported.')
}

const original = {
name: 'Alice',
age: 25,
address: {
city: 'Wonderland',
zip: '12345'
}
}
original.self = original // 循环引用

const deepCopy = deepClone(original)

console.log(deepCopy)
// { name: 'Alice', age: 25, address: { city: 'Wonderland', zip: '12345' }, self: [Circular] }

总结

深拷贝是确保对象在不同引用间独立存在的重要手段。具体选择哪种实现方式取决于数据的复杂度和具体需求。对于简单的对象结构,JSON 序列化和反序列化是快速而有效的方法;对于复杂数据结构,递归和库方法如 Lodash 的 cloneDeep 更加合适。

在html里延迟加载js文件有哪几种方式

  1. defer
  2. async
  3. <script> 标签放在 </body> 标签之前,这样可以确保 HTML 文档先被解析,在页面底部加载脚本
  4. 使用 JavaScript 动态创建 <script> 标签并加载脚本。