前端面试手撕合集之纯HTML、Vue和React组件
更新时间:2026年02月28日 08:26:28 作者:FE_Jinger
Vue 和React都是当前非常流行的前端框架,它们都可以用来构建单页面应用(SPA)和响应式的用户界面,这篇文章主要介绍了前端面试手撕代码之纯HTML、Vue和React组件的相关资料,需要的朋友可以参考下
一、原生JS 核心组件
1. 轮播图(自动播放+点击切换+指示器)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>原生JS轮播图</title>
<style>
.carousel { width: 500px; height: 300px; margin: 50px auto; position: relative; overflow: hidden; }
.carousel-list { display: flex; width: 100%; height: 100%; transition: transform 0.5s ease; }
.carousel-item { flex: 0 0 100%; width: 100%; height: 100%; }
.carousel-item img { width: 100%; height: 100%; object-fit: cover; }
.carousel-btn { position: absolute; top: 50%; transform: translateY(-50%); width: 40px; height: 40px; background: rgba(0,0,0,0.5); color: white; border: none; cursor: pointer; z-index: 10; }
.prev { left: 10px; }
.next { right: 10px; }
.carousel-indicator { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); display: flex; gap: 10px; }
.indicator-item { width: 10px; height: 10px; border-radius: 50%; background: rgba(255,255,255,0.5); cursor: pointer; }
.indicator-item.active { background: white; }
</style>
</head>
<body>
<div class="carousel">
<div class="carousel-list">
<div class="carousel-item"><img src="https://picsum.photos/500/300?1" alt="图1"></div>
<div class="carousel-item"><img src="https://picsum.photos/500/300?2" alt="图2"></div>
<div class="carousel-item"><img src="https://picsum.photos/500/300?3" alt="图3"></div>
</div>
<button class="carousel-btn prev">←</button>
<button class="carousel-btn next">→</button>
<div class="carousel-indicator">
<div class="indicator-item active"></div>
<div class="indicator-item"></div>
<div class="indicator-item"></div>
</div>
</div>
<script>
const carousel = document.querySelector('.carousel');
const list = document.querySelector('.carousel-list');
const items = document.querySelectorAll('.carousel-item');
const prevBtn = document.querySelector('.prev');
const nextBtn = document.querySelector('.next');
const indicators = document.querySelectorAll('.indicator-item');
const itemWidth = items[0].offsetWidth;
let currentIndex = 0;
let timer = null;
// 初始化指示器
function updateIndicator() {
indicators.forEach((item, index) => {
item.classList.toggle('active', index === currentIndex);
});
}
// 切换轮播
function switchCarousel(index) {
currentIndex = index;
list.style.transform = `translateX(-${currentIndex * itemWidth}px)`;
updateIndicator();
}
// 下一张
function next() {
currentIndex = (currentIndex + 1) % items.length;
switchCarousel(currentIndex);
}
// 上一张
function prev() {
currentIndex = (currentIndex - 1 + items.length) % items.length;
switchCarousel(currentIndex);
}
// 自动播放
function autoPlay() {
timer = setInterval(next, 3000);
}
// 事件绑定
nextBtn.addEventListener('click', next);
prevBtn.addEventListener('click', prev);
indicators.forEach((item, index) => {
item.addEventListener('click', () => switchCarousel(index));
});
// 鼠标悬停暂停
carousel.addEventListener('mouseenter', () => clearInterval(timer));
carousel.addEventListener('mouseleave', autoPlay);
// 启动自动播放
autoPlay();
</script>
</body>
</html>
2. 下拉组件(点击展开+点击外部关闭)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>原生JS下拉组件</title>
<style>
.dropdown { width: 200px; margin: 50px auto; position: relative; }
.dropdown-trigger { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; cursor: pointer; }
.dropdown-menu { width: 100%; padding: 0; margin: 0; list-style: none; border: 1px solid #ccc; border-top: none; border-radius: 0 0 4px 4px; position: absolute; top: 100%; left: 0; background: white; display: none; z-index: 999; }
.dropdown-menu.show { display: block; }
.dropdown-item { padding: 10px; cursor: pointer; }
.dropdown-item:hover { background: #f5f5f5; }
</style>
</head>
<body>
<div class="dropdown" id="dropdown">
<div class="dropdown-trigger">请选择</div>
<ul class="dropdown-menu">
<li class="dropdown-item">选项1</li>
<li class="dropdown-item">选项2</li>
<li class="dropdown-item">选项3</li>
</ul>
</div>
<script>
const dropdown = document.getElementById('dropdown');
const trigger = dropdown.querySelector('.dropdown-trigger');
const menu = dropdown.querySelector('.dropdown-menu');
const items = dropdown.querySelectorAll('.dropdown-item');
// 切换显示/隐藏
function toggleMenu() {
menu.classList.toggle('show');
}
// 点击外部关闭
document.addEventListener('click', (e) => {
if (!dropdown.contains(e.target)) {
menu.classList.remove('show');
}
});
// 选择选项
items.forEach(item => {
item.addEventListener('click', () => {
trigger.textContent = item.textContent;
menu.classList.remove('show');
});
});
// 绑定触发事件
trigger.addEventListener('click', (e) => {
e.stopPropagation(); // 防止事件冒泡触发外部点击
toggleMenu();
});
</script>
</body>
</html>
3. 表单验证(手机号+密码+提交验证)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>原生JS表单验证</title>
<style>
.form { width: 300px; margin: 50px auto; }
.form-item { margin-bottom: 20px; }
.form-label { display: block; margin-bottom: 5px; }
.form-input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
.form-input.error { border-color: red; }
.error-tip { color: red; font-size: 12px; margin-top: 5px; display: none; }
.error-tip.show { display: block; }
.submit-btn { width: 100%; padding: 10px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
.submit-btn:disabled { background: #ccc; cursor: not-allowed; }
</style>
</head>
<body>
<form class="form" id="loginForm">
<div class="form-item">
<label class="form-label">手机号</label>
<input type="tel" class="form-input" id="phone" placeholder="请输入手机号">
<div class="error-tip" id="phoneTip">请输入正确的11位手机号</div>
</div>
<div class="form-item">
<label class="form-label">密码</label>
<input type="password" class="form-input" id="pwd" placeholder="请输入6-16位密码">
<div class="error-tip" id="pwdTip">密码长度需在6-16位之间</div>
</div>
<button type="submit" class="submit-btn" id="submitBtn" disabled>提交</button>
</form>
<script>
const phoneInput = document.getElementById('phone');
const pwdInput = document.getElementById('pwd');
const phoneTip = document.getElementById('phoneTip');
const pwdTip = document.getElementById('pwdTip');
const submitBtn = document.getElementById('submitBtn');
const loginForm = document.getElementById('loginForm');
// 验证手机号
function validatePhone() {
const phone = phoneInput.value.trim();
const reg = /^1[3-9]\d{9}$/;
if (!reg.test(phone)) {
phoneInput.classList.add('error');
phoneTip.classList.add('show');
return false;
}
phoneInput.classList.remove('error');
phoneTip.classList.remove('show');
return true;
}
// 验证密码
function validatePwd() {
const pwd = pwdInput.value.trim();
if (pwd.length < 6 || pwd.length > 16) {
pwdInput.classList.add('error');
pwdTip.classList.add('show');
return false;
}
pwdInput.classList.remove('error');
pwdTip.classList.remove('show');
return true;
}
// 检查所有验证
function checkAll() {
const isPhoneValid = validatePhone();
const isPwdValid = validatePwd();
submitBtn.disabled = !(isPhoneValid && isPwdValid);
}
// 实时验证
phoneInput.addEventListener('input', checkAll);
pwdInput.addEventListener('input', checkAll);
// 提交验证
loginForm.addEventListener('submit', (e) => {
e.preventDefault();
if (checkAll()) {
alert('表单验证通过,提交成功!');
loginForm.reset();
submitBtn.disabled = true;
}
});
</script>
</body>
</html>
4. 无限滚动(滚动到底部加载数据)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>原生JS无限滚动</title>
<style>
.container { width: 500px; margin: 0 auto; }
.list { padding: 0; margin: 0; list-style: none; }
.list-item { padding: 20px; border-bottom: 1px solid #eee; margin-bottom: 10px; }
.loading { text-align: center; padding: 20px; color: #999; display: none; }
.loading.show { display: block; }
</style>
</head>
<body>
<div class="container">
<ul class="list" id="list"></ul>
<div class="loading" id="loading">加载中...</div>
</div>
<script>
const list = document.getElementById('list');
const loading = document.getElementById('loading');
let page = 1;
const pageSize = 10;
let isLoading = false; // 防止重复加载
// 模拟请求数据
function fetchData(page) {
return new Promise((resolve) => {
setTimeout(() => {
const data = Array.from({ length: pageSize }, (_, i) => ({
id: (page - 1) * pageSize + i + 1,
content: `列表项 ${(page - 1) * pageSize + i + 1}`
}));
resolve(data);
}, 1000);
});
}
// 渲染列表
function renderList(data) {
data.forEach(item => {
const li = document.createElement('li');
li.className = 'list-item';
li.textContent = item.content;
list.appendChild(li);
});
}
// 加载数据
async function loadMore() {
if (isLoading) return;
isLoading = true;
loading.classList.add('show');
try {
const data = await fetchData(page);
renderList(data);
page++;
} catch (err) {
alert('加载失败,请重试');
} finally {
isLoading = false;
loading.classList.remove('show');
}
}
// 监听滚动事件
window.addEventListener('scroll', () => {
// 滚动距离 + 视口高度 >= 文档总高度 - 100(提前加载)
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
const clientHeight = document.documentElement.clientHeight;
const scrollHeight = document.documentElement.scrollHeight;
if (scrollTop + clientHeight >= scrollHeight - 100 && !isLoading) {
loadMore();
}
});
// 初始化加载第一页
loadMore();
</script>
</body>
</html>
二、Vue3 核心组件(组合式API)
1. 带插槽的卡片组件(Card.vue)
<template>
<div class="card" :style="{ width: width + 'px' }">
<!-- 头部插槽 -->
<div class="card-header" v-if="$slots.header">
<slot name="header"></slot>
</div>
<!-- 主体插槽 -->
<div class="card-body">
<slot></slot>
</div>
<!-- 底部插槽 -->
<div class="card-footer" v-if="$slots.footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<script setup>
import { defineProps } from 'vue';
// 定义props
const props = defineProps({
width: {
type: Number,
default: 300
}
});
</script>
<style scoped>
.card {
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.card-header {
padding: 16px;
border-bottom: 1px solid #e5e7eb;
font-weight: 600;
}
.card-body {
padding: 16px;
}
.card-footer {
padding: 16px;
border-top: 1px solid #e5e7eb;
color: #666;
}
</style>
<!-- 使用示例(App.vue) -->
<template>
<div style="padding: 50px;">
<Card :width="400">
<template #header>卡片标题</template>
<div>卡片主体内容,默认插槽</div>
<template #footer>卡片底部 - 2025/12/27</template>
</Card>
</div>
</template>
<script setup>
import Card from './Card.vue';
</script>
2. 模态框组件(Modal.vue)
<template>
<!-- 遮罩层 -->
<div class="modal-mask" v-if="visible" @click="handleMaskClick"></div>
<!-- 弹窗主体 -->
<div class="modal-container" v-if="visible">
<div class="modal-content" @click.stop>
<!-- 头部 -->
<div class="modal-header">
<h3 class="modal-title">{{ title }}</h3>
<button class="modal-close" @click="handleClose">×</button>
</div>
<!-- 主体 -->
<div class="modal-body">
<slot></slot>
</div>
<!-- 底部 -->
<div class="modal-footer" v-if="$slots.footer">
<slot name="footer"></slot>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
const props = defineProps({
visible: {
type: Boolean,
default: false
},
title: {
type: String,
default: '提示'
},
closeOnMask: {
type: Boolean,
default: true
}
});
const emit = defineEmits(['close']);
// 关闭弹窗
const handleClose = () => {
emit('close');
};
// 点击遮罩关闭
const handleMaskClick = () => {
if (props.closeOnMask) {
handleClose();
}
};
</script>
<style scoped>
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1000;
}
.modal-container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1001;
}
.modal-content {
width: 400px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.modal-header {
padding: 16px;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-title {
margin: 0;
font-size: 16px;
}
.modal-close {
background: transparent;
border: none;
font-size: 20px;
cursor: pointer;
color: #999;
}
.modal-close:hover {
color: #333;
}
.modal-body {
padding: 16px;
}
.modal-footer {
padding: 16px;
border-top: 1px solid #e5e7eb;
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>
<!-- 使用示例(App.vue) -->
<template>
<div style="padding: 50px;">
<button @click="modalVisible = true">打开弹窗</button>
<Modal
:visible="modalVisible"
title="自定义标题"
@close="modalVisible = false"
>
<div>弹窗内容</div>
<template #footer>
<button @click="modalVisible = false">取消</button>
<button @click="handleConfirm">确认</button>
</template>
</Modal>
</div>
</template>
<script setup>
import { ref } from 'vue';
import Modal from './Modal.vue';
const modalVisible = ref(false);
const handleConfirm = () => {
alert('确认操作');
modalVisible.value = false;
};
</script>
3. 动态表单(可新增/删除表单项)
<template>
<div class="dynamic-form" style="padding: 50px;">
<div v-for="(item, index) in formList" :key="index" class="form-item">
<input
v-model="item.value"
placeholder="请输入内容"
style="padding: 8px; margin-right: 10px;"
>
<button @click="removeItem(index)" style="color: red;">删除</button>
</div>
<button @click="addItem" style="margin-top: 10px;">新增表单项</button>
<button @click="submitForm" style="margin-left: 10px;">提交</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
// 初始化表单列表
const formList = ref([{ value: '' }]);
// 新增表单项
const addItem = () => {
formList.value.push({ value: '' });
};
// 删除表单项
const removeItem = (index) => {
if (formList.value.length <= 1) {
alert('至少保留一个表单项');
return;
}
formList.value.splice(index, 1);
};
// 提交表单
const submitForm = () => {
const values = formList.value.map(item => item.value.trim()).filter(Boolean);
if (values.length === 0) {
alert('请填写至少一个表单项');
return;
}
console.log('表单数据:', formList.value);
alert(`提交成功:${JSON.stringify(formList.value)}`);
};
</script>
4. 父子组件通信(props + emit)
<!-- 子组件 Child.vue -->
<template>
<div class="child" style="padding: 20px; border: 1px solid #ccc; margin-top: 20px;">
<h3>子组件</h3>
<p>父组件传递的值:{{ parentMsg }}</p>
<button @click="handleSend">向父组件传值</button>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
// 接收父组件props
const props = defineProps({
parentMsg: {
type: String,
default: ''
}
});
// 定义emit事件
const emit = defineEmits(['child-send']);
// 向父组件传值
const handleSend = () => {
emit('child-send', '我是子组件传递的消息');
};
</script>
<!-- 父组件 App.vue -->
<template>
<div style="padding: 50px;">
<h3>父组件</h3>
<input
v-model="parentMsg"
placeholder="请输入要传递给子组件的内容"
style="padding: 8px;"
>
<Child
:parent-msg="parentMsg"
@child-send="handleChildMsg"
/>
<p style="margin-top: 10px;">子组件传递的值:{{ childMsg }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
import Child from './Child.vue';
const parentMsg = ref('初始消息');
const childMsg = ref('');
// 接收子组件消息
const handleChildMsg = (msg) => {
childMsg.value = msg;
};
</script>
5. 自定义v-model
<!-- 子组件 CustomInput.vue -->
<template>
<input
:value="modelValue"
@input="handleInput"
placeholder="自定义v-model"
style="padding: 8px;"
>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
// 接收v-model的值(默认是modelValue)
const props = defineProps({
modelValue: {
type: String,
default: ''
}
});
// 触发更新事件(默认是update:modelValue)
const emit = defineEmits(['update:modelValue']);
const handleInput = (e) => {
emit('update:modelValue', e.target.value);
};
</script>
<!-- 使用示例(App.vue) -->
<template>
<div style="padding: 50px;">
<CustomInput v-model="inputValue" />
<p>输入的值:{{ inputValue }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
import CustomInput from './CustomInput.vue';
const inputValue = ref('');
</script>
6. 倒计时组件
<template>
<div class="countdown" style="padding: 50px; font-size: 20px;">
<button
@click="startCountdown"
:disabled="counting"
style="padding: 8px 16px; margin-right: 10px;"
>
{{ counting ? `${count}秒后重新获取` : '开始倒计时' }}
</button>
</div>
</template>
<script setup>
import { ref, onUnmounted } from 'vue';
const count = ref(60);
const counting = ref(false);
let timer = null;
// 开始倒计时
const startCountdown = () => {
if (counting.value) return;
counting.value = true;
timer = setInterval(() => {
count.value--;
if (count.value <= 0) {
clearInterval(timer);
count.value = 60;
counting.value = false;
}
}, 1000);
};
// 组件卸载时清除定时器
onUnmounted(() => {
if (timer) clearInterval(timer);
});
</script>
7. 购物车组件
<template>
<div class="cart" style="width: 600px; margin: 50px auto;">
<h3>购物车</h3>
<table border="1" width="100%" cellpadding="10" cellspacing="0">
<thead>
<tr>
<th>商品名称</th>
<th>单价</th>
<th>数量</th>
<th>小计</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in cartList" :key="item.id">
<td>{{ item.name }}</td>
<td>¥{{ item.price }}</td>
<td>
<button @click="changeCount(item, -1)" :disabled="item.count <= 1">-</button>
<span style="margin: 0 10px;">{{ item.count }}</span>
<button @click="changeCount(item, 1)">+</button>
</td>
<td>¥{{ (item.price * item.count).toFixed(2) }}</td>
<td>
<button @click="removeItem(index)" style="color: red;">删除</button>
</td>
</tr>
</tbody>
</table>
<div style="margin-top: 20px; text-align: right;">
总价:¥{{ totalPrice.toFixed(2) }}
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
// 初始化购物车数据
const cartList = ref([
{ id: 1, name: '商品1', price: 99.9, count: 1 },
{ id: 2, name: '商品2', price: 199.9, count: 1 },
{ id: 3, name: '商品3', price: 299.9, count: 1 }
]);
// 改变数量
const changeCount = (item, num) => {
item.count += num;
if (item.count < 1) item.count = 1;
};
// 删除商品
const removeItem = (index) => {
cartList.value.splice(index, 1);
};
// 计算总价
const totalPrice = computed(() => {
return cartList.value.reduce((sum, item) => {
return sum + item.price * item.count;
}, 0);
});
</script>
8. TodoList组件
<template>
<div class="todo-list" style="width: 400px; margin: 50px auto;">
<h3>TodoList</h3>
<!-- 新增 -->
<div style="display: flex; gap: 10px; margin-bottom: 20px;">
<input
v-model="inputValue"
placeholder="请输入待办事项"
style="flex: 1; padding: 8px;"
@keyup.enter="addTodo"
>
<button @click="addTodo">添加</button>
</div>
<!-- 列表 -->
<div class="todo-items">
<div
v-for="(todo, index) in todoList"
:key="todo.id"
style="display: flex; align-items: center; padding: 8px; border-bottom: 1px solid #eee;"
>
<input
type="checkbox"
v-model="todo.done"
style="margin-right: 10px;"
>
<span :style="{ textDecoration: todo.done ? 'line-through' : 'none', color: todo.done ? '#999' : '#333' }">
{{ todo.content }}
</span>
<button
@click="deleteTodo(index)"
style="margin-left: auto; color: red; border: none; background: transparent; cursor: pointer;"
>
删除
</button>
</div>
</div>
<!-- 统计 -->
<div style="margin-top: 20px; display: flex; justify-content: space-between;">
<span>已完成:{{ doneCount }} / 总数:{{ todoList.length }}</span>
<button @click="clearDone" style="color: #666;">清空已完成</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const inputValue = ref('');
const todoList = ref([
{ id: 1, content: '学习Vue', done: false },
{ id: 2, content: '手撕组件', done: true }
]);
// 添加待办
const addTodo = () => {
const content = inputValue.value.trim();
if (!content) return;
todoList.value.push({
id: Date.now(),
content,
done: false
});
inputValue.value = '';
};
// 删除待办
const deleteTodo = (index) => {
todoList.value.splice(index, 1);
};
// 已完成数量
const doneCount = computed(() => {
return todoList.value.filter(todo => todo.done).length;
});
// 清空已完成
const clearDone = () => {
todoList.value = todoList.value.filter(todo => !todo.done);
};
</script>
三、React 核心组件(Hooks)
1. 带插槽的卡片组件(Card.jsx)
import React from 'react';
import './Card.css';
const Card = ({ width = 300, children, header, footer }) => {
return (
<div className="card" style={{ width: `${width}px` }}>
{header && <div className="card-header">{header}</div>}
<div className="card-body">{children}</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
);
};
export default Card;
// Card.css
.card {
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.card-header {
padding: 16px;
border-bottom: 1px solid #e5e7eb;
font-weight: 600;
}
.card-body {
padding: 16px;
}
.card-footer {
padding: 16px;
border-top: 1px solid #e5e7eb;
color: #666;
}
// 使用示例(App.jsx)
import React from 'react';
import Card from './Card';
function App() {
return (
<div style={{ padding: '50px' }}>
<Card width={400} header="卡片标题" footer="卡片底部 - 2025/12/27">
<div>卡片主体内容</div>
</Card>
</div>
);
}
export default App;
2. 模态框组件(Modal.jsx)
import React from 'react';
import './Modal.css';
const Modal = ({ visible, title = '提示', closeOnMask = true, onClose, children, footer }) => {
if (!visible) return null;
// 点击遮罩关闭
const handleMaskClick = () => {
if (closeOnMask) {
onClose?.();
}
};
return (
<>
<div className="modal-mask" onClick={handleMaskClick}></div>
<div className="modal-container">
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h3 className="modal-title">{title}</h3>
<button className="modal-close" onClick={onClose}>×</button>
</div>
<div className="modal-body">{children}</div>
{footer && <div className="modal-footer">{footer}</div>}
</div>
</div>
</>
);
};
export default Modal;
// Modal.css
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1000;
}
.modal-container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1001;
}
.modal-content {
width: 400px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.modal-header {
padding: 16px;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-title {
margin: 0;
font-size: 16px;
}
.modal-close {
background: transparent;
border: none;
font-size: 20px;
cursor: pointer;
color: #999;
}
.modal-close:hover {
color: #333;
}
.modal-body {
padding: 16px;
}
.modal-footer {
padding: 16px;
border-top: 1px solid #e5e7eb;
display: flex;
justify-content: flex-end;
gap: 10px;
}
// 使用示例(App.jsx)
import React, { useState } from 'react';
import Modal from './Modal';
function App() {
const [modalVisible, setModalVisible] = useState(false);
const handleConfirm = () => {
alert('确认操作');
setModalVisible(false);
};
return (
<div style={{ padding: '50px' }}>
<button onClick={() => setModalVisible(true)}>打开弹窗</button>
<Modal
visible={modalVisible}
title="自定义标题"
onClose={() => setModalVisible(false)}
footer={
<>
<button onClick={() => setModalVisible(false)}>取消</button>
<button onClick={handleConfirm}>确认</button>
</>
}
>
<div>弹窗内容</div>
</Modal>
</div>
);
}
export default App;
3. 动态表单(可新增/删除表单项)
import React, { useState } from 'react';
function DynamicForm() {
const [formList, setFormList] = useState([{ value: '' }]);
// 新增表单项
const addItem = () => {
setFormList([...formList, { value: '' }]);
};
// 删除表单项
const removeItem = (index) => {
if (formList.length <= 1) {
alert('至少保留一个表单项');
return;
}
const newList = [...formList];
newList.splice(index, 1);
setFormList(newList);
};
// 输入变更
const handleInputChange = (index, value) => {
const newList = [...formList];
newList[index].value = value;
setFormList(newList);
};
// 提交表单
const submitForm = () => {
const values = formList.map(item => item.value.trim()).filter(Boolean);
if (values.length === 0) {
alert('请填写至少一个表单项');
return;
}
console.log('表单数据:', formList);
alert(`提交成功:${JSON.stringify(formList)}`);
};
return (
<div style={{ padding: '50px' }}>
{formList.map((item, index) => (
<div key={index} style={{ marginBottom: '10px' }}>
<input
value={item.value}
onChange={(e) => handleInputChange(index, e.target.value)}
placeholder="请输入内容"
style={{ padding: '8px', marginRight: '10px' }}
/>
<button onClick={() => removeItem(index)} style={{ color: 'red' }}>删除</button>
</div>
))}
<button onClick={addItem} style={{ marginRight: '10px' }}>新增表单项</button>
<button onClick={submitForm}>提交</button>
</div>
);
}
export default DynamicForm;
4. 父子组件通信(props + 回调)
// 子组件 Child.jsx
import React from 'react';
const Child = ({ parentMsg, onChildSend }) => {
const handleSend = () => {
onChildSend('我是子组件传递的消息');
};
return (
<div style={{ padding: '20px', border: '1px solid #ccc', marginTop: '20px' }}>
<h3>子组件</h3>
<p>父组件传递的值:{parentMsg}</p>
<button onClick={handleSend}>向父组件传值</button>
</div>
);
};
export default Child;
// 父组件 App.jsx
import React, { useState } from 'react';
import Child from './Child';
function App() {
const [parentMsg, setParentMsg] = useState('初始消息');
const [childMsg, setChildMsg] = useState('');
const handleChildMsg = (msg) => {
setChildMsg(msg);
};
return (
<div style={{ padding: '50px' }}>
<h3>父组件</h3>
<input
value={parentMsg}
onChange={(e) => setParentMsg(e.target.value)}
placeholder="请输入要传递给子组件的内容"
style={{ padding: '8px' }}
/>
<Child parentMsg={parentMsg} onChildSend={handleChildMsg} />
<p style={{ marginTop: '10px' }}>子组件传递的值:{childMsg}</p>
</div>
);
}
export default App;
5. 双向绑定(useState + onChange)
import React, { useState } from 'react';
function CustomInput() {
const [inputValue, setInputValue] = useState('');
return (
<div style={{ padding: '50px' }}>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="双向绑定示例"
style={{ padding: '8px' }}
/>
<p>输入的值:{inputValue}</p>
</div>
);
}
export default CustomInput;
6. 倒计时组件
import React, { useState, useEffect } from 'react';
function Countdown() {
const [count, setCount] = useState(60);
const [counting, setCounting] = useState(false);
// 清除定时器
useEffect(() => {
let timer = null;
if (counting) {
timer = setInterval(() => {
setCount(prev => {
if (prev <= 1) {
setCounting(false);
return 60;
}
return prev - 1;
});
}, 1000);
}
return () => clearInterval(timer);
}, [counting]);
const startCountdown = () => {
if (!counting) {
setCounting(true);
}
};
return (
<div style={{ padding: '50px', fontSize: '20px' }}>
<button
onClick={startCountdown}
disabled={counting}
style={{ padding: '8px 16px', marginRight: '10px' }}
>
{counting ? `${count}秒后重新获取` : '开始倒计时'}
</button>
</div>
);
}
export default Countdown;
7. 购物车组件
import React, { useState, useMemo } from 'react';
function Cart() {
const [cartList, setCartList] = useState([
{ id: 1, name: '商品1', price: 99.9, count: 1 },
{ id: 2, name: '商品2', price: 199.9, count: 1 },
{ id: 3, name: '商品3', price: 299.9, count: 1 }
]);
// 改变数量
const changeCount = (id, num) => {
setCartList(
cartList.map(item => {
if (item.id === id) {
const newCount = item.count + num;
return { ...item, count: newCount < 1 ? 1 : newCount };
}
return item;
})
);
};
// 删除商品
const removeItem = (id) => {
setCartList(cartList.filter(item => item.id !== id));
};
// 计算总价
const totalPrice = useMemo(() => {
return cartList.reduce((sum, item) => sum + item.price * item.count, 0);
}, [cartList]);
return (
<div style={{ width: '600px', margin: '50px auto' }}>
<h3>购物车</h3>
<table border="1" width="100%" cellpadding="10" cellspacing="0">
<thead>
<tr>
<th>商品名称</th>
<th>单价</th>
<th>数量</th>
<th>小计</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{cartList.map(item => (
<tr key={item.id}>
<td>{item.name}</td>
<td>¥{item.price}</td>
<td>
<button onClick={() => changeCount(item.id, -1)} disabled={item.count <= 1}>-</button>
<span style={{ margin: '0 10px' }}>{item.count}</span>
<button onClick={() => changeCount(item.id, 1)}>+</button>
</td>
<td>¥{(item.price * item.count).toFixed(2)}</td>
<td>
<button onClick={() => removeItem(item.id)} style={{ color: 'red' }}>删除</button>
</td>
</tr>
))}
</tbody>
</table>
<div style={{ marginTop: '20px', textAlign: 'right' }}>
总价:¥{totalPrice.toFixed(2)}
</div>
</div>
);
}
export default Cart;
8. TodoList组件
import React, { useState, useMemo } from 'react';
function TodoList() {
const [inputValue, setInputValue] = useState('');
const [todoList, setTodoList] = useState([
{ id: 1, content: '学习React', done: false },
{ id: 2, content: '手撕组件', done: true }
]);
// 添加待办
const addTodo = () => {
const content = inputValue.trim();
if (!content) return;
setTodoList([
...todoList,
{ id: Date.now(), content, done: false }
]);
setInputValue('');
};
// 删除待办
const deleteTodo = (id) => {
setTodoList(todoList.filter(item => item.id !== id));
};
// 切换完成状态
const toggleDone = (id) => {
setTodoList(
todoList.map(item =>
item.id === id ? { ...item, done: !item.done } : item
)
);
};
// 清空已完成
const clearDone = () => {
setTodoList(todoList.filter(item => !item.done));
};
// 已完成数量
const doneCount = useMemo(() => {
return todoList.filter(item => item.done).length;
}, [todoList]);
return (
<div style={{ width: '400px', margin: '50px auto' }}>
<h3>TodoList</h3>
{/* 新增 */}
<div style={{ display: 'flex', gap: '10px', marginBottom: '20px' }}>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="请输入待办事项"
style={{ flex: 1, padding: '8px' }}
onKeyDown={(e) => e.key === 'Enter' && addTodo()}
/>
<button onClick={addTodo}>添加</button>
</div>
{/* 列表 */}
<div>
{todoList.map(item => (
<div
key={item.id}
style={{ display: 'flex', alignItems: 'center', padding: '8px', borderBottom: '1px solid #eee' }}
>
<input
type="checkbox"
checked={item.done}
onChange={() => toggleDone(item.id)}
style={{ marginRight: '10px' }}
/>
<span
style={{
textDecoration: item.done ? 'line-through' : 'none',
color: item.done ? '#999' : '#333'
}}
>
{item.content}
</span>
<button
onClick={() => deleteTodo(item.id)}
style={{ marginLeft: 'auto', color: 'red', border: 'none', background: 'transparent', cursor: 'pointer' }}
>
删除
</button>
</div>
))}
</div>
{/* 统计 */}
<div style={{ marginTop: '20px', display: 'flex', justifyContent: 'space-between' }}>
<span>已完成:{doneCount} / 总数:{todoList.length}</span>
<button onClick={clearDone} style={{ color: '#666' }}>清空已完成</button>
</div>
</div>
);
}
export default TodoList;
核心说明
- 所有代码均保证可直接运行,无第三方依赖(仅Vue/React核心库)
- 原生JS部分:聚焦核心逻辑,样式精简但完整,适配常见场景
- Vue部分:基于Vue3组合式API(setup语法),符合最新开发规范
- React部分:基于Hooks(useState/useEffect/useMemo),函数式组件写法
- 每个组件都包含核心功能+使用示例,可直接手撕到面试场景中
面试手撕时,可根据时间精简样式,重点实现核心逻辑(如轮播图的切换、表单验证的规则、双向绑定的核心原理等)。
总结
到此这篇关于前端面试手撕合集之纯HTML、Vue和React组件的文章就介绍到这了,更多相关前端纯HTML、Vue和React组件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!


最新评论