1.Vue的基本原理
- 当一个Vue实例创建时,Vue会遍历data中的属性,用 Object.defineProperty(vue3.0使用proxy )将它们转为 getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。
- 每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。
2.双向数据绑定的原理
-
数据劫持(Data Hijacking)
Vue 使用
Object.defineProperty()
方法对数据对象的属性进行拦截和劫持。这样,当数据发生变化时,可以通知依赖这些数据的视图进行更新。const data = { message: 'Hello, Vue!' }; Object.keys(data).forEach(key => { let value = data[key]; Object.defineProperty(data, key, { get() { console.log(`获取属性 ${key}`); return value; }, set(newValue) { console.log(`设置属性 ${key} 为 ${newValue}`); value = newValue; // 通知依赖更新 } }); });
-
发布-订阅模式(Publish-Subscribe Pattern)
在Vue中,每个响应式属性都有一个依赖(
Dep
)对象,存储所有依赖于该属性的订阅者(即观察者,Watcher
)。当属性变化时,Dep
会通知所有订阅者进行更新。依赖收集(Dependency Collection)
当Vue实例化时,
Watcher
会读取数据属性,并将自己添加到该属性的依赖列表中。class Dep { constructor() { this.subscribers = []; } addSub(sub) { this.subscribers.push(sub); } notify() { this.subscribers.forEach(sub => { sub.update(); }); } } class Watcher { constructor(obj, key, callback) { this.obj = obj; this.key = key; this.callback = callback; this.value = this.get(); } get() { Dep.target = this; const value = this.obj[this.key]; Dep.target = null; return value; } update() { const value = this.obj[this.key]; this.callback(value); } } const data = { message: 'Hello, Vue!' }; const dep = new Dep(); Object.keys(data).forEach(key => { let value = data[key]; Object.defineProperty(data, key, { get() { if (Dep.target) { dep.addSub(Dep.target); } return value; }, set(newValue) { value = newValue; dep.notify(); } }); }); // 使用观察者 new Watcher(data, 'message', (newVal) => { console.log(`视图更新: ${newVal}`); }); data.message = 'Hello, World!'; // 输出: 视图更新: Hello, World!
-
模版编译和更新
Vue 的模版编译器会将模版编译成渲染函数,渲染函数会读取响应式数据并生成虚拟DOM。当数据变化时,
Watcher
会触发视图更新,新的虚拟DOM会和旧的虚拟DOM进行比较(diff算法),找到最小的差异,并只更新那些实际变化的部分。 -
双向数据绑定
- 绑定输入事件:监听表单元素的
input
事件,当用户输入时,更新数据模型。 - 绑定视图更新:当数据模型变化时,更新表单元素的值。
- 绑定输入事件:监听表单元素的
总结:首先数据劫持,用Object.defineProperty()
给每个属性加上getter和setter。有一个watcher类,watcher在实例化的时候就会获取一次数据的值从而触getter,getter会把watcher添加进依赖Dep。Dep是用来管理watcher的,他有把watcher添加进管理列表subscribers
的方法addSub()
和通知方法notify()
。当数据更新的时候,触发响应式数据的setter,就会调用Dep中的notify()
,会通知subscribers
中所有的watcher调用他们的update()
方法进行更新。
3.使用 Object.defineProperty() 来进行数据劫持有什么缺点?
在对一些属性进行操作时,使用这种方法无法拦截,比如通过下标方式修改数组数据或者给对象新增属性,这都不能触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作。
在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据劫持。唯一的缺点是兼容性的问题,因为 Proxy 是 ES6 的语法。
4.computed和watch
- computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。
- watch 侦听器 : 更多的是观察的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。
5.slot是什么?有什么作用?原理是什么?
slot又名插槽,是Vue的内容分发机制,组件内部的模板引擎使用slot元素作为承载分发内容的出口。
- 默认插槽:又名匿名插槽,当slot没有指定name属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。
- 具名插槽:带有具体名字的插槽,也就是带有name属性的slot,一个组件可以出现多个具名插槽。
- 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。
实现原理:当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.$slot
中,默认插槽为vm.$slot.default
,具名插槽为vm.$slot.xxx
,xxx 为插槽名,当组件执行渲染函数时候,遇到slot标签,使用$slot
中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽
6.如何保存页面的当前的状态
-
组件会被卸载
- 将状态存储在LocalStorage / SessionStorage
- 各种传值
-
组件不会被卸载
直接把页面写成一个组件,控制他隐藏。
-
keep-alive
用
<keep-alive>
包裹,这个时候组件就不会执行一系列钩子函数,只会执行activated和deactivated//组件中 <keep-alive> <router-view v-if="$route.meta.keepAlive"></router-view> </kepp-alive>
//router.js { path: '/', name: 'xxx', component: ()=>import('../src/views/xxx.vue'), meta:{ keepAlive: true // 需要被缓存 } },
7.常见的事件修饰符及其作用
.stop
:等同于 JavaScript 中的event.stopPropagation()
,防止事件冒泡;.prevent
:等同于 JavaScript 中的event.preventDefault()
,防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播);.capture
:与事件冒泡的方向相反,事件捕获由外到内;.self
:只会触发自己范围内的事件,不包含子元素;.once
:只会触发一次。
8.v-model 是如何实现的,语法糖实际是什么?
就是语法糖,省略了一些代码,实际上就是绑定了一个事件,传递了一个prop。
-
用在表单上
<input v-model="sth" /> // 等同于 <input v-bind:value="message" v-on:input="message=$event.target.value" > //$event 指代当前触发的事件对象; //$event.target 指代当前触发的事件对象的dom; //$event.target.value 就是当前dom的value值; //在@input方法中,value => sth; //在:value中,sth => value;
-
用在自定义组件上
// 父组件 <aa-input v-model="aa"></aa-input> // 等价于 <aa-input v-bind:value="aa" v-on:input="aa=$event.target.value"></aa-input>
在自定义组件中Vue2和Vue3有些许的不同,Vue2中
v-model
在同一个组件上只能用一次,想要用多次就用.sync
。<!-- Test.vue --> <CustomInput :firstName.sync="obj.firstName" :lastName.sync="obj.lastName"></CustomInput> <!-- CustomInput.vue --> <template> <div> <div><span>{{firstName}}</span><span @click="changeX">更改姓</span></div> <div><span>{{lastName}}</span><span @click="changeM">更改名</span></div> </div> </template> <script> export default { props: { firstName: String, lastName: String }, methods: { changeX () { this.$emit('update:firstName', 'liu') }, changeM () { this.$emit('update:lastName', 'yz') } } } </script>
Vue3中可以用多次
v-model
,但是要定义一下名称<!-- Test.vue --> <MyComponent v-model:first-name="first" v-model:last-name="last" /> <!-- MyComponent.vue --> defineProps({ firstName: String, lastName: String }) defineEmits(['update:firstName', 'update:lastName']) </script> <template> <input type="text" :value="firstName" @input="$emit('update:firstName', $event.target.value)" /> <input type="text" :value="lastName" @input="$emit('update:lastName', $event.target.value)" /> </template>
9.data为什么是一个函数而不是对象
Vue组件可能存在多个实例,如果使用对象形式定义data,则会导致它们共用一个data对象,那么状态变更将会影响所有组件实例。
10.对keep-alive的理解,它是如何实现的,具体缓存的是什么?
把组件的实例添加到缓存里面,并且缓存它的key。当组件切换回来的时候会在cache对象中找到对应的实例,重新激活。
11.$nextTick 原理及作用
在 Vue 中,数据的变化会触发 DOM 更新,但这种更新是异步执行的,以便在同一个事件循环中多次数据修改只会触发一次 DOM 更新。$nextTick
方法允许你在 DOM 更新完成后执行代码,这对需要依赖最新 DOM 状态的操作非常有用。
12.Vue 中给 data 中的对象属性添加一个新的属性时会发生什么?如何解决?
视图不会有变化,除非用了api $set()
<template>
<div>
<ul>
<li v-for="value in obj" :key="value"> {{value}} </li>
</ul>
<button @click="addObjB">添加 obj.b</button>
</div>
</template>
<script>
export default {
data () {
return {
obj: {
a: 'obj.a'
}
}
},
methods: {
addObjB () {
this.$set(this.obj, 'b', 'obj.b')
}
}
}
</script>
13.Vue中封装的数组方法有哪些,其如何实现页面更新
push(),pop(),shift(),unshift(),splice(),sort(),reverse()
- 创建
arrayMethods
对象:首先,创建一个对象arrayMethods
,它继承自Array.prototype
,并对数组的变更方法进行重写。 - 重写数组方法:对每个需要重写的方法(如
push
、pop
等),使用Object.defineProperty
定义新的方法。在新的方法中,首先调用原始的数组方法,然后进行依赖通知。 - 依赖通知:重写的方法在执行原始操作后,会调用观察者的
dep.notify()
方法,通知所有依赖该数组的观察者,从而触发视图更新。 - 观察新插入的元素:对于
push
、unshift
和splice
方法,会对新插入的元素进行观察,以确保这些新元素也是响应式的。
14.Vue template 到 render 的过程
-
模板解析和编译
- 模板解析(Parsing) :将模板字符串解析成抽象语法树(AST)。
- 优化(Optimization) :标记静态节点和静态根节点,静态节点生成的DOM不会变化,以提升性能。
- 代码生成(Code Generation) :将优化后的 AST 转换成渲染函数的代码字符串。
-
渲染函数执行和虚拟 DOM 生成
- 渲染函数在组件实例的上下文中执行,返回一个虚拟 DOM 树(VNode Tree)。
-
虚拟 DOM 渲染成真实 DOM
- 初始渲染:将虚拟 DOM 树转换为真实 DOM 元素,并插入到页面中。
- 更新渲染:在数据更新时,生成新的虚拟 DOM 树,与旧的虚拟 DOM 树进行比较(diff 算法),找出需要更新的部分,并进行最小量的 DOM 操作以更新视图。
15.Vue data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗?
不会的,DOM的更新是异步的,会把所有的DOM更新事件放在一个队列里面,一起更新。
16.简述 mixin、extends 的覆盖逻辑
- 生命周期钩子:生命周期钩子(如
created
、mounted
等)会合并成一个数组,所有的钩子函数会按顺序执行。先执行extends
中的钩子函数,再执行mixin
中的钩子函数,最后执行组件自身的钩子函数。 - 数据(data) :数据选项会进行递归合并。如果存在同名的属性,组件自身的数据属性会覆盖
extends
和mixin
中的数据属性。注意,数据属性在合并时是浅合并。 - 方法(methods) :方法选项会合并成一个对象。如果存在同名的方法,组件自身的方法会覆盖
extends
和mixin
中的方法。 - 计算属性(computed) :计算属性的合并逻辑与方法类似,同名的计算属性会被覆盖。
- 其他选项:其他选项(如
components
、directives
等)会进行递归合并,组件自身的选项会优先覆盖extends
和mixin
中的选项。
总结就是,对于生命周期不同的钩子会按顺序执行,同一个钩子会先extends,再mixin,最后才是组件。对于其他的,同名的情况下组件自身的会覆盖mixin和extends的。
17.自定义指令
例如实现一个按钮防抖
import Vue from 'vue';
Vue.directive('debounce', {
bind(el, binding) {
if (typeof binding.value !== 'function') {
console.warn(`Expect a function, got ${typeof binding.value}`);
return;
}
// 使用防抖函数包装传入的函数
const debouncedFn = debounce(binding.value, binding.arg || 300);
// 将防抖函数存储在元素的自定义属性中,以便在 unbind 钩子中可以访问
el.__debouncedFn__ = debouncedFn;
// 添加事件监听器
el.addEventListener('click', debouncedFn);
},
unbind(el) {
// 移除事件监听器
el.removeEventListener('click', el.__debouncedFn__);
// 删除自定义属性
delete el.__debouncedFn__;
}
});
-
全局注册
//写在main.js中 Vue.directive('focus', { // 当绑定元素插入到 DOM 中时 inserted(el) { el.focus(); } });
-
局部注册
//写在组件内 const MyComponent = { directives: { focus: { inserted(el) { el.focus(); } } } };
自定义指令的生命周期:
- bind: 只调用一次,指令第一次绑定到元素时调用。这是进行一次性初始化的好地方。
- inserted: 被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)。
- update: 所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。
- componentUpdated: 指令所在组件的 VNode 及其子 VNode 全部更新后调用。
- unbind: 只调用一次,指令与元素解绑时调用。
18.assets和static的区别
assets中的东西会打包,而static不会,所以已经第三方的资源文件可以放在static中,因为这些东西已经压缩处理过了不需要再处理一次。
19.Vue的性能优化有哪些
-
编码阶段
- 尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher
- v-if和v-for不能连用
- 如果需要使用v-for给每项元素绑定事件时使用事件代理
- SPA 页面采用keep-alive缓存组件
- 在更多的情况下,使用v-if替代v-show
- key保证唯一
- 使用路由懒加载、异步组件
- 防抖、节流
- 第三方模块按需导入
- 长列表滚动到可视区域动态加载
- 图片懒加载
-
SEO优化
- 预渲染
- 服务端渲染
-
打包优化
- 压缩代码
- Tree Shaking/Scope Hoisting
- 使用cdn加载第三方模块
- 多线程打包happypack
- splitChunks抽离公共文件
- sourceMap优化
-
用户体验
- 骨架屏
- 还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。
20.v-if和v-for哪个优先级更高?如果同时出现,应如何优化?
Vue2,Vue3两边不一样,但是总而言之不要一起用。如果需要先做判断再v-for,那就可以使用computed计算过后再v-for。
21.说一下Vue的生命周期
- beforeCreate(创建前) :数据观测和初始化事件还未开始,此时 data 的响应式追踪、event/watcher 都还没有被设置,也就是说不能访问到data、computed、watch、methods上的方法和数据。
- created(创建后) :实例创建完成,实例上配置的 options 包括 data、computed、watch、methods 等都配置完成,但是此时渲染得节点还未挂载到 DOM,所以不能访问到
$el
属性。 - beforeMount(挂载前) :在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。此时还没有挂载html到页面上。
- mounted(挂载后) :在el被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html 页面中。此过程中进行ajax交互。
- beforeUpdate(更新前) :响应式数据更新时调用,此时虽然响应式数据更新了,但是对应的真实 DOM 还没有被渲染。
- updated(更新后) :在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。此时 DOM 已经根据响应式数据的变化更新了。调用时,组件 DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
- beforeDestroy(销毁前) :实例销毁之前调用。这一步,实例仍然完全可用,
this
仍能获取到实例。 - destroyed(销毁后) :实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务端渲染期间不被调用。
- 特殊:actived和deactived,是使用了
<keeep-alive>
包裹的组件独有的,分别对应消失和隐藏。
22.Vue 子组件和父组件执行顺序
加载渲染过程:
- 父组件 beforeCreate
- 父组件 created
- 父组件 beforeMount
- 子组件 beforeCreate
- 子组件 created
- 子组件 beforeMount
- 子组件 mounted
- 父组件 mounted
更新过程:
- 父组件 beforeUpdate
- 子组件 beforeUpdate
- 子组件 updated
- 父组件 updated
销毁过程:
- 父组件 beforeDestroy
- 子组件 beforeDestroy
- 子组件 destroyed
- 父组件 destoryed
23.组件通信
-
父传子
在父组件中用
v-bind
传递数据,子组件用props接受。 -
字传父
父组件中写一个方法用
v-on
绑定一个事件在子组件上,子组件用$emit()
触发事件,数据以函数入参的方式传给父组件。 -
兄弟组件
用EventBus全局事件总线传值。
-
祖先传后代
-
用provide / inject
大致就是在祖先中写provide提供数据,后代用inject接受数据,但是不好用我感觉,还不如用eventBus。
-
pinia / vuex
-
-
其他冷门的方法
-
ref / $ref
在子组件上标记ref,然后获取子组件实例,使用子组件的方法。
-
parent/parent / parent/children
已经不推荐用了,别用。
-
24.Vue-Router 的懒加载如何实现
正常写法
import List from '@/components/list.vue'
const router = new VueRouter({
routes: [
{ path: '/list', component: List }
]
})
-
用箭头函数+import
const List = () => import('@/components/list.vue') const router = new VueRouter({ routes: [ { path: '/list', component: List } ] })
-
使用箭头函数+require
const router = new Router({ routes: [ { path: '/list', component: resolve => require(['@/components/list'], resolve) } ] })
-
使用webpack的require.ensure技术
// r就是resolve const List = r => require.ensure([], () => r(require('@/components/list')), 'list'); // 路由也是正常的写法 这种是官方推荐的写的 按模块划分懒加载 const router = new Router({ routes: [ { path: '/list', component: List, name: 'list' } ] }))
25.hash路由和history路由
一个是修改#后面的hash值进行路由跳转,一个是修改.com/后面的路径,history路由要后台支持,比如修改NGINX的配置,禁止服务器自动匹配资源,将请求交给Vue来处理。
location / {
root html;
try_files $uri /index.html;
}
26.如何定义动态路由?如何获取传过来的动态参数?
-
param方式
- 配置路由格式:
/router/:id
- 传递的方式:在path后面跟上对应的值
- 传递后形成的路径:
/router/123
- 通过
$route.params.id
获取传递的值
- 配置路由格式:
-
query方式
- 配置路由格式:
/router
,也就是普通配置 - 传递的方式:对象中使用query的key作为传递方式
- 传递后形成的路径:
/route?id=123
- 通过$route.query 获取传递的值
- 配置路由格式:
27.Vue-router 路由钩子在生命周期的体现
-
全局钩子
- router.beforeEach 全局前置守卫 进入路由之前
- beforeResolve 全局解析守卫
- router.afterEach 全局后置钩子 进入路由之后
router.beforeEach((to, from) => { ... }); router.afterEach((to, from) => { ... });
-
组件内钩子
-
beforeRouteUpdate
组件被复用但是地址有改变,比如说传参变了。
-
beforeRouteEnter
beforeRouteEnter(to, from, next) { next(target => { if (from.path == '/classProcess') { target.isFromProcess = true } }) }
-
beforeRouteLeave 同上
-
-
单个路由钩子
在路由表里面写的
export default [ { path: '/', name: 'login', component: login, beforeEnter: (to, from, next) => { console.log('即将进入登录页面') next() } } ]
假如说从A页面到B页面完整的顺序
beforeRouteLeave beforeEach beforeEnter beforeRouteEnter beforeResolve afterEach 然后组件生命周期
28.params和query的区别
有两种方式写路由跳转,一种叫编程式导航,还有种叫声明式导航。
第一种params会以路径的形式在url中显示参数,第二种使用编程式导航写params,但是这种方式刷新会丢失参数,导致页面显示有问题。query传参参数会以查询参数的形式在url中显示参数。
29.Vuex
Vuex 实现了一个单向数据流,在全局拥有一个 State 存放数据,当组件要更改 State 中的数据时,必须通过 Mutation 提交修改信息, Mutation 同时提供了订阅者模式供外部插件调用获取 State 数据的更新。而当所有异步操作(常见于调用后端接口异步获取更新数据)或批量的同步操作需要走 Action ,但 Action 也是无法直接修改 State 的,还是需要通过Mutation 来修改State的数据。最后,根据 State 的变化,渲染到视图上。
- State: Vuex 的状态存储是响应式的,当
State
中的数据变化时,依赖这些数据的组件会自动更新。 - Getters: 用于派生
State
,类似于计算属性。 - Mutations: 是同步事务,用于更改
State
。通过提交Mutation
,可以确保State
的变化是可追踪的。 - Actions: 可以包含异步操作,通过分发
Action
来提交Mutation
,使得State
的变化是可预测的。
30.Vue2和Vu3的区别
Vue2使用Object.defineProperty,Vue3使用proxy,Vue3有setup模式,Vue2是选项是api(options api),Vue3是组合式api(composition api)。巴拉巴拉的。Vue2太过依赖于this。
31. DIFF算法的原理
主要流程:
-
初步对比:
- 从根节点开始,递归地比较新旧虚拟 DOM 树的每个节点。
- 如果节点类型不同,直接替换整个节点。
- 如果节点类型相同,则继续比较节点的属性和子节点。
-
更新属性:
- 对比新旧节点的属性,找出不同并进行更新。
- 新增或删除属性。
- 更新已有属性的值。
-
处理子节点:
-
使用 Keyed Diff 算法,通过节点的
key
属性唯一标识每个节点,从而提高比较效率。主要是建立了一个旧子节点的
key-index
映射表,方便快速判断子节点是否存在,定位子节点。(oldKeyToIndex列表,输入新子节点的key就可判断了。) -
双端比较用于进一步优化子节点的更新,减少节点的移动次数。
-
具体来说,双端比较通过从头和尾同时进行对比,尽可能减少需要移动的节点数量。
假如说就叫旧子节点list,新子节点list,还有对应的头尾指针;后面的判断如果是true就调用更新函数;先比较旧头部节点和新头部节点的key,再比较旧尾节点和新尾节点的key,再旧头和新尾,再旧尾和新头;还没有就直接查找,找不到就立即插入新指针的位置,找到了就根据key获取旧元素的index,插入当前的位置。
-
32.通过路由传递props(少见)
通过配置路由时设定props
const routes = [
{
path: '...',
name: '...',
component: ...,
props: true //就是这里
}
];
有三种模式:
-
布尔值
true
会将params作为props传给组件,组件可以直接接受这个props,props的key就是占位符,比如
/destination/:user
,那组件接受props就是user。 -
对象
只能写死,把这个对象传给组件
const routes = [ { path: '/destination', name: 'destination', component: DestinationComponent, props: { user: 'JohnDoe' } } ]; //组件接受的props就是user
-
函数
这种方法比较灵活,能同时传query形式的参数也能传递params,也最推荐。
const routes = [ { path: '/destination/:user', name: 'destination', component: DestinationComponent, props: route => ({ user: route.params.user, isAdmin: route.query.admin === 'true' }) } ];
总结
专注收集整理Java面试题,不定期给大伙分享面试中的高频题与大厂难题。
如果你觉得文章对你有用,可以点赞和关注哦!❤
欢迎大家在评论区留下你宝贵的建议📢