2025.11.07面试


2025.11.07面试

jQuery选择的标签和document获取的标签有什么区别

  • jQuery选择器返回的是jQuery对象,它封装了DOM元素并提供了一套丰富的API方法,支持链式调用,但性能相对较低,体积较大。

  • document方法返回的是原生DOM元素或NodeList,需要使用原生JavaScript方法操作,性能更好,但代码相对繁琐,需要处理浏览器兼容性问题。

核心区别

特性 jQuery 选择 document 选择
返回类型 jQuery 对象 DOM 元素/NodeList
方法调用 jQuery 方法 原生 DOM 方法
链式调用 支持 不支持
浏览器兼容 自动处理 需要手动处理
性能 相对较慢 更快
内存占用 更大 更小

代码示例

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
// 1. 选择元素
// jQuery
const $elements = $('.my-class'); // 返回 jQuery 对象
// document
const elements = document.querySelectorAll('.my-class'); // 返回 NodeList

// 2. 操作元素
// jQuery - 链式调用
$('.my-class')
.css('color', 'red')
.addClass('active')
.on('click', handler);

// document - 需要遍历
document.querySelectorAll('.my-class').forEach(element => {
element.style.color = 'red';
element.classList.add('active');
element.addEventListener('click', handler);
});

// 3. 事件处理
// jQuery - 统一的事件处理
$('.btn').on('click', function() {
console.log($(this).text()); // this 指向 DOM 元素
});

// document - 原生事件
document.querySelector('.btn').addEventListener('click', function() {
console.log(this.textContent); // this 指向 DOM 元素
});

// 4. 属性操作
// jQuery
$('#input').val(); // 获取值
$('#input').val('new value'); // 设置值

// document
document.getElementById('input').value; // 获取值
document.getElementById('input').value = 'new value'; // 设置值

// 5. 创建元素
// jQuery
const $newElement = $('<div>', {
class: 'new-div',
text: 'Hello World'
});

// document
const newElement = document.createElement('div');
newElement.className = 'new-div';
newElement.textContent = 'Hello World';

相互转换

1
2
3
4
5
6
7
8
// jQuery → DOM
const $div = $('.my-div');
const domElement = $div[0]; // 方法1
const domElement = $div.get(0); // 方法2

// DOM → jQuery
const div = document.querySelector('.my-div');
const $div = $(div);

使用场景建议

  • jQuery:快速开发、需要兼容老浏览器、大量DOM操作

  • document:现代浏览器、性能要求高、项目体积敏感

怎么实现vue中的v-model

v-model的本质是语法糖,它实际上是value属性绑定和input事件监听的组合。

实现原理:

  • 在组件内部通过props接收modelValue(Vue3)或value(Vue2)

  • 当数据变化时,通过$emit触发update:modelValue(Vue3)或input(Vue2)事件

  • 父组件通过v-model自动完成双向数据绑定

关键点: 要正确定义props和emits,确保数据流的双向同步。

v-model 的本质

1
2
3
4
5
6
7
8
<!-- 这行代码 -->
<input v-model="message">

<!-- 实际上是以下代码的语法糖 -->
<input
:value="message"
@input="message = $event.target.value"
>

自定义组件实现 v-model

Vue 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
<!-- 自定义输入组件 CustomInput.vue -->
<template>
<input
:value="value"
@input="$emit('input', $event.target.value)"
@blur="$emit('blur')"
>
</template>

<script>
export default {
props: ['value'],
// 或者明确声明
// model: {
// prop: 'value',
// event: 'input'
// }
}
</script>

<!-- 使用 -->
<template>
<div>
<CustomInput v-model="username" />
<!-- 等价于 -->
<CustomInput :value="username" @input="username = $event" />
</div>
</template>

vue3实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 自定义输入组件 CustomInput.vue -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
</template>

<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

<!-- 使用 -->
<template>
<CustomInput v-model="username" />
</template>

