常见Vue原理面试题,看你翅膀硬了没
1. Vue的响应式原理是什么?请详细说明Object.defineProperty()和Proxy的区别和用法。
响应式原理:Vue中采用了数据劫持的方式,通过Object.defineProperty()函数来监听数据变化,并在数据变化时触发对应的更新函数。 Object.defineProperty()与Proxy的区别:前者只能监听属性的读取和修改,后者可以监听数组的变化等更多场景,且性能更高。
// 实现observe函数,对data对象中的所有属性进行数据劫持
function observe(data) {
if (!data || typeof data !== 'object') {
return
}
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key])
})
}
// 定义defineReactive函数,通过Object.defineProperty()函数来监听数据变化
function defineReactive(obj, key, val) {
observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log('get value:', val)
return val
},
set: function reactiveSetter(newVal) {
console.log('set value:', newVal)
val = newVal
}
})
}
// 测试代码
const obj = { name: 'Tom', age: 18 }
observe(obj)
obj.age = 20
console.log(obj.age) // 输出:20
2. Vue的模板编译原理是什么?请说明它的优化策略和实现方式。并手动实现一个简单的模板编译器。
模板编译原理:Vue中将用户写好的模板转换成渲染函数,实际渲染时调用该函数进行渲染。模板编译主要包括三个阶段:解析、优化和生成。
优化策略:Vue在模板编译阶段会对模板进行静态节点标记和静态根节点标记,从而可以避免不必要的重复渲染和提升整体渲染性能。
实现方式:Vue通过将模板解析成抽象语法树(AST),再转换成render函数的方式来实现模板编译。最终生成的render函数就是一个虚拟DOM的描述对象,然后通过虚拟DOM的diff算法进行渲染更新。
// 简化版的 Vue 模板编译器
function compile(template) {
var element = document.createElement('div');
element.innerHTML = template;
Array.from(element.childNodes).forEach(function(node) {
if (node.nodeType === Node.TEXT_NODE) {
var reg = /\{\{(.+)\}\}/;
var match = node.textContent.match(reg);
if (match) {
var key = match[1].trim();
node.textContent = '{{' + key + '}}';
node._key = key;
}
} else if (node.nodeType === Node.ELEMENT_NODE) {
Array.from(node.attributes).forEach(function(attr) {
if (attr.name.startsWith('v-')) {
if (attr.name === 'v-model') {
node.value = '';
node.addEventListener('input', function(event) {
vm[attr.value] = event.target.value;
});
}
node.removeAttribute(attr.name);
}
});
}
if (node.childNodes.length > 0) {
compile(node.innerHTML);
}
});
return element.innerHTML;
}
var vm = new Vue({
el: '#app',
data: {
message: 'Hello, World!'
},
template: '<div><h1>{{message}}</h1></div>'
});
document.getElementById('app').innerHTML = compile(vm.template);
3. Vue中的虚拟DOM算法是什么?请说明其原理和优化策略。
虚拟DOM算法:Vue中通过比较新旧虚拟DOM树之间的差异来最小化更新操作,从而提高渲染效率。虚拟DOM算法主要包括两个步骤:Diff算法和更新操作。
原理:Vue将模板编译成虚拟DOM树,并将其与上一个虚拟DOM树进行比较,找出需要更新的节点并进行更新操作。
优化策略:Vue采用了一些策略来减少比较的次数,优化了虚拟DOM树的构建和比较的性能。
// 旧的虚拟DOM树
const prevVNode = {
tag: 'div',
props: { id: 'container' },
children: [
{ tag: 'h1', children: 'Hello, World!' },
{ tag: 'p', children: 'Welcome to my website.' }
]
}
// 新的虚拟DOM树
const nextVNode = {
tag: 'div',
props: { id: 'container' },
children: [
{ tag: 'h1', children: 'Hello, Vue!' },
{ tag: 'p', children: 'Welcome to the Vue world.' }
]
}
// 执行diff算法,得到需要更新的节点
function diff(prevVNode, nextVNode) {
if (prevVNode.tag === nextVNode.tag) {
const patchProps = {}
const prevChildren = prevVNode.children
const prevProps = prevVNode.props
const nextChildren = nextVNode.children
const nextProps = nextVNode.props
// diff props
for (const key in nextProps) {
const prevVal = prevProps[key]
const nextVal = nextProps[key]
if (prevVal !== nextVal) {
patchProps[key] = nextVal
}
}
// diff children
if (prevChildren.length !== nextChildren.length) {
patchProps.children = nextChildren
} else {
const patchChildren = []
for (let i = 0; i < nextChildren.length; i++) {
const childPatch = diff(prevChildren[i], nextChildren[i])
patchChildren.push(childPatch)
}
patchProps.children = patchChildren
}
return { type: 'update', props: patchProps }
} else {
return { type: 'replace', node: nextVNode }
}
}
// 执行更新操作,将差异应用到真实DOM树上
function patch(node, patchProps) {
switch (patchProps.type) {
case 'replace':
const newNode = createDOMElement(patchProps.node)
node.parentNode.replaceChild(newNode, node)
break
case 'update':
updateDOMProps(node, patchProps.props)
for (const patchChild of patchProps.children) {
patch(node.children[patchChild.index], patchChild.props)
}
break
}
}
4. Vue中的组件通信有哪些方式?请分别说明它们的特点和应用场景。
组件通信方式:
-
父子组件通信:通过props传递数据和事件监听。 特点:简单易用,适用于父子组件之间的数据交互。 应用场景:父子组件之间的状态传递和操作。
-
子父组件通信:通过
$emit
触发自定义事件和$on
监听事件。 特点:适用于子组件向父组件传递数据和状态变更。 应用场景:子组件向父组件传递用户输入或其他数据。 -
兄弟组件通信:通过
$emit/ $on
或vuex进行数据共享。 特点:适用于兄弟组件之间的状态共享和协同工作。 应用场景:多个组件之间需要共享状态或数据。 -
跨级组件通信:通过
provide/ inject
进行跨级数据传递。 特点:适用于祖先组件向后代组件传递数据。 应用场景:多级嵌套组件之间的状态共享和传递。
// 子组件
<template>
<div>
<h1>{{ title }}</h1>
<button @click="handleClick">点击我</button>
</div>
</template>
<script>
export default {
props: ['title'],
methods: {
handleClick() {
this.$emit('change', '子组件按钮被点击了')
}
}
}
</script>
// 父组件
<template>
<div>
<h2>{{ subtitle }}</h2>
<child :title="title" @change="handleChange"></child>
</div>
</template>
<script>
import Child from './Child.vue'
export default {
components: { Child },
data() {
return {
title: '父子组件通信示例',
subtitle: ''
}
},
methods: {
handleChange(msg) {
this.subtitle = msg
}
}
}
</script>
5. Vue中的v-model指令在表单元素上的使用方式有哪些?请分别说明它们的区别和注意事项。
v-model指令在表单元素上的使用方式:
-
v-model="value":用于单选框、复选框和选择框的数据绑定,绑定的是选择的值。 区别:单选框和复选框绑定的是选中状态,而选择框绑定的是选中的值。 注意事项:当多个单选框或复选框绑定同一个数据时,需要为每个元素添加不同的value属性。
-
v-model="value":用于输入框等表单元素的双向数据绑定,绑定的是输入框的值。 区别:v-model与value属性的实现方式不同,v-model实现了双向绑定的效果。 注意事项:需要为输入框添加type属性,并且该元素必须支持input事件或change事件。
// 单选框
<template>
<div>
<input type="radio" v-model="gender" value="male">男
<input type="radio" v-model="gender" value="female">女
</div>
</template>
<script>
export default {
data() {
return {
gender: ''
}
}
}
</script>
// 输入框
<template>
<div>
<input type="text" v-model="message">
<p>输入的内容是:{{ message }}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: ''
}
}
}
</script>
6. Vue中的计算属性computed和侦听器watch有什么区别?请举例说明它们的使用场景。
computed和watch的区别:
- computed是基于依赖缓存的,只有当依赖的数据发生变化时才会重新计算;watch则是通过监听数据变化来触发回调函数。
- computed适用于多个值之间的计算和处理,而watch则适用于单独的数据变化后执行异步或复杂的操作。
// 计算属性
<template>
<div>
<p>商品数量:{{ quantity }}</p>
<p>商品总价:{{ totalPrice }}</p>
</div>
</template>
<script>
export default {
data() {
return {
price: 10,
quantity: 3
}
},
computed: {
totalPrice() {
return this.price * this.quantity
}
}
}
</script>
// 侦听器
<template>
<div>
<input type="text" v-model="message">
<p>输入的消息是:{{ message }}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: ''
}
},
watch: {
message(newVal, oldVal) {
console.log('输入的消息发生了变化:', newVal, oldVal)
// 执行异步操作
this.$http.get('/api/messages', { params: { message: newVal } }).then(res => {
console.log(res.data)
})
}
}
}
</script>
7. 简述 Vue 中的异步更新队列原理。
Vue 在更新视图时会将所有数据变化的 watcher(观察者)加入到一个异步更新队列中,等到下一个事件循环时才执行更新操作。这样做的好处是避免频繁的更新视图,提高性能。
- 在 watcher 对象构造函数中,将当前 watcher 对象 push 到全局的异步更新队列 queue 中;
- 在下一个事件循环方式中,通过 flushQueue 函数逐个遍历 queue 数组中的 watcher,调用其 run 方法进行更新;
- 清空 queue 数组,以待下一次更新。
// 定义全局异步更新队列
var queue = [];
// 异步更新队列处理函数
function flushQueue() {
queue.forEach(function(watcher) {
watcher.run();
});
queue = [];
}
// 定义 Watcher 类
class Watcher {
constructor() {
queue.push(this); // 将当前 watcher 加入到异步更新队列中
}
run() {
console.log('更新视图!'); // 执行更新操作
}
}
// 创建多个 Watcher 对象
var watcher1 = new Watcher();
var watcher2 = new Watcher();
// 延迟一定时间,再执行异步更新操作
setTimeout(function() {
flushQueue(); // 清空异步更新队列,逐个执行更新
}, 0);
8. 简述 Vue 中的事件机制原理,并手动实现一个简单的事件总线。
Vue 中的事件机制是利用事件总线 EventBus 进行封装实现的。事件总线将事件的发送和接收解耦,通过一个中心化的事件分发器(Event Bus)来管理,使得多个组件间的通信变得简单而灵活。
// 定义 EventBus 类
class EventBus {
constructor() {
this.events = {};
}
$on(name, callback) {
if (!this.events[name]) {
this.events[name] = [];
}
this.events[name].push(callback);
}
$emit(name, ...args) {
if (this.events[name]) {
this.events[name].forEach(function(callback) {
callback(...args);
});
}
}
}
// 创建 EventBus 实例
var bus = new EventBus();
// 定义事件处理函数
function handleMessage(message) {
console.log(`收到消息:${message}`);
}
// 绑定 message 事件处理函数
bus.$on('message', handleMessage);
// 触发 message 事件,并传递参数
bus.$emit('message', 'Hello, World!');
转载自:https://juejin.cn/post/7231719572000653372