面试题合集
1.vue2 和 vue3 双向绑定的原理是什么?
- 响应式原理不同,Object.defineProperty 和 new Proxy()
$set
在 vue3 为 undefined- vue2 为
options api
;vue3 是composion api
和setup
语法糖 v-if
和v-for
的优先级不同- 在 Vue 2 中,当它们同时出现在同一个元素上时,
v-for
的优先级高于v-if
。 - 在 Vue 3 中,这种行为有所改变。Vue 3 中,
v-if
的优先级高于v-for
。这种改变可以提高性能,避免了不必要的循环渲染。
- 在 Vue 2 中,当它们同时出现在同一个元素上时,
ref
和$children
不同- 在 Vue 2 中,
ref
通常用于获取 DOM 元素的引用或者创建响应式数据 - 在 Vue 3 中,
ref
也可以用于创建响应式数据,但它的功能更加强大,可以用于包裹普通 JavaScript 值、DOM 元素、甚至是组件实例。使用ref
创建的数据,可以通过.value
来访问或修改。
- 在 Vue 2 中,
Vue 2 使用了基于Object.defineProperty的双向绑定原理:
<!DOCTYPE html>
<html>
<head>
<title>基于 Object.defineProperty 的双向绑定示例</title>
</head>
<body>
<div id="app">
<input
id="inputField"
type="text"
/>
<p id="outputText"></p>
</div>
<script>
const data = {
message: ''
};
const inputField = document.getElementById('inputField');
const outputText = document.getElementById('outputText');
Object.defineProperty(data, 'message', {
get() {
return this._message;
},
set(value) {
this._message = value;
inputField.value = value; // 更新输入框的值
outputText.textContent = value; // 更新文本显示
}
});
inputField.addEventListener('input', function () {
data.message = inputField.value;
});
</script>
</body>
</html>
Vue 3 引入了基于Proxy的双向绑定原理:
<!DOCTYPE html>
<html>
<head>
<title>基于 Proxy 的双向绑定示例</title>
</head>
<body>
<div id="app">
<input
id="inputField"
type="text"
/>
<p id="outputText"></p>
</div>
<script>
const data = {
message: ''
};
const inputField = document.getElementById('inputField');
const outputText = document.getElementById('outputText');
const dataProxy = new Proxy(data, {
get(target, property) {
return target[property];
},
set(target, property, value) {
target[property] = value;
inputField.value = value; // 更新输入框的值
outputText.textContent = value; // 更新文本显示
return true;
}
});
inputField.addEventListener('input', function () {
dataProxy.message = inputField.value;
});
</script>
</body>
</html>
2.vue 的 render 函数是什么? render 函数和 template 模板有什么关系?
- Vue 中的
render
函数是用于创建虚拟 DOM(Virtual DOM)的 JavaScript 函数。与template
模板不同,render
函数是更底层、更灵活的方式来定义 Vue 组件的渲染逻辑。
总之,template
是一种声明性的模板语法,适合大多数情况,而 render
函数提供了更高级和灵活的方式来定义组件的渲染逻辑,可以用于处理一些复杂的场景。在大多数情况下,你可以使用 template
来构建组件,但如果需要更多控制权,可以使用 render
函数。
3.vue 如何解析指令? 模板变量? html 标签
Vue 解析指令、模板变量和 HTML 标签的过程是在模板编译阶段完成的,该阶段将模板字符串转换为可执行的渲染函数。
- Vue 使用正则表达式和解析器来识别和解析指令
- 解析模板变量:Vue 在编译过程中会识别这些插值表达式,并将它们替换为生成虚拟 DOM 的代码。
- 例如,对于 {{ message }},Vue 会生成一个虚拟 DOM 节点,该节点的值将由 message 变量的当前值填充。
- Vue 在编译模板时也会解析 HTML 标签,包括普通的 HTML 元素标签和自定义组件标签。当 Vue 遇到自定义组件标签时,它会查找并解析对应的组件定义,并将其转换为渲染函数。
4.vue 是怎么解析 template 的? template 会变成什么?
- 解析模板:Vue 会解析传入的 template 字符串,其中包含了 HTML 标记、指令(如 v-if、v-for 等)、插值表达式(如{{ message }})等。
- 生成抽象语法树(AST):Vue 会将解析后的模板转换成一个抽象语法树(AST),这个树结构表示了模板中的各种元素、指令和数据绑定。
- 生成渲染函数:基于 AST,Vue 会生成一个渲染函数,这个函数会接收应用的状态(即 data 属性)作为参数,并返回一个虚拟 DOM。
- 渲染虚拟 DOM:当应用的状态发生变化时,Vue 会调用渲染函数,生成新的虚拟 DOM。然后,Vue 会使用新的虚拟 DOM 与之前的虚拟 DOM 进行比较,找出变化的部分。
- 更新实际的 DOM:最后,Vue 会根据虚拟 DOM 的变化,批量更新实际的 DOM,以反映应用状态的变化。
vue 源码学习总结 深入解析 template 编译成 render 函数过程
5.vue 的 keep-alive 的作用是什么?怎么实现的?如何刷新的?
主要作用:
- 缓存组件实例
- 避免重复渲染
- 保存组件状态
- 条件缓存
实现原理:
- 缓存组件实例:当
<keep-alive>
包裹的组件首次渲染时,Vue.js 会将该组件的实例保存在内部的缓存列表中。 - 组件切换:当需要重新渲染包裹在
<keep-alive>
中的组件时(例如,通过路由切换或条件显示/隐藏),Vue.js 会首先检查缓存中是否已经有了这个组件的实例。 - 复用缓存:如果缓存中存在该组件的实例,Vue.js 会选择性地将缓存中的实例重新挂载到 DOM 上,而不是重新创建一个新的实例。这样可以避免不必要的渲染和数据初始化过程。
- 生命周期钩子:当组件从缓存中取出并重新挂载时,Vue.js 会调用组件的一系列生命周期钩子,如
activated
和deactivated
,以便让开发者可以在组件复用和销毁时执行特定的逻辑。 - 缓存策略:Vue.js 还提供了一些缓存策略选项,允许你控制缓存的行为,例如设置缓存的最大数量,或定义哪些组件需要被缓存。
总之,<keep-alive>
的实现原理是基于 Vue.js 的组件生命周期和虚拟 DOM 渲染流程,通过缓存和复用组件实例,以提高性能和用户体验。
刷新缓存:
- 使用
include
和exclude
属性。 - 调用
$forceUpdate()
方法。 - 动态改变组件的
key
属性。
6.Vue2.0 v-for 中 :key 到底有什么用?
需要使用 key 来给每个节点做一个唯一标识,Diff 算法就可以正确的识别此节点,找到正确的位置区插入新的节点。所以一句话,key 的作用主要是为了高效的更新虚拟 DOM。
7.v-for
和 v-if
使用问题
v-for 的优先级比 v-if 的优先级高,在遍历每一条 item 后,会执行 v-if,会造成不必要的计算,影响性能。
解决方法:通过使用 computed 属性,来筛选出符合条件项,然后再使用 v-for 遍历
8.实例属性 data 为什么是一个 function
组件是一个可复用的实例,当你引用一个组件的时候,组件里的 data 是一个普通的对象,所有用到这个组件的都引用的同一个 data,就会造成数据污染。
将 data 封装成函数后,在实例化组件的时候,我们只是调用了 data 函数生成的数据副本,避免了数据污染。
9.hash 和 history 模式的区别
hash 模式:在浏览器 url 中提现为#,#以及之后的内容用window.location.hash
读取。特点:hash 虽然在 URL 中,但不被包括在 HTTP 请求中;用来指导浏览器动作,对服务端安全无用,hash 不会重加载页面。
hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 http://www.xxx.com,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回 404 错误。
history 模式:history 采用 HTML5 的新特性;且提供了两个新方法:pushState(),replaceState()可以对浏览器历史记录栈进行修改,以及 popState 事件的监听到状态变更。 history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,如 http://www.xxx.com/items/id
。后端如果缺少对 /items/id 的路由处理,将返回 404 错误。
10.computed 和 watch
Computed Properties(计算属性)
计算属性是基于它们所依赖的数据进行动态计算的属性。当其所依赖的数据发生变化时,计算属性会重新计算其值。Vue 会缓存计算属性的值,只有在它所依赖的响应式数据发生变化时,才会重新求值。
计算属性的原理是基于 Vue 的响应式系统。当计算属性依赖的响应式数据发生变化时,Vue 会检测到这种变化,并重新计算计算属性的值。这种机制保证了计算属性的响应性和高效性。
计算属性的实现
在 Vue.js 2 内部,当你定义一个计算属性时,Vue 会将其转换成一个称为 Watcher 的对象,并将其添加到组件实例的 vm 对象中。当依赖的响应式数据发生变化时,该 Watcher 对象会收到通知并重新计算其值。
Vue 的响应式系统通过 Object.defineProperty 或者 Proxy 来劫持对响应式数据的访问和修改,当数据被访问时,会进行依赖收集,当数据变化时,会通知所有依赖该数据的 Watcher 对象进行更新。
<!-- ParentComponent.vue -->
<template>
<div>
<p>Computed Property: {{ sum }}</p>
<input
type="number"
v-model="a"
/>
+
<input
type="number"
v-model="b"
/>
=
<input
type="number"
v-model="sum"
/>
</div>
</template>
<script>
export default {
data() {
return {
a: 1,
b: 2
};
},
computed: {
sum: {
get: function () {
console.log('get');
return Number(this.a) + Number(this.b);
},
set: function (newValue) {
// 在这里进行计算属性变化后的处理
console.log('sum 更新为:', newValue);
}
}
},
methods: {
addA() {
this.a++;
},
updateSum() {
this.addA();
this.sum = this.a + 10;
}
}
};
</script>
在上面的示例中,sum 计算属性定义了 set 函数,当尝试给 sum 赋值时(虽然实际上是不被允许的),set 函数会被调用,并打印出新的值。但是请注意,computed 属性并不是通过直接赋值来修改的,而是由其依赖的响应式数据的变化间接地影响其值的。
Watchers(观察者)
Watcher 是一个能够监听到 Vue 实例数据变化的机制。可以利用它来执行自定义的逻辑。
Watcher 的原理是在数据发生变化时执行指定的回调函数。这个回调函数可以执行任何逻辑,比如发送 HTTP 请求、触发动画效果等。
Watcher 的实现
当你使用 watch 选项时,Vue 会创建一个 Watcher 对象来监视数据的变化,并在数据变化时执行相应的回调函数。这个 Watcher 对象与计算属性的 Watcher 对象相似,但是它执行的逻辑不同,主要是执行你定义的回调函数。
在内部实现上,Vue 会为每个 watch 选项创建一个独立的 Watcher 对象,并在数据变化时调用对应的回调函数。
总的来说,computed 和 watch 的实现都依赖于 Vue 的响应式系统和 Watcher 对象,通过对响应式数据的监听和计算来实现相应的功能。
11.vue2 和 vue3 生命周期
12.在 vue 中监听页面滚动
<template>
<div class="main-container">
<p
v-for="(t, i) in page"
:key="i"
>
{{ t }}
</p>
</div>
</template>
<script>
export default {
name: 'Test',
data() {
return {
page: new Array(20).fill().map(i => Math.random().toString(36).substring(2))
};
},
mounted() {
document.addEventListener('scroll', this.scroll);
},
beforeDestroy() {
document.removeEventListener('scroll', this.scroll);
},
methods: {
scroll() {
const scrollHeight = document.body.scrollHeight; // 页面高度
const screenHeight = window.screen.height; // 屏幕高度
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop; // 页面滚动卷去高度
console.log('页面高度:', scrollHeight, '\r\n屏幕高度:', screenHeight, '\r\n页面滚动卷去高度:', scrollTop);
}
}
};
</script>
<style lang="less" scoped>
p {
display: block;
background-color: #e9a4a4;
min-width: 200px;
height: 44px;
line-height: 44px;
margin: 20px auto 0;
border: 1px solid #a5c5b5;
border-radius: 2px;
}
</style>
13. this.$nextTick
Vue.js 利用了浏览器的事件循环机制,确保 this.$nextTick 中的回调函数在当前 DOM 更新周期之后执行。这样做的好处是,你可以在更新视图之后立即访问 DOM,确保获取到最新的 DOM 元素状态。
在具体实现上,Vue.js 使用了微任务(microtask)或宏任务(macrotask)来实现 this.$nextTick。
14. 在 vue2 中,直接改变数组中某项的值为什么不会在页面更新?
在 Vue 2 中,直接改变数组中某项的值不会触发页面更新的原因是因为 Vue 的响应式系统无法检测到这种修改。
Vue 2 的响应式系统是基于 ES5 的Object.defineProperty
实现的。当 Vue 将一个对象转换成响应式对象时,它会遍历对象的所有属性,并使用Object.defineProperty
对每个属性进行拦截,当属性被访问或修改时,Vue 能够捕获到并通知相关的依赖进行更新。但是,这种拦截只能对对象的属性进行,而不能对数组的索引进行拦截。
当直接修改数组中的某项值时,例如 arr[0] = newValue
,这种修改操作并不会触发Object.defineProperty
的拦截器,因此 Vue 无法检测到数组元素的变化,也就无法触发页面的更新。
为了解决这个问题,Vue 提供了一些特殊的数组方法,如push
、pop
、shift
、unshift
、splice
、sort
、reverse
等,这些方法在内部已经进行了重写,并且能够通知 Vue 进行页面的更新。如果想要修改数组中的某项值并触发页面更新,可以使用Vue.set
或者数组的修改方法来实现。
下面是 Vue 2 中对数组的部分源码:
javascriptCopy codeconst arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) {
// 保存原始方法
const original = arrayProto[method];
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args);
const ob = this.__ob__; // 获取到数组对应的 Observer 对象
let inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
if (inserted) ob.observeArray(inserted); // 对新增的元素进行响应式处理
ob.dep.notify(); // 通知依赖更新
return result;
});
});
在这段代码中,Vue 首先创建了一个原型为Array.prototype
的对象arrayMethods
,然后遍历了数组的常用变更方法(如push
、pop
、shift
等),对每一个方法进行了重写。在重写的方法中,Vue 进行了两个操作:
- 执行原始方法:首先调用原始方法执行数组的变更操作,例如
push
方法会向数组末尾添加元素。 - 手动通知:在数组变更操作执行完成后,手动通知数组对应的 Observer 对象进行依赖的更新。
通过这种方式,Vue 能够实现对数组中某个索引位置的值的改变的监听,并在数组发生变化时,及时通知相关的依赖进行更新。