表单提交优化(多次提交表单)

防重复提交的核心思路是状态控制:

前端层面:

  1. 按钮禁用:提交期间禁用按钮,防止重复点击

  2. 加载状态:显示loading状态,提升用户体验

  3. 请求标记:为每个请求生成唯一标识,确保处理最新请求

  4. 防抖处理:设置提交间隔,避免短时间多次提交

后端层面:

  1. Token机制:每次请求验证唯一token

  2. 幂等性设计:相同请求只处理一次

  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
45
46
47
48
49
50
51
52
53
<template>
<form @submit.prevent="handleSubmit">
<input v-model="formData.name" required>
<button
type="submit"
:disabled="submitting"
:class="{ 'loading': submitting }"
>
{{ submitting ? '提交中...' : '提交' }}
</button>
</form>
</template>

<script setup>
import { ref, reactive } from 'vue'

const formData = reactive({
name: '',
email: ''
})
const submitting = ref(false)

const handleSubmit = async () => {
if (submitting.value) return

submitting.value = true
try {
// 模拟 API 调用
await submitForm(formData)
alert('提交成功!')
// 重置表单
Object.assign(formData, { name: '', email: '' })
} catch (error) {
console.error('提交失败:', error)
alert('提交失败,请重试')
} finally {
submitting.value = false
}
}

const submitForm = (data) => {
return new Promise((resolve) => {
setTimeout(() => resolve({ success: true }), 2000)
})
}
</script>

<style scoped>
.loading {
opacity: 0.6;
cursor: not-allowed;
}
</style>

方案二:防抖函数

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
<script setup>
import { ref, reactive } from 'vue'

const formData = reactive({ name: '', email: '' })
const submitting = ref(false)

// 防抖函数
const debounce = (func, wait) => {
let timeout
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout)
func(...args)
}
clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
}

const handleSubmit = debounce(async () => {
if (submitting.value) return

submitting.value = true
try {
await submitForm(formData)
alert('提交成功!')
} catch (error) {
alert('提交失败')
} finally {
submitting.value = false
}
}, 1000) // 1秒内只能提交一次
</script>

方案三:全局状态管理

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
// stores/submitStore.js
import { defineStore } from 'pinia'

