Vue2 相关问题
下面是当简历项目为vue2 时面试遇到过得问题
1、为什么data 返回值是个函数
为了确保每个组件实例都有独立的、隔离的数据作用域;
假如data是对象,所有组件实例会共享同一个数据对象,如果某个组件实例中修改了这个对象会影响其他的组件实例。
而写成函数的形式,每次创建一个新的组件实例,这个函数都会被调用从而返回一个新的独立的数据对象,每个组件实例都有自己的数据对象,互不干扰。
handlebars
export default {
data() {
return {
message: 'Hello, Vue!'
};
}
2、nextTick 是什么
在vue中,我们变更了数据DOM也会更新,但不是同步生效的,vue会记录所有的更改并在下一次事件循环中批量更新DOM,以避免不必要的DOM更新。但在开发中,我们可能需要数据变更后等DOM 也变更后再执行某种操作。nextTick 方法允许你延迟执行代码,直到 Vue 完成下一个 DOM 更新周期。在该方法的回调中我们可以访问到更新后的DOM
import { nextTick } from 'vue'
function increment() {
state.count++
nextTick(() => {
// 访问更新后的 DOM
})
}
通常使用情况:
1、需要计算DOM最新的大小或位置
2、确保元素有了样式后进行过渡或动画
3、确保DOM数据展示是最新的后再执行某种操作
3、什么是计算属性
模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。使用计算属性来描述依赖响应式状态的复杂逻辑,实现逻辑和结构的分离,同时可以实现复用;计算属性有getter 和 setter ,返回值为一个新的派生数据,计算属性会自动追踪响应式依赖,当依赖变化时重新计算;
javascript
<script setup>
import { reactive, computed } from 'vue'
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})
// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? 'Yes' : 'No'
})
</script>
<template>
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</template>
4、computed 和 wacth 的区别
概念上的区别,computed 是计算属性,返回一个派生的计算结果;watch 是监听器,可以监听指定数据源并监听到变化后触发指定的回调函数;
不同点:
1、computed 支持缓存,只有依赖数据发生改变,才会重新进行计算; watch不支持缓存,数据变,直接会触发相应的操作;
2、computed第一次加载时就监听;watch默认第一次加载时不监听(需要设置immediate:true)
3、不支持异步,当computed内有异步操作时无效,无法监听数据的变化;watch支持异步;
4、computed 有getter 和 setter 方法,watch 可一设置deep:true, immediate:true,flush: 'post'
5、wacth 的flush: 'post'有什么作用
默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。
如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: 'post' 选项:
javascript
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
6、watch 和watchEffect() 的区别
1、回调会立即执行,不需要指定 immediate: true
2、不再需要明确传递数据源,会自动追踪依赖项
以下是代码示例:
javascript
const todoId = ref(1)
const data = ref(null)
watch(
todoId,
async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
},
{ immediate: true }
)
javascript
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
7、计算属性和方法的区别
1.计算属性是一个属性 必须要有返回值 methods不一定
2.计算属性在页面渲染时 不需要加括号 methods必须要加
3.计算属性有缓存,一个计算属性仅会在其响应式依赖更新时才重新计算。 methods没有缓存 从性能上来讲 计算属性更具有优势
8、v-if 和 v-show 的区别
v-if 是 真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做------直到条件第一次变为真时,才会开始渲染条件块。
v-show 就简单得多, 不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 display 属性进行切换。
所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。
9、v-if 和 v-for 一起使用优先级如何,如何解决
在vue2中,v-for的优先级是高于v-if的,如果同时出现,每次渲染都会先执行循环再判断条件,无论如何循环都不可避免,浪费了性能;另外需要注意的是在vue3则完全相反,v-if的优先级高于v-for,所以v-if执行时,它调用的变量还不存在,就会导致异常。
解决办法:
1、将原来使用v-if判断的项目使用计算属性过滤,只要遍历过滤后的列表
2、在外层包裹template,在外层循环,内层判断
javascript
<!--
这会抛出一个错误,因为属性 todo 此时
没有在该实例上定义
-->
<li v-for="todo in todos" v-if="!todo.isComplete">
{{ todo.name }}
</li>
在外新包装一层 <template> 再在其上使用 v-for 可以解决这个问题 (这也更加明显易读):
template
<template v-for="todo in todos">
<li v-if="!todo.isComplete">
{{ todo.name }}
</li>
</template>
10、为什么v-for 需要加key
Vue 默认按照"就地更新"的策略来更新通过 v-for 渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。为了给 Vue 一个提示,以便它可以跟踪每个节点的标识,从而重用和重新排序现有的元素,你需要为每个元素对应的块提供一个唯一的 key attribute。
11、v-on 常见修饰符
.once - 只触发一次回调。
.prevent - 调用 event.preventDefault()阻止默认行为
.stop - 调用 event.stopPropagation()阻止冒泡
.self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。
.capture - 添加事件侦听器时使用 capture 模式,默认情况下是事件冒泡, 如果想变成事件捕获, 那么就需要使用.capture修饰符
12、vue常见指令
1、v-if:根据表达式的值的真假条件渲染元素。在切换时元素及它的数据绑定 / 组件被销毁并重建。
2、v-show:根据表达式之真假值,切换元素的 display CSS 属性。
3、v-for:循环指令,基于一个数组或者对象渲染一个列表,vue 2.0以上必须需配合 key值 使用。
4、v-bind:动态地绑定一个或多个特性,或一个组件 prop 到表达式。
5、v-on:用于监听指定元素的DOM事件,比如点击事件。绑定事件监听器。
6、v-model:实现表单输入和应用状态之间的双向绑定
7、v-pre:跳过这个元素和它的子元素的编译过程。可以用来显示原始 Mustache 标签。跳过大量没有指令的节点会加快编译。
8、v-once:只渲染元素和组件一次。随后的重新渲染,元3 素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能
13、keep-alive 是什么
keep-alive 组件是 vue 的内置组件,用于缓存内部组件实例。这样做的目的在于,keep-alive 内部的组件切回时,不用重新创建组件实例,而直接使用缓存中的实例,一方面能够避免创建组件带来的开销,另一方面可以保留组件的状态。
14、keep-alive的常用属性有哪些?
keep-alive 具有 include 和 exclude 属性,通过它们可以控制哪些组件进入缓存。另外它还提供了 max 属性,通过它可以设置最大缓存数,当缓存的实例超过该数时,vue 会移除最久没有使用的组件缓存
15、与keep-alive相关的生命周期函数是什么,什么场景下会进行使用
受keep-alive的影响,其内部所有嵌套的组件都具有两个生命周期钩子函数,分别是 activated 和 deactivated,它们分别在组件激活和失活时触发。第一次 activated 触发是在 mounted 之后
javascript
<script setup>
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
// 调用时机为首次挂载
// 以及每次从缓存中被重新插入时
})
onDeactivated(() => {
// 在从 DOM 上移除、进入缓存
// 以及组件卸载时调用
})
</script>
16、keep-alive的实现原理是什么?
javascript
keep-alive的实现原理是什么?
// keep-alive 内部的声明周期函数
created () {
this.cache = Object.create(null)
this.keys = []
}
key 数组记录目前缓存的组件 key 值,如果组件没有指定 key 值,则会为其自动生成一个唯一的 key 值
cache 对象以 key 值为键,vnode 为值,用于缓存组件对应的虚拟 DOM
在 keep-alive 的渲染函数中,其基本逻辑是判断当前渲染的 vnode 是否有对应的缓存,如果有,从缓存中读取到对应的组件实例;如果没有则将其缓存。
当缓存数量超过 max 数值时,keep-alive 会移除掉 key 数组的第一个元素。
17、前端路由和后端路由的区别
1、前端路由通常基于URL路径来映射到特定的组件或页面。后端路由根据客户端发送的请求路径和HTTP方法来决定执行哪些逻辑处理。
2、前端路由切换页面时通常不会导致网页的刷新,保持浏览器的历史记录。后端路由可能导致网页的刷新,因为可能需要重新加载整个页面。
3、前端路由可以在不向服务器发送请求的情况下实现页面间的导航。后端路由涉及到与服务器的通信,以确定返回的内容类型(如HTML、JSON等)。
4、前端路由采用的是客户端渲染,后端路由是服务端渲染。
18、全局组件和局部组件的区别
1、全局组件注册在全局,应用中的任意模块都可以使用;局部组件在vue2中需要再使用的组件中注册,vue3中在<script setup>
的单文件组件中直接导入即可使用无需注册。但是局部组件仅在导入的组件中可以使用;
2、全局组件没被使用过的组件在生产打包中不会被自动移除(tree-shaking);
3、全局组件在大型项目中会让依赖关系变得不明确,如果使用过多不利于维护,局部组件显示导入,依赖关系明确。
19、vue中数据流是怎么样的,实际场景中需要修改props怎么办
所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。
导致你想要更改一个 prop 的需求通常来源于以下两种场景:
prop 被用于传入初始值;而子组件想在之后将其作为一个局部数据属性。在这种情况下,最好是新定义一个局部数据属性,从 props 上获取初始值即可:
javascript
export default {
props: ['initialCounter'],
data() {
return {
// 计数器只是将 this.initialCounter 作为初始值
// 像下面这样做就使 prop 和后续更新无关了
counter: this.initialCounter
}
}
}
需要对传入的 prop 值做进一步的转换。在这种情况中,最好是基于该 prop 值定义一个计算属性:
javascript
export default {
props: ['size'],
computed: {
// 该 prop 变更时计算属性也会自动更新
normalizedSize() {
return this.size.trim().toLowerCase()
}
}
}
20、属性透传是什么
透传 attribute"指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on 事件监听器。最常见的例子就是 class、style 和 id
在vue2中,可以通过 $attrs 这个实例属性来访问组件的所有透传 attribute
javascript
export default {
created() {
console.log(this.$attrs)
}
}
在vue3中,你可以在 <script setup>
中使用 useAttrs() API 来访问一个组件的所有透传 attribute:
javascript
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>
如果没有使用 <script setup>,
attrs 会作为 setup() 上下文对象的一个属性暴露:
javascript
export default {
setup(props, ctx) {
// 透传 attribute 被暴露为 ctx.attrs
console.log(ctx.attrs)
}
}
21、插槽是什么
插槽是子组件中提供给父组件使用的一个占位符,用slot标签表示,父组件可以在这个占位符中填充任何模板代码,比如HTML、组件等,填充的内容会替换掉子组件的标签(替换占位符)。
Vue中的插槽大致可以分为默认插槽、具名插槽和作用域插槽三种。
如果只有一个占位符通常会使用默认插槽,而如果有多个slot 则会使用具名插槽就是给slot 添加name属性,父组件插入的内容同样要加上v-slot:插槽名称。作用域插槽是指插槽的内容可以访问父组件的状态,但无法访问子组件的状态;如果想要访问子组件的数据就需要使用作用域插槽,在子组件传入,插槽的内容可以通过prop接收;
传入:
javascript
<slot name="header" message="hello"></slot>
接收:
javascript
<template #header="headerProps">
{{ headerProps }}
</template>
22、组件通信的方式
1、父子组件通过prop、emit 接收和传递
在vue2中的用法:
父组件:
javascript
<template>
<div class="section">
<com-article :articles="articleList" @onEmitIndex="onEmitIndex"></com-article>
<p>{{currentIndex}}</p>
</div>
</template>
子组件:
javascript
<template>
<div>
<div v-for="(item, index) in articles" :key="index" @click="emitIndex(index)">{{item}}</div>
</div>
</template>
<script>
export default {
props: ['articles'],
methods: {
emitIndex(index) {
this.$emit('onEmitIndex', index) // 触发父组件的方法,并传递参数index
}
}
}
</script>
在vue3中的用法:
在使用 <script setup>
的单文件组件中,props 可以使用 defineProps() 宏来声明:
javascript
<script setup>
const props = defineProps(['foo'])
const emit = defineEmits(['inFocus', 'submit'])
function buttonClick() {
emit('submit')
}
console.log(props.foo)
</script>
在没有使用 <script setup>
的组件中,prop 可以使用 props 选项来声明:
javascript
export default {
props: ['foo'],
emits: ['inFocus', 'submit'],
setup(props,ctx) {
// setup() 接收 props 作为第一个参数
console.log(props.foo)
ctx.emit('submit')
}
}
二、事件总线eventBus,在vue3中已被删除,因为在大型项目中使用Event Bus可能会变得难以维护和调试,同时也可能会影响应用程序的性能。
(1)创建事件中心管理组件之间的通信
javascript
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
(2)发送事件
假设有两个兄弟组件firstCom和secondCom:
javascript
<template>
<div>
<first-com></first-com>
<second-com></second-com>
</div>
</template>
<script>
import firstCom from './firstCom.vue'
import secondCom from './secondCom.vue'
export default {
components: { firstCom, secondCom }
}
</script>
(3)接收事件
在secondCom组件中发送事件:
javascript
<template>
<div>求和: {{count}}</div>
</template>
<script>
import { EventBus } from './event-bus.js'
export default {
data() {
return {
count: 0
}
},
mounted() {
EventBus.$on('addition', param => {
this.count = this.count + param.num;
})
}
}
</script>
在上述代码中,这就相当于将num值存贮在了事件总线中,在其他组件中可以直接访问。事件总线就相当于一个桥梁,不用组件通过它来通信。
虽然看起来比较简单,但是这种方法也有不变之处,如果项目过大,使用这种方式进行通信,后期维护起来会很困难。
三、ref / $refs
在vue2中,将ref 用于组件,父组件对子组件的每一个属性和方法都有完全的访问权,通过expose 选项可以用于限制对子组件实例的访问
javascript
export default {
expose: ['publicData', 'publicMethod'],
data() {
return {
publicData: 'foo',
privateData: 'bar'
}
},
methods: {
publicMethod() {
/* ... */
},
privateMethod() {
/* ... */
}
}
}
在上面这个例子中,父组件通过模板引用访问到子组件实例后,仅能访问 publicData 和 publicMethod。
在vue3中,使用了 <script setup>
的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup>
的子组件中的任何东西,除非子组件在其中通过 defineExpose 宏显式暴露:
javascript
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({
a,
b
})
</script>
四、provde 和 inject
依赖注入,
在vue2中的使用:
javascript
export default {
provide: {
message: 'hello!'
}
}
export default {
inject: ['message'],
created() {
console.log(this.message) // injected value
}
}
在vue3中的使用:
javascript
<script setup>
import { provide } from 'vue'
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
import { inject } from 'vue'
const message = inject('message')
</script>
五、全局状态管理
vue2时推荐使用vuex,vue3 推荐使用pinia 做状态管理。相比于 Vuex,Pinia 提供了更简洁直接的 API,并提供了组合式风格的 API,最重要的是,在使用 TypeScript 时它提供了更完善的类型推导。
23、组件生命周期
什么是 vue 生命周期?
对于 vue 来讲,生命周期就是一个 vue 实例从创建到销毁的过程
vue 生命周期的作用是什么?
给予了开发者在不同的生命周期阶段添加业务代码的能力
vue 2生命周期有几个阶段?
1、beforeCreate(创建前):在组件实例初始化完成之后立即调用,在这个阶段数据和方法都不能被访问
2、created(创建后):组件实例处理完了与状态相关的选项后调用,但还未挂载。通常在这个生命周期发起数据请求
3、 beforeMount(载入前):当前阶段虚拟 DOM 已经创建完成,即将开始渲染
4、mounted(载入后):在挂载完成后发生,在当前阶段,真实的 DOM 挂载完毕,数据完成双向绑定,可以访问到 DOM 节点
5、beforeUpdate(更新前):发生在更新之前,也就是响应式数据发生更新,虚拟 DOM 重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。
6、updated(更新后):发生在更新完成之后,当前阶段组件 DOM 已完成更新。
7、beforeDestroy(销毁前):发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器
8、destroyed(销毁后):发生在实例销毁之后,这个时候只剩下了 DOM 空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。
vue3 生命周期有几个阶段?
1、setup() : 开始创建组件之前,在 beforeCreate 和 created 之前执行,创建的是 data 和 method
2、onBeforeMount() : 组件挂载到节点上之前执行的函数;
3、onMounted() : 组件挂载完成后执行的函数;
4、onBeforeUpdate(): 组件更新之前执行的函数;
5、onUpdated(): 组件更新完成之后执行的函数;
6、onBeforeUnmount(): 组件卸载之前执行的函数;
7、onUnmounted(): 组件卸载完成后执行的函数;
keep-alive 独有的生命周期
另外还有 keep-alive 独有的生命周期,分别为 activated 和 deactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 activated 钩子函数;在vue3中对应的分别是 onActivated 和 onDeactivated
24、父子组件的生命周期执行顺序
我们可以发现父子组件在加载的时候,执行的先后顺序为父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted。
25、对vue的理解
定义:
vue 是一套用于构建用户界面的渐进式框架,vue 的核心库只关注视图层。
模式:
是基于MVVM模式实现的一套框架。
在vue中:Model:指的是js中的数据,如对象,数组等等。
View:指的是页面视图viewModel:指的是vue实例化对象,
viewModel:是连接view和model的桥梁,通常要实现一个observer观察者,当数据发生变化,ViewModel能够监听到数据的这种变化,然后通知到对应的视图做自动更新,而当用户操作视图,ViewModel也能监听到视图的变化,然后通知数据做改动,这实际上就实现了数据的双向绑定;
双向数据绑定的原理:
在vue2中实现数据双向绑定是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应监听回调。Object.defineProperty,没有对对象的新属性和数组进行属性劫持;更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。
Vue 2 使用 getter / setters 完全是出于支持旧版本浏览器的限制。而在 Vue 3 中则使用了 Proxy 来创建响应式对象【和原来有什么区别,存在什么问题】,仅将 getter / setter 用于 ref
26、怎么理解vue的渐进式
渐进式就是框架分层,可以一部分嵌入,你可以只用核心的视图渲染功能来开发,也可以使用全家桶,慢慢的过渡
从里向外分别是:视图层渲染--组件机制--路由机制--状态管理--构建工具
27、使用 Object.defineProperty() 来进行数据劫持有什么缺点?
在对一些属性进行操作时,使用这种方法无法拦截,比如通过下标方式修改数组数据或者给对象新增属性,这都不能触发组件的重新渲染
因为 Object.defineProperty 不能拦截到这些操作。
Object.defineProperty,没有对对象的新属性和数组进行属性劫持;更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。
包括七个方法:push,pop,shift,unshift,sort,reverse,splice(Vue 将 data 中的数组进行了原型链重写,指向了自己定义的数组原型方法。这样当调用数组 api 时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化)
在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据劫持。使用Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为 Proxy 是 ES6 的语法。
①对象新属性无法更新视图,删除属性无法更新视图,为什么?怎么办?
javascript
this.$set(this.obj, 'b', 'obj.b')
this.$delete(obj, key)
set()方法相当于手动的去把obj.b处理成一个响应式的属性,此时视图也会跟着改变了。
②直接arr[index] = xxx无法更新视图怎么办?为什么?怎么办?
arr.splice(index, 1, item) 或者 Vue.$set(arr, index, value)
27、vue的优缺点?
优点:
①灵活性:vue渐进式如果我们的应用足够小,可以只使用 vue 的核心库即可;随着应用的规模不断扩大,可以根据需求引入 vue-router、vuex、vue-cli 等其他工具。
②轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十 kb
③ 简单易学:国人开发,中文文档,不存在语言障碍 ,易于理解和学习;
并且支持双向数据绑定、组件化、数据和结构的分离、引入虚拟DOM运行速度快;
④组件化:Vue 通过组件,把一个单页应用中的各种模块拆分到一个一个单独的组件(component)中,我们只要先在父级应用中写好各种组件标签。组件化开发的优点:提高开发效率、方便重复使用、简化调试步骤、提升整个项目的可维护性、便于协同开发。
⑤虚拟DOM:在传统开发中,用 JQuery 或者原生的 JavaScript DOM 操作函数对 DOM 进行频繁操作的时候,浏览器要不停的渲染新的 DOM 树,导致在性能上面的开销特别的高。【虚拟dom有什么好处】
⑥ 双向数据绑定: 通过 MVVM 思想实现数据的双向绑定,Vue 会自动对页面中某些数据的变化做出响应。
缺点:
① 和其他框架一样不利于 SEO,不适合做需要浏览器抓取信息的电商网站,比较适合做后台管理系统。
② SPA的特性,会在首页的时候将所有的js、css数据全部加载,当项目过于庞大时,这个白屏时间就会比较明显。
29、什么是虚拟DOM
虚拟DOM 是一种编程概念,将UI通过数据结构虚拟的表示出来,保存在内存中,其实就是一个JS对象,代表一个虚拟节点;
javascript
const vnode = {
type: 'div',
props: {
id: 'hello'
},
children: [
/* 更多 vnode */
]
}
vue模版编译时就是将其构建为虚拟DOM树,运行时渲染器调用渲染函数然后遍历虚拟DOM树并创建实际的DOM节点,当一个依赖发生变化的时候会创建一个更新后的虚拟DOM树,运行时渲染器使用diff算法遍历对比新旧树,将必要的更新应用到真实的DOM上,从而有效的减少页面渲染的次数,减少修改DOM的重绘重排次数,提高渲染性能。
30、vue 和react 的区别
后续补充
Vue3相关问题
2023年12月31日起,vue2 不再维护,所以猜测现在问的vue3会更多,以下是针对vue3 的问题整理
1、什么是 mixins 混入
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被"混合"进入该组件本身的选项。
// 定义一个混入对象
javascript
var myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}
// 定义一个使用混入对象的组件
var Component = Vue.extend({
mixins: [myMixin]
})
var component = new Component() // => "hello from mixin!"
当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行"合并"。
比如,数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
混入对象的钩子将在组件自身钩子之前调用。
2、什么是组合式函数
组合式函数"(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。组合式函数约定用驼峰命名法命名,并以"use"作为开头。
如果在每个需要获取数据的组件中都要重复这种模式,那就太繁琐了。让我们把它抽取成一个组合式函数:
javascript
// fetch.js
import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
fetch(url)
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
return { data, error }
}
现在我们在组件里只需要:
java
<script setup>
import { useFetch } from './fetch.js'
const { data,
error } = useFetch('...')
</script>
3、组合式函数和混合的区别
Vue 2 的用户可能会对 mixins 选项比较熟悉。它也让我们能够把组件逻辑提取到可复用的单元里。然而 mixins 有三个主要的短板:
1、不清晰的数据来源:当使用了多个 mixin 时,实例上的数据属性来自哪个 mixin 变得不清晰,这使追溯实现和理解组件行为变得困难。这也是我们推荐在组合式函数中使用 ref + 解构模式的理由:让属性的来源在消费组件时一目了然。
2、命名空间冲突:多个来自不同作者的 mixin 可能会注册相同的属性名,造成命名冲突。若使用组合式函数,你可以通过在解构变量时对变量进行重命名来避免相同的键名。
3、隐式的跨 mixin 交流:多个 mixin 需要依赖共享的属性名来进行相互作用,这使得它们隐性地耦合在一起。而一个组合式函数的返回值可以作为另一个组合式函数的参数被传入,像普通函数那样。
基于上述理由,我们不再推荐在 Vue 3 中继续使用 mixin。保留该功能只是为了项目迁移的需求和照顾熟悉它的用户
4、什么是组合式API
是一系列API 的集合,使我们可以使用函数而不是声明选项的方式书写 Vue 组件。
包括:
响应式API ref() 和 reactive()
生命周期狗子 onMounted() 和 onUnmounted()
依赖注入 provide() 和 inject()
5、组合式API 和选项式 API 的区别
1、更好的逻辑复用
组合式 API 最基本的优势是它使我们能够通过组合函数来实现更加简洁高效的逻辑复用。在选项式 API 中我们主要的逻辑复用机制是 mixins,而组合式 API 解决了 mixins 的所有缺陷。
2、更灵活的代码组织
选项式处理相同逻辑关注点的代码被强制拆分在了不同的选项中,位于文件的不同部分。如果我们想要将一个逻辑关注点抽取重构到一个可复用的工具函数中,需要从文件的多个不同部分找到所需的正确片段。组合式同一个逻辑关注点相关的代码被归为了一组,不再需要为了抽象而重新组织代码,大大降低了重构成本。
3、更好的类型推导
用组合式 API 重写的代码可以享受到完整的类型推导,不需要书写太多类型标注。
4、更小的生产包体积
由于 <script setup>
形式书写的组件模板被编译为了一个内联函数,和 <script setup>
中的代码位于同一作用域,被编译的模板可以直接访问 <script setup>
中定义的变量,无需从实例中代理。不像选项式 API 需要依赖 this 上下文对象访问属性。这对代码压缩更友好,因为本地变量的名字可以被压缩,但对象的属性名则不能。
6、vue3 推荐使用pinia 做状态管理,为什么
现有用户可能对 Vuex 更熟悉,它是 Vue 之前的官方状态管理库。由于 Pinia 在生态系统中能够承担相同的职责且能做得更好,因此 Vuex 现在处于维护模式。它仍然可以工作,但不再接受新的功能。对于新的应用,建议使用 Pinia。事实上,Pinia 最初正是为了探索 Vuex 的下一个版本而开发的,因此整合了核心团队关于 Vuex 5 的许多想法。
相比于 Vuex,Pinia 提供了更简洁直接的 API,并提供了组合式风格的 API,最重要的是,在使用 TypeScript 时它提供了更完善的类型推导。
7、从 Vue CLI 迁移到 Vite
Vue CLI 是官方提供的基于 Webpack 的 Vue 工具链,它现在处于维护模式。我们建议使用 Vite 开始新的项目,除非你依赖特定的 Webpack 的特性。在大多数情况下,Vite 将提供更优秀的开发体验。Vite 是一个轻量级的、速度极快的构建工具,对 Vue SFC 提供第一优先级支持。作者是尤雨溪,同时也是 Vue 的作者!
要使用 Vite 来创建一个 Vue 项目,非常简单:
handlebars
$ npm create vue@latest
8、vue2 和 3 响应式原理的区别
在 JavaScript 中有两种劫持 property 访问的方式:getter / setters 和 Proxies。Vue 2 使用 getter / setters 完全是出于支持旧版本浏览器的限制。而在 Vue 3 中则使用了 Proxy 来创建响应式对象,仅将 getter / setter 用于 ref。
9、ref 和 reactive 的区别
1、返回的对象不同,ref() 接收参数,并将其包裹在一个带有 .value 属性的 ref 对象中返回,reactive() 返回的是一个原始对象的 Proxy;
2、数据类型不同,ref() 方法允许我们创建可以使用任何值类型的响应式 ref,reactive() 只能用于对象类型 (对象、数组和如 Map、Set 这样的集合类型)
3、访问方式不同,ref:使用 .value 属性来访问和修改值。reactive:可以直接访问和修改对象或数组的属性或元素,而无需使用 .value。
10、reactive 的缺陷
1、有限的值类型:它只能用于对象类型 (对象、数组和如 Map、Set 这样的集合类型)。它不能持有如 string、number 或 boolean 这样的原始类型。
2、不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地"替换"响应式对象,因为这样的话与第一个引用的响应性连接将丢失:
javascript
let state = reactive({ count: 0 })
// 上面的 ({ count: 0 }) 引用将不再被追踪
// (响应性连接已丢失!)
state = reactive({ count: 1 })
3、对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接:
javascript
const state = reactive({ count: 0 })
// 当解构时,count 已经与 state.count 断开连接
let { count } = state
// 不会影响原始的 state
count++
// 该函数接收到的是一个普通的数字
// 并且无法追踪 state.count 的变化
// 我们必须传入整个对象以保持响应性
callSomeFunction(state.count)
由于这些限制,我们建议使用 ref() 作为声明响应式状态的主要 API。
11、vue3新特性
一、引入了Tree-Shaking,所有的API都通过ES6模块化的方式引入,减少包体积
二、Vue3 没有提供单独的 onBeforeCreate 和 onCreated 方法,因为 setup 本身是在这两个生命周期之前执行的,Vue3 建议我们直接在 setup 中编写这两个生命周期中的代码。用setup代替了 beforeCreate 和 created 这两个生命周期
三、响应式原理的变化,Vue 3 使用了新的响应式系统,将 Proxy 作为其核心,以取代 Vue 2.x 中使用的Object.defineProperty。这样可以更快地追踪变化并更新视图。
四、使用Composition API 管理和组织代码代替选项式
五、不再要求必须有一个根节点,在vue2.x中,要求每个模板必须有一个根节点
六、v-for和v-if优先级,在vue2.x中,在一个元素上同时使用v-for和v-if,v-for有更高的优先级,因此在vue2.x中做性能优化,有一个重要的点就是v-for和v-if不能放在同一个元素上。而在vue3中,v-if比v-for有更高的优先级。
七、Vue 3 使用 TypeScript 重新编写了核心代码,提供了更好的类型支持和开发体验。
Vuex 相关问题
1、vuex是什么
Vuex是一个专为vue.js应用程序开发的状态管理模式。当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:
1、多个视图依赖于同一状态(传参的方法对于多层嵌套的组件将会非常繁琐)
2、来自不同视图的行为需要变更同一状态(通常通过父子组件传递事件,复杂时会产生难以维护的代码)
vuex将共享状态抽取出来,以一个全局单例模式管理,通过一个应用使用一个store实例用来存储应用层级的状态;
如果其他组件需要访问某个全局状态通过store.state.属性名即可访问,若组件需要修改某个全局状态时只能通过调用store.commit('increment') 来提交mutation更改数据;但是mutation 必须是同步函数,如果想要异步的修改需要调用store.dispatch('increment')分发action,再由Action 提交的是 mutation,而不是Action直接变更状态。最后,根据State的变化,渲染到视图上。
2、Vuex核心概念有哪些
1、state => 基本数据(数据源存放地)
2、getters => 相当于store 中的计算属性(如果没有当我们需要根据state的数据派生时每个地方都得使用函数处理一下)
3、mutations => 提交更改数据的方法,同步
4、actions => 像一个装饰器,包裹mutations,使之可以异步。
5、modules => 将一个store实例分割模块
3、Vuex 为什么要分模块
模块:由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。
4、Mutation 和 Action 的区别
mutation是唯一修改state的途径,每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler),这个回调函数就是实际进行状态更改的地方,并且它会接受 state 作为第一个参数。
而Action 可以包含任意异步操作,但不是直接修改state,action内部需要提交mutation来修改状态,action函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此可以调用 context.commit 提交一个 mutation。在视图更新时,是先触发actions在触发mutation。
javascript
store.commit('increment')
Action 提交的是 mutation,而不是直接变更状态。
javascript
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
5、为什么 Vuex 的 mutation 中不能做异步操作?
每个mutation执行完成后都会对应到一个新的状态变更,如果mutation支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难。
6、Vue.js中ajax请求代码应该写在组件的methods中还是vuex的actions中?
一、如果请求来的数据不要被其他组件公用,仅仅在请求的组件内使用,就不需要放入vuex 的state里。
二、如果被其他地方复用,这个很大几率上是需要的,如果需要,请将请求放入action里,方便复用,并包装成promise返回。
7、 Vuex 和 localStorage 的区别
1、应用场景不同,vuex是为vuejs程序开发的,解决组件直接数据共享问题,数据可以做到响应式,而 localStorage是本地存储技术,通过用来保存需要跨页面传递的数据,不能响应式;
2、vuex的数据是存储在内存中的,而localStorage是存储在本地,并且只能存储字符串类型的数据,存储对象需要JSON的stringify和parse方法进行处理。
3、vuex 刷新页面数据会丢失,而localstorage不会
8、Vuex和单纯的全局对象有什么区别?
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样可以方便地跟踪每一个状态的变化,从而能够实现一些工具帮助更好地了解我们的应用
9、mapState 和 mapGetters 是干啥的
使用 mapState 辅助函数帮助我们生成计算属性
javascript
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'
export default {
// ...
computed: mapState({
// 箭头函数可使代码更简练
count: state => state.count,
// 传字符串参数 'count' 等同于 `state => state.count`
countAlias: 'count',
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性
javascript
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}
VueRouter 相关问题
1、路由守卫
vue-router的路由守卫主要用来通过跳转或取消的方式守卫导航。
包括三种:
1、全局前置/钩子:beforeEach、beforeResolve、afterEach
2、路由独享的守卫:beforeEnter
3、组件内的守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
全局的守卫:
beforeEach 在路由跳转前触发,以取消或更改路由的跳转;比如说用于登录验证;
beforeResolve 在路由组件被渲染之前触发,如数据预加载等;
afterEach 是在路由完成跳转后触发,比如修改页面标题;
javascript
const router = createRouter({ ... })
router.beforeEach((to, from) => {
// ...
// 返回 false 以取消导航
return false
});
router.beforeResolve(async to => {
if (to.meta.requiresCamera) {
try {
await askForCameraPermission()
} catch (error) {
if (error instanceof NotAllowedError) {
// ... 处理错误,然后取消导航
return false
} else {
// 意料之外的错误,取消导航并把错误传给全局处理器
throw error
}
}
}
})
router.afterEach((to, from) => {
sendToAnalytics(to.fullPath)
})
路由独享守卫,只有在进入路由时触发,不会在参数哈希等改变时触发。
javascript
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: (to, from) => {
// reject the navigation
return false
},
},
]
组件内守卫:
beforeRouteEnter:在渲染该组件的对应路由被验证前调用,不能获取组件实例 this
!因为当守卫执行时,组件实例还没被创建!
beforeRouteUpdate:在当前路由改变,但是该组件被复用时调用,举例来说,对于一个带有动态参数的路径 /users/:id
,在 /users/1
和 /users/2
之间跳转的时候,由于会渲染同样的 UserDetails
组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 this
beforeRouteLeave:在导航离开渲染该组件的对应路由时调用,与 beforeRouteUpdate
一样,它可以访问组件实例 this
javascript
const UserDetails = {
template: `...`,
beforeRouteEnter(to, from) {
},
beforeRouteUpdate(to, from) {
},
beforeRouteLeave(to, from) {
},
}
2、讲一下完整的导航守卫流程?
导航被触发。
在失活的组件里调用离开守卫beforeRouteLeave(to,from,next)。
调用全局的beforeEach( (to,from,next) =>{} )守卫。
在重用的组件里调用 beforeRouteUpdate(to,from,next) 守卫。
在路由配置里调用beforeEnter(to,from,next)路由独享的守卫。
解析异步路由组件。
在被激活的组件里调用beforeRouteEnter(to,from,next)。
在所有组件内守卫和异步路由组件被解析之后调用全局的beforeResolve( (to,from,next) =>{} )解析守卫。
导航被确认。
调用全局的afterEach( (to,from) =>{} )钩子。
触发 DOM 更新。
3、query和params的区别是什么
1、query用path编写传参地址,而params用name编写传参地址;
2、query传的参数会显示在url地址栏中,而params传参不会显示在地址栏中。
3、query刷新页面时参数不会消失,而params刷新页面时参数会消失;
4、router和route的区别
1、router是VueRouter的一个对象,通过Vue.use(VueRouter)和VueRouter构造函数得到一个router的实例对象,这个对象中是一个全局的对象,他包含了所有的路由包含了许多关键的对象和属性。
2、route是一个跳转的路由对象,每一个路由都会有一个route对象,是一个局部的对象,可以获取对应的name,path,params,query等
$route.path 字符串,等于当前路由对象的路径,会被解析为绝对路径,如 "/home/news"
$route.params 对象,包含路由中的动态片段和全匹配片段的键值对
5、hash和history模式的区别
由于vue是单页面应用,在项目构建过程中只有一个html文件。切换页面的时候既要让url发生变化又不能触发html的重新加载,因此不能使用普通超链接的方式跳转。
通过路由系统将项目的组件和可以访问url路径进行绑定,提供了两种页面跳转和加载模式:
1、hahs模式
hash模式通过修改#后面的url片段,配合window.onhashchange 事件监听hash部分的变化使用JS来实现DOM对象的切换来实现。hahs模式#后的路由从未发送给服务器不需要在服务器层进行特殊处理,但#不美观,也不利于seo
2、hash模式
使用的是window.history 对象的pushState() 方法重写url路径,浏览器会有历史记录,不会触发页面重新加载,不过一旦刷新网页如果服务器未配置路径转发规则会出现404