紧接着上篇文章。在结束了下午三点半的面试之后,六点迎来了我今天的第三场面试。此时我已经身体乏力、疲惫不堪了,没想到面试是这么消耗精力的一件事,需要集中精神全神贯注,说话方式也得和平时不一样,需要口齿伶俐,说白了就是得夹着嗓子说话(苦笑)。但好在这场面试答得比较好,第二天就给了offer。
好了废话不多说,一起来看看这份面试题吧。
1. vue3 的响应式原理
vue3的响应式原理主要是基于ES6打造的proxy来实现的。比如最常用的两个api,reactive 和 ref:
reactive 是借助proxy的代理作用,代理该引用类型的属性,当该属性被读取值时,返回该属性的值并且为该属性添加副作用函数;当该属性被修改值时,触发掉该属性的副作用函数
ref既可以代理原始类型也可以代理引用类型,当代理的是原始类型时,返回的是一个RefImpl的实例,通过类身上的get和set属性去读取值和修改值;当代理的是引用类型时,其实走的还是reactive的调用机制。
2. 聊聊虚拟dom
虚拟DOM是一个轻量级的JavaScript对象,是真实DOM的抽象表示,主要用于提升页面渲染性能,减少操作真实DOM结构的次数。因为操作真实DOM代价是非常昂贵的,会频繁引发回流与重绘。而采用虚拟DOM结构,当数据变化时,框架会生成一个新的虚拟DOM树,使用Diff算法将新旧两棵虚拟DOM树进行对比,找出需要更新的最小差异部分,将差异部分批量应用到真实DOM,减少渲染次数,并且让开发者只需要关注数据状态,无需手动操作DOM,大大地提升了开发效率,并且虚拟DOM还支持跨平台开发。
3. vue的两种路由方式
vue的路由方式有两种:hash路由和history路由,我已经写过文章了,详情请见这篇文章:
[]((笔记)前端路由1. 什么是前端路由 前端路由是通过url路径来匹配对应的代码块的这么一套机制 修改 url 后页面要更 - 掘金)
4. 浏览器的同源策略及跨域
浏览器的同源策略及跨域也是常考的问题之一了,JYM一定要在心里自己多复述几遍,确保能够清晰流利地讲给面试官听。面试是一个展示自己的过程,自己会的一定要大声流利地说出来,慢一点也没有关系,要让面试官清晰地知道我们是会的,让面试官给我们加分。
关于跨域的文章我也写过,在这里就不赘述了。跨域就请见这篇文章:
[> ]((笔记)什么是跨域?1. 什么是跨域? 想象一下,你住在一个小区里,小区有门禁系统,只允许本小区的住户自由进出。如果外来 - 掘金)
5. 浏览器的存储
浏览器的存储有四种,分别是:localStorage,sessionStorage、cookies和indexDB。它们的特点分别是:
localStorage是永久存储,大小在5MB-10MB左右,以字符串类型的键值对进行存储,不可跨域。
sessionStorage是本会话页面有效,大小也在5MB-10MB左右,以字符串类型的键值对进行存储,不可跨域。
cookies可以存放复杂的数据类型,大小4kb-5kb左右,只能由后端来设置,它在每次发送请求时都会自动携带在响应头中传给后端。
indexDB是一种客户端数据库,也能存放复杂数据类型,大小理论上无限大,根据你电脑的硬盘决定。
6. 浏览器的垃圾回收机制
这道题没有答出来,在那里胡编乱造(笑。
浏览器的垃圾回收机制(Garbage Collection, GC)是自动管理内存的核心机制,用于释放不再使用的对象所占用的内存,防止内存泄漏。以下是其核心原理和实现方式的详细说明:
1. 垃圾回收的基本目标
- 自动释放内存 :开发者无需手动管理内存(如
malloc/free
),由引擎自动追踪和回收"不可达"(Unreachable)的对象。 - 避免内存泄漏:防止因程序逻辑错误导致无用对象长期占用内存。
2. 关键算法
2.1 标记-清除(Mark and Sweep)
-
现代浏览器的主流算法(如 V8、SpiderMonkey)。
-
步骤:
- 标记阶段:从根对象(Roots,如全局变量、当前函数作用域链、活动线程等)出发,递归遍历所有可达对象,标记为"存活"。
- 清除阶段:遍历堆内存,回收未被标记的对象,将其内存返回空闲列表。
-
优点:解决循环引用问题(若两个对象互相引用,但无法从根访问,仍会被回收)。
2.2 引用计数(Reference Counting)
- 早期算法(如旧版 IE),因无法处理循环引用已基本被淘汰。
- 原理:记录每个对象的被引用次数,当引用数为 0 时立即回收。
- 缺陷 :循环引用会导致对象永远无法回收(如
A → B → A
)。
3. 优化策略
3.1 分代回收(Generational Collection)
-
内存分代:基于对象存活时间,将堆分为:
- 新生代(Young Generation) :存放短生命周期对象(如临时变量)。使用 Scavenge 算法(复制存活对象到新空间,清空旧空间)。
- 老生代(Old Generation) :存放长生命周期对象(如全局变量)。使用 标记-清除 或 标记-整理(Mark-Compact) (清除后整理内存碎片)。
-
晋升机制:新生代对象经历多次 GC 后仍存活,会被移到老生代。
3.2 增量标记(Incremental Marking)
- 问题:一次性标记所有对象会导致主线程卡顿(Stop-The-World)。
- 解决:将标记过程拆分为多个小步骤,穿插在 JavaScript 执行间隙。
3.3 惰性清理(Lazy Sweeping)
- 清理阶段延迟执行或分片执行,减少对主线程的影响。
3.4 并行/并发回收
- 并行:GC 任务在多个辅助线程并行执行(如 V8 的并行标记)。
- 并发:GC 线程与主线程同时运行(需处理读写竞争)。
4. 触发 GC 的时机
-
主动触发 :开发者可通过
window.gc()
(需启动 Chrome 时加--js-flags="--expose-gc"
)。 -
被动触发:
- 分配内存时,空闲内存不足。
- 脚本执行暂停期间(如事件循环空闲)。
- 根据时间或内存分配阈值动态调整。
5. 内存泄漏的常见原因
即使有 GC,仍需开发者注意:
- 意外的全局变量 (未声明的变量会挂载到
window
)。 - 未清除的定时器或事件监听 (如
setInterval
、addEventListener
)。 - 闭包引用外部变量(如函数内部持有外部大对象)。
- 脱离 DOM 的引用 (如
const elements = document.querySelectorAll('div')
,即使 DOM 移除,elements
仍引用节点)。
7. 组件传值
一、基础传值方式
1. Props / 自定义事件(父子通信)
- Props(父 → 子) :父组件通过属性向子组件传递数据。
js
<!-- 父组件 -->
<Child :title="parentTitle" />
<!-- 子组件 -->
<script setup>
const props = defineProps({
title: { type: String, required: true }
});
</script>
自定义事件(子 → 父) :子组件通过 emit
触发事件,父组件监听。
js
<!-- 子组件 -->
<button @click="$emit('update', newValue)">提交</button>
<!-- 父组件 -->
<Child @update="handleUpdate" />
2. v-model 双向绑定
1. 默认的单个 v-model
js
<!-- 父组件 -->
<Child v-model="message" />
<!-- 等价于 -->
<Child
:modelValue="message"
@update:modelValue="newValue => message = newValue"
/>
子组件需要定义 modelValue
prop 并触发 update:modelValue
事件:
js
<!-- 子组件 -->
<script setup>
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
const handleInput = (e) => {
emit('update:modelValue', e.target.value); // 将新值传给父组件
};
</script>
<template>
<input
:value="modelValue"
@input="handleInput"
/>
</template>
2. 多个 v-model
绑定
Vue3 允许同时绑定多个属性,例如同时绑定 name
和 age
:
js
<!-- 父组件 -->
<Child v-model:name="userName" v-model:age="userAge" />
<!-- 等价于 -->
<Child
:name="userName"
@update:name="newName => userName = newName"
:age="userAge"
@update:age="newAge => userAge = newAge"
/>
子组件需定义对应的 props 和事件:
js
<!-- 子组件 -->
<script setup>
const props = defineProps(['name', 'age']);
const emit = defineEmits(['update:name', 'update:age']);
const updateName = (val) => {
emit('update:name', val); // 触发 name 更新
};
const updateAge = (val) => {
emit('update:age', val); // 触发 age 更新
};
</script>
二、跨层级通信
3. Provide / Inject
- 父组件提供数据 :使用
provide
共享数据。
js
// 父组件(Composition API)
import { provide, ref } from 'vue';
const count = ref(0);
provide('countKey', count); // 提供响应式数据
子孙组件注入数据 :使用 inject
获取数据。
js
// 子组件
import { inject } from 'vue';
const injectedCount = inject('countKey', defaultValue);
4. 状态管理(Pinia / Vuex)
- Pinia(推荐) :Vue3 官方推荐的状态管理库。
js
// store/user.js
export const useUserStore = defineStore('user', {
state: () => ({ name: 'Alice' }),
actions: {
updateName(newName) { this.name = newName; }
}
});
// 组件中使用
const userStore = useUserStore();
userStore.updateName('Bob');
8. get 和 post 请求的区别
1. 设计目的
- GET
用于获取资源(幂等操作),不会修改服务器数据,仅请求指定信息。 - POST
用于提交数据到服务器(非幂等操作),通常会导致服务器状态变化(如创建或更新资源)。
2. 参数位置
- GET
参数通过**URL查询字符串(Query String)**传递,格式为?key1=value1&key2=value2
,直接可见。
示例:
https://api.example.com/users?id=123&name=John
- POST
参数通过**请求体(Request Body)**传递,对用户不可见(但抓包仍可查看)。
示例:
js
POST /users HTTP/1.1
Content-Type: application/json
{"name": "John", "age": 30}
3. 数据长度限制
- GET
受URL长度限制 (不同浏览器限制不同,通常为 2KB~8KB)。
原因: 参数附加在URL中,浏览器或服务器可能截断超长URL。 - POST
理论上无长度限制(数据在请求体中),但实际受服务器配置限制(如Nginx默认限制为1MB)。
4. 缓存与历史记录
- GET
会被浏览器主动缓存 (如静态资源),URL可能保留在浏览器历史记录或书签中。
场景: 重复访问同一URL时可直接使用缓存。 - POST
默认不缓存,浏览器不会保存请求体数据,也不会保留在历史记录中。
5. 安全性
- GET
参数明文暴露在URL中,可能被浏览器历史、服务器日志记录或他人直接看到,不适合传输敏感信息(如密码)。 - POST
参数在请求体中,相对更隐蔽,但若不使用HTTPS,数据仍可能被截获(如抓包工具)。
注意: 安全性取决于是否加密(HTTPS),而非请求方法本身。
9. v-if 和 v-show 的区别
v-if直接让dom结构消失,不加载,而v-show是让display属性为none。
如果需要频繁的切换组件最好使用v-show。
10. rem 和 js实现移动端的适配是怎么做的?
使用js获取屏幕的宽度然后去修改根元素的字体daxiao,这样就能实现rem单位基于屏幕宽度的变化,1rem就是一倍根元素的字体大小。
js
(function () {
const setRootFontSize = () => {
const designWidth = 750; // 设计稿宽度(如 750px 对应 iPhone 6/7/8)
const baseFontSize = 100; // 1rem = 100px(方便计算,如设计稿中 200px → 2rem)
const scale = document.documentElement.clientWidth / designWidth;// 获取屏幕宽度相对于设计稿屏幕宽度的倍数
document.documentElement.style.fontSize = baseFontSize * Math.min(scale, 2) + 'px'; // 限制最大缩放比例
};
setRootFontSize();
window.addEventListener('resize', setRootFontSize);
})();
11. css 的盒子模型
浏览器渲染一个容器时,会把容器渲染成 4 个区域,分别是 content、padding、border、margin。由这4 个区域组成了一个盒子模型。
标准盒模型 :width/height
= content
,不含 padding
和 border
。
IE盒模型 :width/height
= content
+ padding
+ border
。
切换 :box-sizing: content-box
(标准) / border-box
(IE模型)。
12. vue的slot
1. 默认插槽(Default Slot)
- 作用 :父组件传递的内容会被渲染到子组件中
<slot>
标签的位置。 - 子组件:
js
<!-- ChildComponent.vue -->
<template>
<div>
<slot>默认内容(当父组件未提供时显示)</slot>
</div>
</template>
父组件:
js
<ChildComponent>
<p>这是父组件传递的内容</p>
</ChildComponent>
2. 具名插槽(Named Slots)
- 作用:通过名称标识多个插槽,实现内容精准分发。
- 子组件:
js
<!-- ChildComponent.vue -->
<template>
<div>
<slot name="header"></slot>
<slot></slot> <!-- 默认插槽 -->
<slot name="footer"></slot>
</div>
</template>
父组件:
js
<ChildComponent>
<template v-slot:header>
<h1>标题</h1>
</template>
<p>默认插槽的内容</p> <!-- 隐式对应默认插槽 -->
<template #footer> <!-- 简写语法 -->
<p>页脚</p>
</template>
</ChildComponent>
3. 作用域插槽(Scoped Slots)
- 作用:子组件向父组件的插槽传递数据,实现数据驱动的内容渲染。
- 子组件:
js
<!-- ChildComponent.vue -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot :item="item"></slot>
</li>
</ul>
</template>
父组件:
js
<ChildComponent>
<template v-slot:default="{ item }"> <!-- 接收子组件传递的数据 -->
<span>{{ item.text }}</span>
</template>
</ChildComponent>
13. 手写节流或深拷贝
最后一道题是手写题,从节流和深拷贝中选一个,我选了节流。
不知道为什么在面试的时候敲代码手都是抖的,思路也比较混乱,但还好是写出来的。
节流函数
节流函数的原理是:利用闭包存放上一次点击按钮时的时间,比较当前点击按钮的时间和上一次点击按钮的时间,如果时间小于设定的时间的话,不执行函数;当大于设定的时间时,才执行函数,并更新上次点击按钮的时间的值。
js
function throttle(fn, wait) {
let preTime = null
return function (...args) {
let nowTime = Date.now()
if (nowTime - preTime > wait) {
fn.call(this, ...args)
preTime = nowTime
} } }
深拷贝
深拷贝就是浅拷贝加上递归,当需要对象的属性是原始类型时,直接放到新对象中;当属性是引用类型时,递归自己:
js
function deepClone(obj) {
let clone = obj instanceof Array ? [] : {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
if (obj[key] instanceof Object) {
clone[key] = deepClone(obj[key])
} else {
clone[key] = obj[key]
}
}
}
return clone
}
总结
感觉知识永远学不完,看文章时总是有不会的,唉,革命尚未完成,同志还须努力。还得沉淀沉淀再沉淀!