export const useSubmitStore = defineStore('submit', {
state: () => ({
submittingForms: new Set()
}),

actions: {
startSubmit(formId) {
if (this.submittingForms.has(formId)) {
throw new Error('表单正在提交中')
}
this.submittingForms.add(formId)
},

endSubmit(formId) {
this.submittingForms.delete(formId)
},

isSubmitting(formId) {
return this.submittingForms.has(formId)
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup>
import { useSubmitStore } from '@/stores/submitStore'

const submitStore = useSubmitStore()
const formId = 'user-registration'

const handleSubmit = async () => {
if (submitStore.isSubmitting(formId)) {
alert('请勿重复提交')
return
}

try {
submitStore.startSubmit(formId)
await submitForm(formData)
alert('提交成功!')
} catch (error) {
alert('提交失败')
} finally {
submitStore.endSubmit(formId)
}
}
</script>

自己实现一个带搜素的下拉框

功能设计:

  • 输入框支持输入搜索

  • 下拉列表展示过滤结果

  • 键盘导航(上下箭头、回车选择)

  • 点击外部自动关闭

技术实现要点:

  1. 数据流:使用v-model实现双向绑定

  2. 搜索过滤:支持本地过滤和远程搜索两种模式

  3. 用户体验:防抖搜索、加载状态、空状态提示

  4. 交互优化:点击外部关闭、键盘快捷键、动画效果

组件设计:

  • 合理的props设计(options、filterable、remote等)

  • 完整的事件体系(change、search等)

  • 支持插槽自定义选项渲染

  • 良好的可访问性支持

核心难点: 搜索性能优化和键盘交互的完整实现。

完整实现

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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
<template>
<div class="custom-select" ref="selectRef">
<!-- 输入框 -->
<div
class="select-trigger"
:class="{ 'is-open': isOpen, 'has-value': selectedOption }"
@click="toggleDropdown"
>
<input
ref="inputRef"
type="text"
v-model="searchText"
:placeholder="selectedOption ? selectedOption.label : placeholder"
@input="handleSearch"
@keydown="handleKeydown"
@focus="openDropdown"
/>
<span class="select-arrow">▼</span>
<span
v-if="clearable && selectedOption"
class="clear-btn"
@click.stop="clearSelection"
>
×
</span>
</div>

<!-- 下拉菜单 -->
<div v-show="isOpen" class="select-dropdown">
<!-- 搜索状态 -->
<div v-if="loading" class="dropdown-loading">
搜索中...
</div>

<!-- 选项列表 -->
<div
v-for="(option, index) in filteredOptions"
:key="option.value"
class="dropdown-option"
:class="{
'selected': isSelected(option),
'highlighted': highlightedIndex === index
}"
@click="selectOption(option)"
@mouseenter="highlightedIndex = index"
>
<slot name="option" :option="option">
<span class="option-label">{{ option.label }}</span>
</slot>
<span v-if="isSelected(option)" class="check-mark">✓</span>
</div>

<!-- 空状态 -->
<div v-if="!filteredOptions.length" class="dropdown-empty">
{{ searchText ? '未找到匹配项' : '暂无数据' }}
</div>
</div>
</div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'

// Props
const props = defineProps({
modelValue: {
type: [String, Number, Object],
default: null
},
options: {
type: Array,
required: true,
default: () => []
},
placeholder: {
type: String,
default: '请选择...'
},
clearable: {
type: Boolean,
default: false
},
filterable: {
type: Boolean,
default: true
},
remote: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
},
debounce: {
type: Number,
default: 300
}
})

// Emits
const emit = defineEmits(['update:modelValue', 'change', 'search'])

// 响应式数据
const isOpen = ref(false)
const searchText = ref('')
const highlightedIndex = ref(-1)
const selectRef = ref(null)
const inputRef = ref(null)

// 计算属性
const selectedOption = computed(() => {
return props.options.find(opt => opt.value === props.modelValue) || null
})

const filteredOptions = computed(() => {
if (!props.filterable || !searchText.value) {
return props.options
}

const searchLower = searchText.value.toLowerCase()
return props.options.filter(option =>
option.label.toLowerCase().includes(searchLower)
)
})

// 方法
const isSelected = (option) => {
return option.value === props.modelValue
}

const toggleDropdown = () => {
isOpen.value = !isOpen.value
if (isOpen.value) {
highlightedIndex.value = -1
nextTick(() => {
inputRef.value?.focus()
})
}
}

const openDropdown = () => {
isOpen.value = true
highlightedIndex.value = -1
}

const closeDropdown = () => {
isOpen.value = false
searchText.value = ''
highlightedIndex.value = -1
}

const selectOption = (option) => {
emit('update:modelValue', option.value)
emit('change', option)
closeDropdown()
}

const clearSelection = () => {
emit('update:modelValue', null)
emit('change', null)
searchText.value = ''
}

// 防抖搜索
let searchTimer = null
const handleSearch = () => {
if (props.remote) {
clearTimeout(searchTimer)
searchTimer = setTimeout(() => {
emit('search', searchText.value)
}, props.debounce)
}
}

// 键盘导航
const handleKeydown = (event) => {
if (!isOpen.value) return

switch (event.key) {
case 'ArrowDown':
event.preventDefault()
highlightedIndex.value =
highlightedIndex.value >= filteredOptions.value.length - 1
? 0
: highlightedIndex.value + 1
break

case 'ArrowUp':
event.preventDefault()
highlightedIndex.value =
highlightedIndex.value <= 0
? filteredOptions.value.length - 1
: highlightedIndex.value - 1
break

case 'Enter':
event.preventDefault()
if (highlightedIndex.value >= 0 &&
filteredOptions.value[highlightedIndex.value]) {
selectOption(filteredOptions.value[highlightedIndex.value])
}
break

case 'Escape':
closeDropdown()
break

case 'Tab':
closeDropdown()
break
}
}

// 点击外部关闭
const handleClickOutside = (event) => {
if (selectRef.value && !selectRef.value.contains(event.target)) {
closeDropdown()
}
}

// 生命周期
onMounted(() => {
document.addEventListener('click', handleClickOutside)
})

onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
clearTimeout(searchTimer)
})
</script>

<style scoped>
.custom-select {
position: relative;
width: 100%;
max-width: 300px;
}

.select-trigger {
position: relative;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 8px 32px 8px 12px;
background: white;
cursor: pointer;
transition: border-color 0.3s;
}

.select-trigger:hover {
border-color: #c0c4cc;
}

.select-trigger.is-open {
border-color: #409eff;
}

.select-trigger input {
border: none;
outline: none;
width: 100%;
background: transparent;
cursor: pointer;
font-size: 14px;
}

.select-trigger input:focus {
cursor: text;
}

.select-arrow {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
transition: transform 0.3s;
color: #c0c4cc;
}

.select-trigger.is-open .select-arrow {
transform: translateY(-50%) rotate(180deg);
}

.clear-btn {
position: absolute;
right: 30px;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
color: #c0c4cc;
font-size: 16px;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
}

.clear-btn:hover {
color: #909399;
}

.select-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
max-height: 200px;
overflow-y: auto;
background: white;
border: 1px solid #e4e7ed;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
z-index: 1000;
margin-top: 4px;
}

.dropdown-option {
padding: 8px 12px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
transition: background-color 0.3s;
}

.dropdown-option:hover {
background-color: #f5f7fa;
}

.dropdown-option.selected {
color: #409eff;
background-color: #f0f9ff;
}

.dropdown-option.highlighted {
background-color: #f5f7fa;
}

.dropdown-loading,
.dropdown-empty {
padding: 8px 12px;
color: #c0c4cc;
text-align: center;
font-size: 14px;
}

.check-mark {
color: #409eff;
font-weight: bold;
}
</style>

使用示例

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
<template>
<div>
<h3>选择的值: {{ selectedValue }}</h3>

<!-- 基础使用 -->
<CustomSelect
v-model="selectedValue"
:options="options"
placeholder="请选择水果"
/>

<!-- 可搜索 -->
<CustomSelect
v-model="selectedValue"
:options="options"
placeholder="搜索水果..."
filterable
clearable
/>

<!-- 远程搜索 -->
<CustomSelect
v-model="selectedValue"
:options="options"
placeholder="远程搜索..."
filterable
remote
:loading="loading"
@search="handleRemoteSearch"
/>
</div>
</template>

<script setup>
import { ref } from 'vue'

const selectedValue = ref('')
const loading = ref(false)
const options = ref([
{ label: '苹果', value: 'apple' },
{ label: '香蕉', value: 'banana' },
{ label: '橙子', value: 'orange' },
{ label: '葡萄', value: 'grape' },
{ label: '西瓜', value: 'watermelon' }
])

const handleRemoteSearch = async (query) => {
loading.value = true
// 模拟远程搜索
setTimeout(() => {
options.value = [
{ label: `搜索结果: ${query}`, value: 'search-result' },
{ label: '其他选项', value: 'other' }
]
loading.value = false
}, 1000)
}
</script>

这个自定义下拉框组件具有以下特性:

  • 支持 v-model 双向绑定

  • 本地搜索和远程搜索

  • 键盘导航支持

  • 点击外部关闭

  • 清空选项

  • 加载状态

  • 自定义选项插槽

  • 防抖搜索