vue基础
1. Vue的基本原理
Vue实例创建时,vue当中的data属性,通过object.defineProperty定义成为一个响应式的数据,通过getter方法获取相应的值,通过setter方法修改相应的值,在属性被访问和修改时,通知相应的watcher实例 把使用当前属性的元素进行收集起来,当属性的setter的方法被调用时,watcher就会重新计算,通知相关的元素进行更新
2. 双向数据绑定的原理
Vue采用数据劫持(object.defineProperty) 结合发布者-订阅者模式,通过object.defineProperty来劫持各个属性的setter getter (访问器) ,在数据变动时发布消息给订阅者,触发相应的监听回调
1. Observe(劫持器 监听器)先将所有的数据进行递归遍历,通过object.defineProperty来劫持各个属性的setter getter将数据进行劫持起来,给对象中的某个值进行赋值就会触发setter,可以监听到数据的变化
2. 通过compile(模板编译器)来解析模板指令(render渲染的细节),将模板中的变量替换成真实的数据,并通过计算将template转成真正的html在页面展示出来, 把当前属性的依赖每个节点绑定更新函数,添加到watcher订阅者中,一旦数据有变动,收到通知,更新相关的依赖
3. watcher订阅者是Observe(劫持器 监听器)和compile(模板编译器)之间的桥梁 watcher在new自身时往属性订阅器(dep)添加相关属性的依赖,属性自身有一个update()方法,属性调用setter方法时,通过dep.notice()通知watcher来调用属性自身的update()方法,并触发compile中的回调函数,完成视图的更新
4. MVVM整合了Observe(劫持器 监听器) compile(模板编译器) watcher订阅者 通过Observe(劫持器 监听器)来监听model数据变化,
通过compile(模板编译器)来编译模板指令
最后利用watcher订阅者是Observe(劫持器 监听器)和compile(模板编译器)之间的桥梁,达到双向数据绑定的效果
Model层数据发生变化就会被watcher监听到 来通知compile完成视图的重新渲染
3. 使用 Object.defineProperty() 来进行数据劫持有什么缺点?
- 使用 Object.defineProperty()在对一些属性进行操作时是无法拦截的。
- 修改数组中的某条数据或者给对象新增属性和删除属性,都不能触发组件的重新渲染,
- 因为 Object.defineProperty()只是能对现有的属性进行获取和修改不能删除现有属性。
- Vue3.0 而是通过使用 Proxy 对对象进行代理,可以监听到任何方式的数据改变,从而实现数据劫持。
- 使用Proxy 的好处是它可以监听到任何方式的数据改变,缺点是兼容性的问题,因为 Proxy 是 ES6 的语法。
4. MVVM、MVC、MVP的区别
(1)MVC
MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构。
- View 负责数据的显示
- Model 负责存储数据,以及对相应数据的操作。
- 并且 View 和 Model 应用了观察者模式,当 Model 层发生改变的时候它会通知有关 View 层更新页面。
- Controller 层是 View 层和 Model 层的纽带,当用户与页面产生交互的时候,Controller通过调用 Model 层,来完成对 Model 的修改,然后 Model 层再去通知 View 层更新。
(2)MVVM
MVVM 分为 Model、View、ViewModel:
-
Model 负责存储数据,以及对相应数据的操作;
-
View 负责数据的展示;
-
ViewModel负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;
-
Model和View并无直接关联,而是通过ViewModel来进行联系的,
-
Model和ViewModel之间有着双向数据绑定的联系。因此当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步。
这种模式实现了 Model和View的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作DOM。
(3)MVP
- MVC 模式中使用观察者模式,来实现当 Model 层数据发生变化的时候,通知 View 层的更新。 View 层和 Model 层耦合在一起,
- MVP 的模式通过使用 Presenter 来实现对 View 层和 Model 层的解耦。
- MVC 中的Controller 只知道 Model 的接口,因此它没有办法控制 View 层的更新,
- MVP 模式中,View 层的接口暴露给了 Presenter 因此可以在 Presenter 中将 Model 的变化和 View 的变化绑定在一起,以此来实现 View 和 Model 的同步更新。这样就实现了对 View 和 Model 的解耦,
5. Computed 和 Watch 的区别
Computed(计算属性):
-
依赖其他响应式属性计算得出的结果。
-
自动缓存计算结果,只有在依赖属性变化时才会重新计算。
-
可以在模板中直接使用Computed属性,会自动追踪依赖关系。
-
不支持异步,当computed中有异步操作时,无法监听数据的变化
Watch(监听器):
-
用于监听特定数据的变化,并在变化时执行自定义的操作。
-
可以监听单个或多个数据的变化,包括深度监听对象或数组的内部属性的变化。
-
适合执行异步、复杂或耗时的操作,如发送网络请求或触发副作用操作。
-
Watch可以提供先前值和当前值的参数,以便在变化时执行特定逻辑。
总结:
-
Computed用于声明计算属性,根据依赖自动计算,适合简单的计算逻辑。
-
Watch用于监听数据的变化,在变化时触发特定操作,适合处理复杂、异步操作。
-
Computed是给模板使用的属性,而Watch是监听数据的变化执行对应的副作用操作。
watch和watchEffect的区别
- watch
watch(特定数据,(新值,旧值)=>{},{ deep: true })
{ immediate: true }创建监听以后,立即执行
监听具体的某一个数据的变化,变化时执行相应的操作,支持异步操作
监听响应式对象,默认开启深度监听
watch([x, () => y.value], ([newX, newY]) => {},{deep:true})
监听多个数据的变化
- watchEffect
watchEffect(() => {
console.log('doubled的值变为:', doubled.value);
});
回调函数会立即执行,自动追踪相关的依赖,依赖发生改变以后,回调函数再次执行
取消事件监听
事件监听返回一个回调函数,调用回调函数来取消事件监听
const unwatch = watchEffect(() => {})
当该侦听器不再需要时 调用unwatch()
<template>
<div>
<input v-model="count" type="text" />
<p>Count doubled: {{ doubled }}</p>
</div>
</template>
<script>
import { ref, watch, watchEffect } from 'vue';
export default {
setup() {
const count = ref(1);
const doubled = ref(2);
// 使用watch来监视count的变化
watch(count, (newVal, oldVal) => {
doubled.value = newVal * 2;
console.log(`count值发生了变化,新值为${newVal},旧值为${oldVal}`);
});
// 使用watchEffect来自动追踪其依赖并执行响应的计算
watchEffect(() => {
console.log('doubled的值变为:', doubled.value);
// 这里可以进行一些副作用的操作,比如根据doubled的值更新UI或者进行网络请求等操作
});
return {
count,
doubled
};
}
};
</script>
6. Computed 和 Methods 的区别
不同点:
- computed: 计算属性是基于它们的依赖进行缓存的,只有在它的相关依赖发生改变时才会重新求值;
- method 调用总会执行该函数。
7. slot是什么?有什么作用?原理是什么?
-
插槽slot作为插槽的出口,父组件提供的插槽内容将在哪里被渲染。
-
使组件更加灵活和具有可复用性。
-
默认插槽:又名匿名插槽,没有name属性值,一个组件内只有有一个匿名插槽。
-
具名插槽:带有name属性的slot,一个组件可以出现多个具名插槽。
-
作用域插槽:可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件
实现原理:当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.$slot中,默认插槽为vm.$slot.default,具名插槽为vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到slot标签,使用$slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
8. 过滤器的作用,如何实现一个过滤器
在Vue中使用filters来过滤数据,filters不会修改数据,而是过滤数据,改变用户看到的输出
使用场景:
- 时间、价格等数据格式的输出显示。
- 比如后端返回一个 年月日的日期字符串,前端需要展示为 多少天前 的数据格式,此时就可以用
fliters过滤器来处理数据。
过滤器是一个函数,它会把表达式中的值始终当作函数的第一个参数。
过滤器用在插值表达式 {{ }} 和 v-bind 表达式 中,然后放在操作符“ | ”后面进行指示。
例如,在显示金额,给商品价格添加单位:
<li>商品价格:{{item.price | filterPrice}}</li>
filters: {
filterPrice (price) {
return price ? ('¥' + price) : '--'
}
}
9. 如何保存页面的当前的状态
两种情况:
- 前组件会被卸载
- 前组件不会被卸载
组件会被卸载:
(1)将状态存储在LocalStorage / SessionStorage
在组件即将被销毁的生命周期中在 LocalStorage / SessionStorage 中把当前组件的 state 通过 JSON.stringify() 储存下来
优点:
- 兼容性好,不需要额外库或工具。
- 简单快捷,基本可以满足大部分需求。
缺点:
- 状态通过 JSON 方法储存(相当于深拷贝),如果状态中有特殊情况(比如 Date 对象、Regexp 对象等)的时候会得到字符串而不是原来的值。
(2)路由传值
通过 react-router 的 Link 组件的 prop —— to 可以实现路由间传递参数的效果。
在这里需要用到 state 参数,在 B 组件中通过 history.location.state 就可以拿到 state 值,保存它。返回 A 组件时再次携带 state 达到路由状态保持的效果。
优点:
- 简单快捷,不会污染 LocalStorage / SessionStorage。
- 可以传递 Date、RegExp 等特殊对象
缺点:
- 如果 A 组件可以跳转至多个组件,那么在每一个跳转组件内都要写相同的逻辑。
组件不会被卸载:
(1)单页面渲染
要切换的组件作为子组件,父组件中正常储存页面状态。
优点:
- 代码量少
- 不需要考虑状态传递过程中的错误
缺点:
- 增加 A 组件维护成本
- 需要传入额外的 prop 到 B 组件
- 无法利用路由定位页面
用keep-alive来缓存页面,当组件在keep-alive内被切换时组件的activated、deactivated这两个生命周期钩子函数会被执行 被包裹在keep-alive中的组件的状态将会被保留:
<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 // 需要被缓存
}
},
10. 常见的事件修饰符及其作用
.stop:等同于event.stopPropagation(),防止事件冒泡;.prevent:等同于event.preventDefault(),取消默认行为;.capture:与事件冒泡的方向相反,事件捕获由外到内;.self:只会触发自己范围内的事件,不包含子元素;.once:只会触发一次。
11. v-if、v-show、v-html 的原理
- v-if生成vnode的时候会忽略对应节点,render的时候就不会渲染;
- v-show生成vnode,render的时候也会渲染成真实节点,只是在render过程中修改了display;
- v-html设置innerHTML为v-html的值。
12. v-if和v-show的区别
- 手段:v-if是动态的向DOM树内添加或者删除DOM元素;v-show是通过设置DOM元素的display属性;
- 编译过程:v-if切换有一个局部编译/卸载的过程,;v-show只是简单的基于css切换;
- 编译条件:v-if只有在条件第一次变为真时才开始局部编译; v-show是在任何条件下,都被编译,然后被缓存,而且DOM元素保留;
- 性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;
13. v-model 是如何实现的,语法糖实际是什么?
(1)作用在表单元素上 动态绑定了 input 的 value 指向了 messgae 变量,并且在触发 input 事件的时候去动态把 message设置为目标值:
<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;
(2)作用在组件上 在自定义组件中,v-model 默认会利用名为 value 的 prop和名为 input 的事件
本质是一个父子组件通信的语法糖,通过prop和$.emit实现。 因此父组件 v-model 语法糖本质上可以修改为:
<child :value="message" @input="function(e){message = e}"></child>
在组件的实现中,可以通过 v-model属性来配置子组件接收的prop名称,以及派发的事件名称。 例子:
// 父组件
<aa-input v-model="aa"></aa-input>
// 等价于
<aa-input v-bind:value="aa" v-on:input="aa=$event.target.value"></aa-input>
// 子组件:
<input v-bind:value="aa" v-on:input="onmessage"></aa-input>
props:{value:aa,}
methods:{
onmessage(e){
$emit('input',e.target.value)
}
}
15. v-model 可以被用在自定义组件上吗?如果可以,如何使用?
可以。v-model 实际上是一个语法糖,如:
<input v-model="searchText">
实际上相当于:
<input
v-bind:value="searchText"
v-on:input="searchText = $event.target.value"
>
用在自定义组件上也是同理:
<custom-input v-model="searchText">
相当于:
<custom-input
v-bind:value="searchText"
v-on:input="searchText = $event"
></custom-input>
显然,custom-input 与父组件的交互如下:
- 父组件将
searchText变量传入custom-input 组件,使用的 prop 名为value; - custom-input 组件向父组件传出名为
input的事件,父组件将接收到的值赋值给searchText;
所以,custom-input 组件的实现应该类似于这样:
Vue.component('custom-input', {
props: ['value'],
template: `
<input
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
`
})
16. data为什么是一个函数而不是对象
data 是一个对象,那么子组件中的 data 属性值会互相污染,产生副作用。 data 是一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的 data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。
17. 对keep-alive的理解,它是如何实现的,具体缓存的是什么?
实现组件缓存,当组件切换时不会对当前组件进行卸载。
参数
keep-alive接收三个参数:
include:可传字符串、正则表达式、数组,名称匹配成功的组件会被缓存exclude:可传字符串、正则表达式、数组,名称匹配成功的组件不会被缓存max:可传数字,限制缓存组件的最大数量,超过max则按照LRU算法进行置换
include和exclude,传数组情况居多。
-
两个生命周期 activated/deactivated
-
activated 组件激活时调用
-
deactivated 组件失活时调用
-
created:初始化一个cache、keys,cache用来存缓组件的虚拟dom集合,keys用来存缓存组件的key集合。 -
mounted:实时监听include、exclude这两个的变化,并执行相应操作。 -
destroyed:删除掉所有缓存相关的东西。 在destroyed组件销毁时for循环执行pruneCacheEntry函数- 1:遍历集合,执行所有缓存组件的$destroy方法
- 2:将cache对应key的内容设置为null
- 3:删除keys中对应的元素
使用场景
- Tab 页面切换:
在一个页面中有多个 Tab,每个 Tab 对应一个组件,使用<keep-alive>可以在 Tab 切换时保持各个 Tab 组件的状态,避免每次切换 Tab 都重新渲染组件。 - 路由页面缓存:
对于一些频繁切换的路由页面,可以使用<keep-alive>缓存路由组件,提高路由切换时的效率,并且可以保持页面状态。 - 模态框/对话框:
当打开模态框或对话框时,如果希望关闭后再次打开时能够保持上一次的状态,可以使用<keep-alive>缓存模态框的组件。 - 列表/表格数据:
对于需要频繁展示的列表或表格数据,可以通过<keep-alive>缓存列表或表格组件,以保持数据的加载状态和滚动位置,提升用户体验。 - 含有表单数据的页面:
在包含表单数据的页面中,通过<keep-alive>缓存页面组件,可以避免用户在填写表单数据后切换页面导致数据丢失,提供更好的用户体验。
18. $nextTick 原理及作用
- nextTick 中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法
- 在vue生命周期中,如果在created()钩子进行DOM操作,也一定要放在
nextTick()的回调函数中。
19. Vue 中给 data 中的对象属性添加一个新的属性时会发生什么?如何解决?
- 新增属性没有被Vue转换为响应式的属性,不会触发视图的更新,这时就需要使用Vue的全局 api $set()
- $set()方法相当于手动的去处理成一个响应式的属性
20. Vue中封装的数组方法有哪些,其如何实现页面更新
在Vue中,对响应式处理利用的是Object.defineProperty对数据进行拦截,而这个方法并不能监听到数组内部变化,所以需要对这些操作进行一个改写,让Vue能监听到其中的变化。 那Vue是如何实现让这些数组方法实现元素的实时更新的呢,下面是Vue中对这些方法的封装:
// 缓存数组原型
const arrayProto = Array.prototype;
// 实现 arrayMethods.__proto__ === Array.prototype
export const arrayMethods = Object.create(arrayProto);
// 需要进行功能拓展的方法
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse"
];
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function(method) {
// 缓存原生数组方法
const original = arrayProto[method];
def(arrayMethods, method, function mutator(...args) {
// 执行并缓存原生数组功能
const result = original.apply(this, args);
// 响应式处理
const ob = this.__ob__;
let inserted;
switch (method) {
// push、unshift会新增索引,所以要手动observer
case "push":
case "unshift":
inserted = args;
break;
// splice方法,如果传入了第三个参数,也会有索引加入,也要手动observer。
case "splice":
inserted = args.slice(2);
break;
}
//
if (inserted) ob.observeArray(inserted);// 获取插入的值,并设置响应式监听
// notify change
ob.dep.notify();// 通知依赖更新
// 返回原生数组方法的执行结果
return result;
});
});
简单来说就是,重写了数组中的那些原生方法,首先获取到这个数组的__ob__,也就是它的Observer对象,如果有新的值,就调用observeArray继续对新的值观察变化(也就是通过target__proto__ == arrayMethods来改变了数组实例的型),然后手动调用notify,通知渲染watcher,执行update。
21. Vue 单页应用与多页应用的区别
概念:
- SPA单页面应用只有一个主页面的应用,一开始只需要加载一次js、css等相关资源。单页应用跳转,就是切换相关组件,仅仅刷新局部资源。
- MPA多页面应用,指有多个独立页面的应用,每个页面必须重复加载js、css等相关资源。多页应用跳转,需要整页资源刷新。
区别:
22. Vue template 到 render 的过程
Vue的template并不是真正的html结构,不是W3C的规范语法,浏览器不认得v-show类型的指令
先通过调用parse方法将template转化为ast(抽象语法树) 它是一种用JS对象的形式来描述整个模板 解析过程是利用正则表达式顺序解析模板,当解析到开始标签,闭合标签,文本的时候会分别执行对应的回调函数,ast以节点为单位来描述一个一个的类型 ast元素节点总共三种类型 type为1表示普通元素 2为表达式 3为纯文本
对静态节点做优化
通过optimize会深度遍历ast ,将ast中的静态节点区分出来,并做一个标记,由于静态节点的DOM不会改变,ast更新时不会重复计算静态节点,性能得到了优化
Ast转render函数
23. Vue data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗?
不会立即同步执行重新渲染。Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环tick中,Vue 刷新队列并执行实际(已去重的)工作。
24. 简述 mixin、extends 的覆盖逻辑
mixin 和 extends均是用于合并、拓展组件的,两者均通过 mergeOptions 方法实现合并。
- Vue 的 mixin 用来抽离多个组件之间公共的业务逻辑,实现复用
<template>
<div class="about">
<div>{{ name }}</div>
<button @click="get">获取了</button>
</div>
</template>
<script>
import {myMixin} from '../mixin'
export default {
//使用mixins
mixins: [myMixin]
}
</script>
- extends 主要是为了便于扩展单文件组件,接收一个对象或构造函数。将传入组件的 options 和当前组件的 options 进行了合并
25.描述下Vue自定义指令
指令本质上是装饰器,是 vue 对 HTML 元素的扩展,给 HTML 元素增加自定义功能。vue 编译 DOM 时,会找到指令对象,执行指令的相关方法。
- 全局定义:
Vue.directive("focus",{}) - 局部定义:
directives:{focus:{}}
自定义指令有五个生命周期(也叫钩子函数),分别是 bind、inserted、update、componentUpdated、unbind
1. bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
2. inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
3. update:被绑定于元素所在的模板更新时调用,而无论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。
4. componentUpdated:被绑定元素所在模板完成一次更新周期时调用。
5. unbind:只调用一次,指令与元素解绑时调用。
原理
1.在生成 ast 语法树时,遇到指令会给当前元素添加 directives 属性
2.通过 genDirectives 生成指令代码
3.在 patch 前将指令的钩子提取到 cbs 中,在 patch 过程中调用对应的钩子
4.当执行指令对应钩子函数时,调用对应指令定义的方法
26. 子组件可以直接改变父组件的数据吗?
子组件不可以直接改变父组件的数据。这样做主要是为了维护父子组件的单向数据流。
Vue提倡单向数据流,即父级 props 的更新会流向子组件。这是为了防止意外的改变父组件状态,
**只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。
27. Vue是如何收集依赖的?
在初始化 Vue 的每个组件时,会对组件的 data 进行初始化,就会将由普通对象变成响应式对象,在这个过程中便会进行依赖收集的相关逻辑,如下所示∶
function defieneReactive (obj, key, val){
const dep = new Dep();
...
Object.defineProperty(obj, key, {
...
get: function reactiveGetter () {
if(Dep.target){
dep.depend();
...
}
return val
}
...
})
}
以上只保留了关键代码,主要就是 const dep = new Dep()实例化一个 Dep 的实例,然后在 get 函数中通过 dep.depend() 进行依赖收集。 (1)Dep Dep是整个依赖收集的核心,其关键代码如下:
class Dep {
static target;
subs;
constructor () {
...
this.subs = [];
}
addSub (sub) {
this.subs.push(sub)
}
removeSub (sub) {
remove(this.sub, sub)
}
depend () {
if(Dep.target){
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subds.slice();
for(let i = 0;i < subs.length; i++){
subs[i].update()
}
}
}
Dep 是一个 class ,其中有一个关 键的静态属性 static,它指向了一个全局唯一 Watcher,保证了同一时间全局只有一个 watcher 被计算,另一个属性 subs 则是一个 Watcher 的数组,所以 Dep 实际上就是对 Watcher 的管理,再看看 Watcher 的相关代码∶
(2)Watcher
class Watcher {
getter;
...
constructor (vm, expression){
...
this.getter = expression;
this.get();
}
get () {
pushTarget(this);
value = this.getter.call(vm, vm)
...
return value
}
addDep (dep){
...
dep.addSub(this)
}
...
}
function pushTarget (_target) {
Dep.target = _target
}
Watcher 是一个 class,它定义了一些方法,其中和依赖收集相关的主要有 get、addDep 等。
(3)过程
在实例化 Vue 时,依赖收集的相关过程如下∶ 初 始 化 状 态 initState , 这 中 间 便 会 通 过 defineReactive 将数据变成响应式对象,其中的 getter 部分便是用来依赖收集的。 初始化最终会走 mount 过程,其中会实例化 Watcher ,进入 Watcher 中,便会执行 this.get() 方法,
updateComponent = () => {
vm._update(vm._render())
}
new Watcher(vm, updateComponent)
get 方法中的 pushTarget 实际上就是把 Dep.target 赋值为当前的 watcher。
this.getter.call(vm,vm),这里的 getter 会执行 vm._render() 方法,在这个过程中便会触发数据对象的 getter。那么每个对象值的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this)。刚才 Dep.target 已经被赋值为 watcher,于是便会执行 addDep 方法,然后走到 dep.addSub() 方法,便将当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。所以在 vm._render() 过程中,会触发所有数据的 getter,这样便已经完成了一个依赖收集的过程。
28. 对 React 和 Vue 的理解,它们的异同
-
-
数据驱动:
- React 使用单向数据流,通过props来传递数据,通过回调函数来处理数据变化。
- Vue 使用双向数据绑定,通过v-model指令可以实现表单数据的双向绑定。
-
-
-
语法:
- React 使用 JSX,它将组件的结构和行为以一种类似 HTML 的语法嵌入到 JavaScript 代码中。
- Vue 使用类似 HTML 的模板语法,可以直接在模板中编写组件的结构和交互行为。
-
-
- 状态管理:
- 在React中,常用的状态管理方案是Redux,而在Vue中,推荐使用Vuex。
-
- diff算法的不同
- react主要使用diff队列保存需要更新哪些DOM,得到patch树,再统一操作批量更新DOM。 Vue 使用双向指针,边对比,边更新DOM
相同之处
-
- 组件化:
- React 和 Vue 都采用了组件化的思想,允许开发者将UI划分为独立的、可复用的组件来构建应用。
-
- 虚拟 DOM:
- React 和 Vue 都使用虚拟 DOM 技术,使得页面更新更加高效,并能够提供更好的性能。
29. Vue的优点
- 响应式数据绑定:Vue采用了响应式的数据绑定机制,数据变化时可自动更新视图,减少手动操作DOM。
- 组件化开发:Vue鼓励组件化开发,提高代码复用性和可维护性,使得开发过程更高效。
- 性能优化:Vue采用虚拟DOM和提供性能优化工具,帮助开发者提升应用性能。
30. assets和static的区别
相同点: assets 和 static 两个都是存放静态资源文件
不相同点: assets 中存放的资源文件在项目打包时,会进行打包上传,最终也都会放置在 static 文件中一同上传至服务器。static 中放置的资源文件就不会要走打包,直接上传至服务器。 static 中的资源文件由于没有进行压缩等操作,所以文件的体积也就相对于 assets 中打包后的文件大
31. delete和Vue.delete删除数组的区别
delete只是被删除的元素变成了empty/undefined其他的元素的键值还是不变。Vue.delete直接删除了数组元素 改变了数组的长度。
32. vue如何监听对象或者数组某个属性的变化
当在项目中直接设置数组的某一项的值,或者直接设置对象的某个属性值,这个时候,你会发现页面并没有更新。这是因为Object.defineProperty()限制,监听不到变化。
解决方式:
- this.$set(你要改变的数组/对象,你要改变的位置/key,你要改成什么value)
this.$set(this.arr, 0, "OBKoro1"); // 改变数组this.$set(this.obj, "c", "OBKoro1"); // 改变对象
- 调用以下几个数组的方法
splice()、 push()、pop()、shift()、unshift()、sort()、reverse()
vue源码里缓存了array的原型链,然后重写了这几个方法,触发这几个方法的时候会observer数据,意思是使用这些方法不用再进行额外的操作,视图自动进行更新。 推荐使用splice方法会比较好自定义,因为splice可以在数组的任何位置进行删除/添加操作
vm.$set 的实现原理是:
- 如果目标是数组,直接使用数组的 splice 方法触发相应式;
- 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)
35. 对SSR的理解
SSR 也就是服务端渲染,也就是将 Vue 在客户端把标签渲染成 HTML 的工作放在服务端完成,然后再把 html 直接返回给客户端。
优点:
SSR 有着更好的 SEO、并且首屏加载速度更快
缺点: 开发条件会受到限制,服务器端渲染只支持 beforeCreate 和 created 两个钩子,
服务器会有更大的负载需求
36. Vue的性能优化有哪些
(1)编码阶段
- 尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher
- v-if和v-for不能连用
- 如果需要使用v-for给每项元素绑定事件时使用事件代理
- SPA 页面采用keep-alive缓存组件
- 在更多的情况下,使用v-if替代v-show
- key保证唯一
- 使用路由懒加载、异步组件
- 防抖、节流
- 第三方模块按需导入
- 长列表滚动到可视区域动态加载
- 图片懒加载
(2)SEO优化
- 预渲染
- 服务端渲染SSR
(3)打包优化
- 压缩代码
- Tree Shaking/Scope Hoisting
- 使用cdn加载第三方模块
- 多线程打包happypack
- splitChunks抽离公共文件
- sourceMap优化
(4)用户体验
- 骨架屏
- PWA
- 还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。
37. 对 SPA 单页面的理解,它的优缺点分别是什么?
SPA仅在页面初始化时加载相应的 HTML、JavaScript 和 CSS。利用路由机制实现 HTML 内容的变换,避免页面的重新加载。
优点:
- 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
- 基于上面一点,SPA 相对对服务器压力小;
- 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;
缺点:
- 初次加载耗时多:初始加载大量的css,js文件,部分页面按需加载;
- 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
- SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。
38. template和jsx的有什么分别?
- JSX具有更高的灵活性,在复杂的组件中,更具有优势
- template 更符合视图与逻辑分离的习惯,更简单、更直观、更好维护。
- JSX,它将组件的结构和行为以一种类似 HTML 的语法嵌入到 JavaScript 代码中。
- template 使用类似 HTML 的模板语法,可以直接在模板中编写组件的结构和交互行为。
39. vue初始化页面闪动问题
vue初始化之前,由于div是不归vue管的
首先:在css里加上以下代码:
[v-cloak] { display: none;}
如果没有彻底解决问题,则在根元素加上style="display: none;" :style="{display: 'block'}"
40. MVVM的优缺点?
优点:
-
模型和视图相分离:视图负责展示数据,模型则负责处理业务逻辑,从而降低了各个部分之间的耦合性。使得代码更加清晰可读并且易于维护。
-
双向数据绑定:当模型中的数据发生变化,相关的视图会自动更新。
-
可测试性:由于视图和业务逻辑的分离,视图模型中的业务逻辑可以更容易地进行单元测试。
-
提高开发效率:由于数据绑定的存在,可以减少编写重复的界面更新代码的工作量,提高开发效率。
-
可维护性:模型、视图和视图模型的分层设计使得代码更加结构化和可维护。开发者可以更容易地理解和修改特定功能的实现。
缺点:
-
性能开销:MVVM模式在处理视图和数据之间的双向绑定时,会引入一定的性能开销。特别是在处理大规模数据和复杂计算时,可能会对性能产生一定的影响。
-
灵活性限制:MVVM模式在数据和视图之间有严格的约定,这对于一些复杂的交互场景可能存在一定的限制。在需要灵活控制数据和视图交互的情况下,可能需要额外的工作来实现。
生命周期
1. 说一下Vue的生命周期
beforeCreate 创建前,在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问
created 创建后。组件实例已创建完成,数据和事件回调已初始化,但尚未挂载到DOM上。
beforeMount 挂载前:相关的 render 函数首次被调用。
mounted 挂载后,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom 节点
beforeUpdate 更新前,此时数据是新的,DOM是旧的。
updated 更新后,当前阶段组件 Dom 已完成更新。
beforeDestroy 销毁前。在这一步,实例仍然完全可用。清除计时器。
destroyed 销毁后。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。
activated keep-alive 专属,组件被激活时调用
deactivated keep-alive 专属,组件被销毁时调用
异步请求在哪一步发起?
可以在钩子函数 created、beforeMount、mounted 中进行异步请求,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
如果异步请求不需要依赖 Dom 推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
- 能更快获取到服务端数据,减少页面 loading 时间;
- ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;
2. Vue 子组件和父组件执行顺序
加载渲染过程:
- 父组件 beforeCreate
- 父组件 created
- 父组件 beforeMount
- 子组件 beforeCreate
- 子组件 created
- 子组件 beforeMount
- 子组件 mounted
- 父组件 mounted
更新过程:
- 父组件 beforeUpdate
- 子组件 beforeUpdate
- 子组件 updated
- 父组件 updated
销毁过程:
- 父组件 beforeDestroy
- 子组件 beforeDestroy
- 子组件 destroyed
- 父组件 destoryed
3. created和mounted的区别
created在组件实例被创建后执行,未挂载到 DOM 树上,无法访问到组件的 DOMmounted在组件实例被挂载到 DOM 树之后执行,可以访问到组件的 DOM 元素
4. 一般在哪个生命周期请求异步数据
我们可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
- 能更快获取到服务端数据,减少页面加载时间,用户体验更好;
- SSR不支持 beforeMount 、mounted 钩子函数,放在 created 中有助于一致性。
5. keep-alive 中的生命周期哪些
keep-alive用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
如果为一个组件包裹了 keep-alive,那么它会多出两个生命周期:deactivated、activated。同时组件销毁前后的生命周期不会再被触发,因为组件不会被真正销毁。
activated 组件被激活时调用
deactivated 组件被销毁时调用
组件通信
组件通信的方式如下:
(1) props / $emit
父组件通过props向子组件传递数据,子组件通过$emit和父组件通信
1. 父组件向子组件传值
props只能是父组件向子组件进行传值,props使得父子组件之间形成了一个单向下行绑定。子组件的数据会随着父组件不断更新。props可以显示定义一个或一个以上的数据,对于接收的数据,可以是各种数据类型,同样也可以传递一个函数。props属性名规则:若在props中使用驼峰形式,模板中需要使用短横线的形式
// 父组件
<template>
<div id="father">
<son :msg="msgData" :fn="myFunction"></son>
</div>
</template>
<script>
import son from "./son.vue";
export default {
name: father,
data() {
msgData: "父组件数据";
},
methods: {
myFunction() {
console.log("vue");
}
},
components: {
son
}
};
</script>
// 子组件
<template>
<div id="son">
<p>{{msg}}</p>
<button @click="fn">按钮</button>
</div>
</template>
<script>
export default {
name: "son",
props: ["msg", "fn"]
};
</script>
2. 子组件向父组件传值
$emit绑定一个自定义事件,当这个事件被执行的时就会将参数传递给父组件,而父组件通过v-on监听并接收参数。
// 父组件
<template>
<div class="section">
<com-article :articles="articleList" @onEmitIndex="onEmitIndex"></com-article>
<p>{{currentIndex}}</p>
</div>
</template>
<script>
import comArticle from './test/article.vue'
export default {
name: 'comArticle',
components: { comArticle },
data() {
return {
currentIndex: -1,
articleList: ['红楼梦', '西游记', '三国演义']
}
},
methods: {
onEmitIndex(idx) {
this.currentIndex = idx
}
}
}
</script>
//子组件
<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>
(2)eventBus事件总线($emit / $on)
eventBus事件总线适用于父子组件、非父子组件等之间的通信,使用步骤如下: (1)创建事件中心管理组件之间的通信
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
(2)发送事件 假设有两个兄弟组件firstCom和secondCom:
<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>
在firstCom组件中发送事件:
<template>
<div>
<button @click="add">加法</button>
</div>
</template>
<script>
import {EventBus} from './event-bus.js' // 引入事件中心
export default {
data(){
return{
num:0
}
},
methods:{
add(){
EventBus.$emit('addition', {
num:this.num++
})
}
}
}
</script>
(3)接收事件 在secondCom组件中发送事件:
<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值存贮在了事件总线中,在其他组件中可以直接访问。事件总线就相当于一个桥梁,不用组件通过它来通信。
虽然看起来比较简单,但是这种方法也有不变之处,如果项目过大,使用这种方式进行通信,后期维护起来会很困难。
(3)依赖注入(provide / inject)
这种方式就是Vue中的依赖注入,该方法用于父子组件之间的通信。当然这里所说的父子不一定是真正的父子,也可以是祖孙组件,在层数很深的情况下,可以使用这种方法来进行传值。就不用一层一层的传递了。
provide / inject是Vue提供的两个钩子,和data、methods是同级的。并且provide的书写形式和data一样。
provide钩子用来发送数据或方法inject钩子用来接收数据或方法
在父组件中:
provide() {
return {
num: this.num
};
}
在子组件中:
inject: ['num']
还可以这样写,这样写就可以访问父组件中的所有属性:
provide() {
return {
app: this
};
}
data() {
return {
num: 1
};
}
inject: ['app']
console.log(this.app.num)
注意: 依赖注入所提供的属性是非响应式的。
(3)ref / $refs
这种方式也是实现父子组件之间的通信。
ref: 这个属性用在子组件上,它的引用就指向了子组件的实例。可以通过实例来访问组件的数据和方法。
在子组件中:
export default {
data () {
return {
name: 'JavaScript'
}
},
methods: {
sayHello () {
console.log('hello')
}
}
}
在父组件中:
<template>
<child ref="child"></component-a>
</template>
<script>
import child from './child.vue'
export default {
components: { child },
mounted () {
console.log(this.$refs.child.name); // JavaScript
this.$refs.child.sayHello(); // hello
}
}
</script>
(4)$parent / $children
- 使用
$parent可以让组件访问父组件的实例(访问的是上一级父组件的属性和方法) - 使用
$children可以让组件访问子组件的实例,但是,$children并不能保证顺序,并且访问的数据也不是响应式的。
在子组件中:
<template>
<div>
<span>{{message}}</span>
<p>获取父组件的值为: {{parentVal}}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Vue'
}
},
computed:{
parentVal(){
return this.$parent.msg;
}
}
}
</script>
在父组件中:
// 父组件中
<template>
<div class="hello_world">
<div>{{msg}}</div>
<child></child>
<button @click="change">点击改变子组件值</button>
</div>
</template>
<script>
import child from './child.vue'
export default {
components: { child },
data() {
return {
msg: 'Welcome'
}
},
methods: {
change() {
// 获取到子组件
this.$children[0].message = 'JavaScript'
}
}
}
</script>
在上面的代码中,子组件获取到了父组件的parentVal值,父组件改变了子组件中message的值。 需要注意:
- 通过
$parent访问到的是上一级父组件的实例,可以使用$root来访问根组件的实例 - 在组件中使用
$children拿到的是所有的子组件的实例,它是一个数组,并且是无序的 - 在根组件
#app上拿$parent得到的是new Vue()的实例,在这实例上再拿$parent得到的是undefined,而在最底层的子组件拿$children是个空数组 $children的值是数组,而$parent是个对象
(5)$attrs / $listeners
考虑一种场景,如果A是B组件的父组件,B是C组件的父组件。如果想要组件A给组件C传递数据,这种隔代的数据,该使用哪种方式呢?
如果是用props/$emit来一级一级的传递,确实可以完成,但是比较复杂;如果使用事件总线,在多人开发或者项目较大的时候,维护起来很麻烦;如果使用Vuex,的确也可以,但是如果仅仅是传递数据,那可能就有点浪费了。
针对上述情况,Vue引入了$attrs / $listeners,实现组件之间的跨代通信。
先来看一下inheritAttrs,它的默认值true,继承所有的父组件属性除props之外的所有属性;inheritAttrs:false 只继承class属性 。
$attrs:继承所有的父组件属性(除了prop传递的属性、class 和 style ),一般用在子组件的子元素上$listeners:该属性是一个对象,里面包含了作用在这个组件上的所有监听器,可以配合v-on="$listeners"将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件)
A组件(APP.vue):
<template>
<div id="app">
//此处监听了两个事件,可以在B组件或者C组件中直接触发
<child1 :p-child1="child1" :p-child2="child2" @test1="onTest1" @test2="onTest2"></child1>
</div>
</template>
<script>
import Child1 from './Child1.vue';
export default {
components: { Child1 },
methods: {
onTest1() {
console.log('test1 running');
},
onTest2() {
console.log('test2 running');
}
}
};
</script>
B组件(Child1.vue):
<template>
<div class="child-1">
<p>props: {{pChild1}}</p>
<p>$attrs: {{$attrs}}</p>
<child2 v-bind="$attrs" v-on="$listeners"></child2>
</div>
</template>
<script>
import Child2 from './Child2.vue';
export default {
props: ['pChild1'],
components: { Child2 },
inheritAttrs: false,
mounted() {
this.$emit('test1'); // 触发APP.vue中的test1方法
}
};
</script>
C 组件 (Child2.vue):
<template>
<div class="child-2">
<p>props: {{pChild2}}</p>
<p>$attrs: {{$attrs}}</p>
</div>
</template>
<script>
export default {
props: ['pChild2'],
inheritAttrs: false,
mounted() {
this.$emit('test2');// 触发APP.vue中的test2方法
}
};
</script>
在上述代码中:
- C组件中能直接触发test的原因在于 B组件调用C组件时 使用 v-on 绑定了
$listeners属性 - 在B组件中通过v-bind 绑定
$attrs属性,C组件可以直接获取到A组件中传递下来的props(除了B组件中props声明的)
父组件怎么调用子组件中的方法
-
为子组件设置 ref:
在父组件中,在子组件的标签上添加 ref 属性,这样可以在父组件中通过 ref 获取子组件的实例。例如:<template> <div> <child-component ref="childRef"></child-component> </div> </template> -
在父组件中调用子组件方法:
使用 this.$refs.childRef 来访问子组件的实例,然后就可以直接调用子组件的方法了。例如:this.$refs.childRef.childMethod(); -
子组件方法定义:
在子组件中定义需要被调用的方法。例如,在子组件的 methods 中定义一个方法:methods: { childMethod() { // 子组件的具体逻辑 } }
(6)总结
(1)父子组件间通信
- 子组件通过 props 属性来接受父组件的数据,然后父组件在子组件上注册监听事件,子组件通过 emit 触发事件来向父组件发送数据。
- 通过 ref 属性给子组件设置一个名字。父组件通过
$refs组件名来获得子组件,子组件通过$parent获得父组件,这样也可以实现通信。 - 使用 provide/inject,在父组件中通过 provide提供变量,在子组件中通过 inject 来将变量注入到组件中。不论子组件有多深,只要调用了 inject 那么就可以注入 provide中的数据。
(2)兄弟组件间通信
- 使用 eventBus 的方法,它的本质是通过创建一个空的 Vue 实例来作为消息传递的对象,通信的组件引入这个实例,通信的组件通过在这个实例上监听和触发事件,来实现消息的传递。
- 通过
$parent/$refs来获取到兄弟组件,也可以进行通信。
(3)任意组件之间
- 使用 eventBus ,其实就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。
如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候采用上面这一些方法可能不利于项目的维护。这个时候可以使用 vuex ,vuex 的思想就是将这一些公共的数据抽离出来,将它作为一个全局的变量来管理,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的。
路由
1. Vue-Router 的懒加载如何实现
非懒加载:
import List from '@/components/list.vue'
const router = new VueRouter({
routes: [
{ path: '/list', component: List }
]
})
(1)方案一(常用):使用箭头函数+import动态加载
const List = () => import('@/components/list.vue')
const router = new VueRouter({
routes: [
{ path: '/list', component: List }
]
})
(2)方案二:使用箭头函数+require动态加载
const router = new Router({
routes: [
{
path: '/list',
component: resolve => require(['@/components/list'], resolve)
}
]
})
2. 路由的hash和history模式的区别
Vue-Router有两种模式:默认的路由模式是hash模式。
1. hash模式
1.hash 路由模式的实现主要是基于下面几个特点:
- hash模式是开发中默认的模式,#以及#后面的字符称之为 hash, 用 window.location.hash 读取。
- URL 中的 hash 值只是客户端的url中进行显示,向服务端发送请求的时候,hash 部分不会被发送。
- 可以通过hashchang 事件来监听 hash 值的变化,从而对页面进行跳转。
- hash 值的改变会增加浏览器的访问记录,所以可以通过浏览器的回退、前进控制 hash 值的改变。
- 可以通过 a 标签设置 href 值或者通过 js 给location.hash 赋值来改变 hash 值。
window.onhashchange = function(event){
console.log(event.oldURL, event.newURL);
let hash = location.hash.slice(1);
}
2. history模式
history 采用 HTML5 的新特性;且提供了两个新方法:
-
pushState()用于在历史中添加一条记录
-
replaceState()对历史记录进行修改
-
popState 事件的监听到状态变更
-
通过 pushState 和 replaceState 两个API 来操作实现 URL 的变化,不会向后端发送请求,也不会触发popstate事件的执行
-
popstate事件的执行是在点击浏览器的前进后退按钮的时候,会被触发,监听 到URL 的变化,从而对页面进行跳转
history模式需要后台配置支持。如果后台没有正确配置,访问时会返回404。
在vue的router中,通过修改vueRouter的mode属性来决定使用history还是hash。默认为hash模式。
const router = new VueRouter({
mode: 'history',
routes: [...]
})
3. 如何获取页面的hash变化
(1)监听$route的变化
// 监听,当路由发生变化的时候执行
watch: {
$route: {
handler: function(val, oldVal){
console.log(val);
},
// 深度观察监听
deep: true
}
},
(2)window.location.hash读取#值 window.location.hash 的值可读可写,读取来判断状态是否改变,写入时可以在不重载网页的前提下,添加一条历史访问记录。
4. $route 和$router 的区别
- $route 是“路由信息对象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数
- $router 是“路由实例”对象包括了路由的跳转方法,钩子函数等。
5. 如何定义动态路由?如何获取传过来的动态参数?
(1)param方式
- 配置路由格式:
/router/:id - 传递的方式:在path后面跟上对应的值
- 传递后形成的路径:
/router/123
1)路由定义
//在APP.vue中
<router-link :to="'/user/'+userId" replace>用户</router-link>
//在index.js
{
path: '/user/:userid',
component: User,
},
2)路由跳转
// 方法1:
<router-link :to="{ name: 'users', params: { uname: wade }}">按钮</router-link
// 方法2:
this.$router.push({name:'users',params:{uname:wade}})
// 方法3:
this.$router.push('/user/' + wade)
3)参数获取 通过 $route.params.userid 获取传递的值
(2)query方式
- 配置路由格式:
/router,也就是普通配置 - 传递的方式:对象中使用query的key作为传递方式
- 传递后形成的路径:
/route?id=123
1)路由定义
//方式1:直接在router-link 标签上以对象的形式
<router-link :to="{path:'/profile',query:{name:'why',age:28,height:188}}">档案</router-link>
// 方式2:写成按钮以点击事件形式
<button @click='profileClick'>我的</button>
profileClick(){
this.$router.push({
path: "/profile",
query: {
name: "kobi",
age: "28",
height: 198
}
});
}
2)跳转方法
// 方法1:
<router-link :to="{ name: 'users', query: { uname: james }}">按钮</router-link>
// 方法2:
this.$router.push({ name: 'users', query:{ uname:james }})
// 方法3:
<router-link :to="{ path: '/user', query: { uname:james }}">按钮</router-link>
// 方法4:
this.$router.push({ path: '/user', query:{ uname:james }})
// 方法5:
this.$router.push('/user?uname=' + jsmes)
3)获取参数
通过$route.query 获取传递的值
6. Vue-router 路由钩子在生命周期的体现
1.全局导航守卫 2.路由独享守卫 3.组件内导航守卫
一、Vue-Router导航守卫
有的时候,需要通过路由来进行一些操作,比如最常见的登录权限验证,当用户满足条件时,才让其进入导航,否则就取消跳转,并跳到登录页面让其登录。 为此有很多种方法可以植入路由的导航过程:全局的,单个路由独享的,或者组件级的
- 全局路由钩子
vue-router全局有三个路由钩子;
- router.beforeEach 全局前置守卫 进入路由之前
- router.beforeResolve 全局解析守卫在 beforeRouteEnter 调用之后调用
- router.afterEach 全局后置钩子 进入路由之后
具体使用∶
- beforeEach(判断是否登录了,没登录就跳转到登录页)
router.beforeEach((to, from, next) => {
let ifInfo = Vue.prototype.$common.getSession('userData'); // 判断是否登录的存储信息
if (!ifInfo) {
// sessionStorage里没有储存user信息
if (to.path == '/') {
//如果是登录页面路径,就直接next()
next();
} else {
//不然就跳转到登录
Message.warning("请重新登录!");
window.location.href = Vue.prototype.$loginUrl;
}
} else {
return next();
}
})
- afterEach (跳转之后滚动条回到顶部)
router.afterEach((to, from) => {
// 跳转之后滚动条回到顶部
window.scrollTo(0,0);
});
- 单个路由独享钩子
beforeEnter 如果不想全局配置守卫的话,可以为某些路由单独配置守卫,有三个参数∶ to、from、next
export default [
{
path: '/',
name: 'login',
component: login,
beforeEnter: (to, from, next) => {
console.log('即将进入登录页面')
next()
}
}
]
- 组件内钩子
beforeRouteUpdate、beforeRouteEnter、beforeRouteLeave
这三个钩子都有三个参数∶to、from、next
- beforeRouteEnter∶ 进入组件前触发
- beforeRouteUpdate∶ 当前地址改变并且改组件被复用时触发,举例来说,带有动态参数的路径foo/∶id,在 /foo/1 和 /foo/2 之间跳转的时候,由于会渲染同样的foa组件,这个钩子在这种情况下就会被调用
- beforeRouteLeave∶ 离开组件被调用
注意点,beforeRouteEnter组件内还访问不到this,因为该守卫执行前组件实例还没有被创建,需要传一个回调给 next来访问,例如:
beforeRouteEnter(to, from, next) {
next(target => {
if (from.path == '/classProcess') {
target.isFromProcess = true
}
})
}
二、Vue路由钩子在生命周期函数的体现
- 完整的路由导航解析流程(不包括其他生命周期)
- 触发进入其他路由。
- 调用要离开路由的组件守卫beforeRouteLeave
- 调用局前置守卫∶ beforeEach
- 在重用的组件里调用 beforeRouteUpdate
- 调用路由独享守卫 beforeEnter。
- 解析异步路由组件。
- 在将要进入的路由组件中调用 beforeRouteEnter
- 调用全局解析守卫 beforeResolve
- 导航被确认。
- 调用全局后置钩子的 afterEach 钩子。
- 触发DOM更新(mounted)。
- 执行beforeRouteEnter 守卫中传给 next 的回调函数
- 触发钩子的完整顺序
路由导航、keep-alive、和组件生命周期钩子结合起来的,触发顺序,假设是从a组件离开,第一次进入b组件∶
- beforeRouteLeave:路由组件的组件离开路由前钩子,可取消路由离开。
- beforeEach:路由全局前置守卫,可用于登录验证、全局路由loading等。
- beforeEnter:路由独享守卫
- beforeRouteEnter:路由组件的组件进入路由前钩子。
- beforeResolve:路由全局解析守卫
- afterEach:路由全局后置钩子
- beforeCreate:组件生命周期,不能访问tAis。
- created;组件生命周期,可以访问tAis,不能访问dom。
- beforeMount:组件生命周期
- deactivated:离开缓存组件a,或者触发a的beforeDestroy和destroyed组件销毁钩子。
- mounted:访问/操作dom。
- activated:进入缓存组件,进入a的嵌套子组件(如果有的话)。
- 执行beforeRouteEnter回调函数next。
- 导航行为被触发到导航完成的整个过程
- 导航行为被触发,此时导航未被确认。
- 在失活的组件里调用离开守卫 beforeRouteLeave。
- 调用全局的 beforeEach守卫。
- 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
- 在路由配置里调用 beforeEnteY。
- 解析异步路由组件(如果有)。
- 在被激活的组件里调用 beforeRouteEnter。
- 调用全局的 beforeResolve 守卫(2.5+),标示解析阶段完成。
- 导航被确认。
- 调用全局的 afterEach 钩子。
- 非重用组件,开始组件实例的生命周期:beforeCreate&created、beforeMount&mounted
- 触发 DOM 更新。
- 用创建好的实例调用 beforeRouteEnter守卫中传给 next 的回调函数。
- 导航完成
7. Vue-router跳转和location.href有什么区别
- 使用
location.href= /url来跳转,刷新页面; - 使用
history.pushState( /url ),无刷新页面,静态跳转; - 引进 router ,然后使用
router.push( /url )来跳转,使用了diff算法,实现了按需加载,减少了 dom 的消耗。
8. params和query的区别
用法:query要用path来引入,params要用name来引入,接收参数都是类似的,分别是 this.$route.query.name 和 this.$route.params.name 。
url地址显示:query更加类似于get传参url中显示参数,params则类似于post传参url中不显示参数
注意:query刷新不会丢失数据 params刷新会丢失数据。
9. Vue-router 导航守卫有哪些
- 全局前置/钩子:beforeEach、beforeResolve、afterEach
- 路由独享的守卫:beforeEnter
- 组件内的守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
10. 对前端路由的理解
前端路由是指在前端应用中,通过URL来管理不同页面,而无需刷新整个页面,不需要向服务器发送请求获取新的页面。
前端路由通常使用一种叫做“单页面应用”的开发模式来实现。SPA将整个应用加载到浏览器中的一个页面中,然后通过前端路由来根据URL的变化切换显示不同的页面内容。
在前端路由中,常用的工具库有React Router和Vue Router等。它们提供了一组API,用于定义路由规则、处理URL变化和页面切换等功能。同时,它们还支持路由参数、嵌套路由、重定向等高级功能,使得前端路由更加灵活和强大。
vuex
1.vuex的理解
为vue提供的全局状态管理系统,用于多个组件中的数据共享
Vuex的状态是响应式的,状态发生改变,相应的组件也会相应的更新
改变store中的状态唯一的途径是mutation,方便追踪每个状态的变化
主要的模块
State 定义响应式的数据
Getter state对象的读取方法
Mutation 唯一更改state中状态的方法,必须是同步函数
Action 用于提交mutation,而不是直接变更状态,可以是异步操作
Module 允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中
2. Vuex中action和mutation的区别
- Mutation是修改State的唯一途径 同步执行;
- Action不能直接操作state通过调用mutation来完成state状态的修改 异步执行。
- mutation的参数是state,包含store中的数据;store的参数是context,它是 state 的父级,包含 state、getters
3. Vuex 和 localStorage 的区别
(1)最重要的区别
- vuex存储在内存中
- localstorage 则以文件的方式存储在本地,只能存储字符串类型的数据,存储对象需要 JSON的stringify和parse方法进行处理。 读取内存比读取硬盘速度要快
(2)应用场景
- Vuex 它采用集中式存储管理应用的所有组件的状态,vuex用于组件之间的传值。
- localstorage是本地存储,是将数据存储到浏览器,一般是在跨页面传递数据时使用 。
- Vuex能做到数据的响应式,localstorage不能
(3)永久性
刷新页面时vuex存储的值会丢失,localstorage不会。
4. Redux 和 Vuex 有什么区别,它们的共同思想
(1)Redux 和 Vuex区别
- Vuex以mutations变化函数取代Reducer函数,无需switch,只需在对应的mutation函数里改变state值即可
- Vuex由于Vue自动重新渲染的特性,无需订阅重新渲染函数,只要生成新的State即可
- Vuex数据流的顺序是∶View调用store.commit提交对应的请求到Store中对应的mutation函数->store改变(vue检测到数据变化自动渲染)
(2)共同思想
- 单—的数据源
- 变化可以预测
5. 为什么要用 Vuex 或者 Redux
对于多层嵌套的组件传递参数会非常繁琐,并且对于兄弟组件间的传值需要定义在共同的父组件中。通常会导致代码无法维护。
将组件的公共状态抽取出来,采用集中式的管理。代码将会变得更结构化且易维护
6. Vuex和单纯的全局对象有什么区别?
- Vuex 的状态存储是响应式的。若 store 中的状态发生变化,那么相应的组件也会相应地更新。
- 不能直接改变 store 中的状态。改变 store 中的状态的唯一途径mutation。这样可以方便地跟踪每一个状态的变化。
7. 为什么 Vuex 的 mutation 中不能做异步操作?
- Vuex中所有的状态更新的唯一途径都是mutation,异步操作通过 Action 来提交 mutation实现,这样可以方便地跟踪每一个状态的变化
- 每个mutation执行完成后都会对应到一个新的状态变更,如果mutation支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难
8. Vuex的严格模式是什么,有什么作用,如何开启?
在严格模式下,状态变更且不是由mutation函数引起的,将会抛出错误。
在Vuex.Store 构造器选项中开启,如下
const store = new Vuex.Store({
strict:true,
})
9. 如何在组件中批量使用Vuex的getter属性
使用mapGetters辅助函数, 利用对象展开运算符将getter混入computed 对象中
import {mapGetters} from 'vuex'
export default{
computed:{
...mapGetters(['total','discountTotal'])
}
}
10. 如何在组件中重复使用Vuex的mutation
使用mapMutations辅助函数,在组件中这么使用
import { mapMutations } from 'vuex'
methods:{
...mapMutations({
setNumber:'SET_NUMBER',
})
}
然后调用this.setNumber(10)相当调用this.$store.commit('SET_NUMBER',10)
11.vuex和pinia的区别
- Vuex基于Vue2的Options API构建,而Pinia基于Vue3的Composition API构建,这使得Pinia更加灵活和可组合 (体现在不需要通过辅助函数进行注册)。
- Vuex采用全局单例模式,通过一个store对象来管理所有的状态,而Pinia采用分离模式,每个组件都拥有自己的store实例。
- pinia 具有可靠的类型推断支持
- pinia中没有了mutations和modules,同步异步都可在actions进行操作。
- Vuex体积相对较大,而Pinia的体积约1KB,相对较小。
vue3
1. Vue3.0有什么更新
2. defienProperty与proxy有何作用,区别是什么?
Object.defineProperty和Proxy都是用来实现对对象的监视和控制的机制
Object.defineProperty的作用和区别:
- 只能对已有的属性进行操作,无法监视对象的整体操作。
- 无法监控到数组下标和长度的变化。
- 添加或删除对象的属性时,Vue 检测不到。因为添加或删除的对象没有在初始化进行响应式处理,只能通过
$set来调用Object.defineProperty()处理。
示例:
const obj = { name: 'Alice' };
Object.defineProperty(obj, 'name', { writable: false }); // 将name属性设为不可写
Proxy的作用和区别:
- 可以监控整个对象的操作。
- 通过Proxy可以代理整个对象,可以监听同级结构下的所有属性变化,并在代理过程中定义对应的操作方法(如get、set、deleteProperty等),从而实现对对象的行为进行拦截和自定义处理。
- Proxy 可以监听数组的变化
示例:
const handler = {
get: function(target, prop) {
console.log(`Reading property ${prop}`);
return target[prop];
}
};
const proxy = new Proxy({ name: 'Alice' }, handler);
console.log(proxy.name); // 会触发get拦截器,打印"Reading property name"并返回"Alice"
3. Vue3.0 为什么要用 proxy?
- proxy可以直接监听数组的变化;
- proxy可以监听对象而非属性.它在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截。
- Proxy直接可以劫持整个对象,并返回一个新对象。
Proxy 实现的响应式原理与 Vue2的实现原理相同,实现方式大同小异∶
- get 收集依赖
- Set、delete 等触发依赖
4. Vue 3.0 中的 Vue Composition API?
- 代码组织: 在 Vue 2 中,组件的属性、方法和生命周期钩子是按照固定的顺序进行组织的,导致在逻辑相关的代码之间需要来回跳转。而在 Vue 3 中,Composition API 可以按照逻辑关联代码,使得代码更易于维护、理解和重用。
- 逻辑复用: 在 Vue 2 中,逻辑的复用较为受限,通常需要使用 mixins 或较复杂的高阶组件来实现逻辑的共享。而在 Vue 3 中,Composition API 提供了更好的逻辑复用方式,可以更轻松地提取和重用逻辑部分。
- 更好的 TypeScript 支持: 可以更好地进行类型推断和类型校验。
5. Composition API与React Hook很像,区别是什么
React Hook是根据useState调用的顺序来确定下一次重渲染时的state是来源于哪个useState, 限制条件
- 不能在循环、条件、嵌套函数中调用Hook
- 必须确保总是在你的React函数的顶层调用Hook
- useEffect、useMemo等函数必须手动确定依赖关系
Composition API是基于Vue的响应式系统实现的
- 声明在setup函数内,组件实例化只调用一次setup
- Compositon API的调用不需要顾虑调用顺序,也可以在循环、条件、嵌套函数中使用
- 响应式系统自动实现了依赖收集,而React Hook需要手动传入依赖,而且必须必须保证依赖的顺序
虚拟DOM
1.对虚拟DOM的理解?
虚拟Dom是一个JS对象,通过对象的形式来表示DOM结构,是由render函数生成,是render函数的返回值,是对模板的一种W3C的描述,模板上的一些指令是浏览器的识别不了的,vue在编译模板的时候要先将转为AST(抽象语法树),AST在转为render函数, render函数的返回值就是虚拟DOM
2. 虚拟DOM的解析过程
用JS对象来描述整个虚拟DOM树(虚拟DOM)
把虚拟DOM转成真实DOM并插入页面中(render)
虚拟DOM发生了更改,比较新老虚拟DOM树的差异,得到差异对象(diff)
把差异对象应用到真正的DOM树上(patch)
3. 为什么要用虚拟DOM
传统的 DOM 操作非常耗费时间和计算成本,每次数据变化后直接操作 DOM,会造成频繁的页面重绘和回流。而虚拟DOM可以将数据变化的结果先放在内存中,然后与之前的虚拟DOM进行比对,只在需要更新的地方进行真正的 DOM 操作,避免了频繁的 DOM 操作,提高了页面的渲染效率和性能。
易于实现跨平台:虚拟 DOM 与平台无关,可以在客户端、服务端、小程序等多个平台中使用。
4. 虚拟DOM真的比真实DOM性能好吗
对于复杂的数据更新频繁的场景,使用虚拟 DOM 可以带来性能优化。
页面DOM比较少的情况下,由于多了一层虚拟DOM的计算,会比innerHTML插入慢,直接操作真实的DOM会更高效
5. DIFF算法的原理
DIFF算法只做同级比较,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换
先定类型,判断是否为同一类型,同种组件,如果不为同一类型, 同种组件,则不进行比较,直接进行替换
在进行定量,定量就是用key来描述一个组件的内容,如果新旧虚拟DOM的key值相同则表示是同一类型的组件内容,不需要去进行一一比对,节约了性能
6. Vue中key的作用
v-if中的key用来标识一个独立的元素避免切换前后相同元素的一个复用,使用key的元素就不会被复用
v-for中的key给每个列表项提供一个 key 值,更好的追踪元素,高效的更新虚拟DOM
key 是为 Vue中的唯一标记,通过这个 key,diff 操作可以更准确、更快速
7. 为什么不建议用index作为key?
如果使用索引,当对元素进行增删操作的时候,元素的 key 值发生改变,可能会导致所有元素的 key 值发生改变。所以 key 值不建议使用索引。
8.vue2中的diff算法和vue3中的diff算法的区别
- 静态标记和提升:
- Vue 2 中的 diff 算法会重新渲染整个组件,导致大量的 DOM 操作。
- Vue 3 对静态节点进行优化,在重新渲染时可以跳过静态节点,从而减少不必要的操作。
- Fragments 和模板内的组合:
- Vue 2 中,模板中的组件必须有一个单一的根节点
- Vue 3 引入了 Fragments (片段) 的概念,可以直接在模板中返回多个根节点
- 动态 Props 的标记和缓存:
- Vue 2 中,所有的 Props 都会被当作响应式的,静态的 Props 在重新渲染时也会进行 diff 和更新。
- Vue 3 中,组件会区分动态 Props 和静态 Props,只有动态 Props 才会触发 diff 过程。
说说你对发布订阅模式的理解?与观察者模式有何区别?
消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者可能存在
订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者存在
什么是洋葱模型
在洋葱模型中,每一层相当于一个中间件,其处理顺序先是 next() 前请求(Request,从外层到内层)然后执行 next() 函数,最后是 next() 后响应(Response,从内层到外层),也就是说每一个中间件都有两次处理时机。
Koa 的洋葱模型指的是以 next() 函数为分割点,先由外到内执行 Request 的逻辑,再由内到外执行 Response 的逻辑。通过洋葱模型,将多个中间件之间通信等变得更加可行和简单。
优化
1.骨架屏一般有几种方案实现
骨架屏在页面加载过程中展示布局结构,给用户一种页面正在加载的视觉反馈 以下是一些常见的骨架屏实现方案:
- 纯 CSS 方案:使用纯 CSS 绘制骨架屏,可以使用伪元素、背景颜色或渐变来实现。通过控制元素的大小、形状和样式,以及添加动画效果,模拟页面的布局结构和加载过程。
- 基于图片方案:将页面截图、图标或简单的 SVG 图像作为骨架屏的背景,然后使用 CSS 和动画效果来补充和改进骨架屏的展示效果。
- 基于预渲染方案:在后端服务器生成骨架屏的 HTML 片段,作为初始响应返回给浏览器。随后,前端 JavaScript 会将骨架屏替换为具体的内容,实现平滑的过渡效果。
- 基于组件方案:使用通用的组件库或框架中提供的骨架屏组件。通过配置组件参数,来自动生成基于布局结构的骨架屏。
- 基于第三方库方案:使用第三方的骨架屏库,如
react-loading-skeleton、vue-content-loading等。这些库提供了易于使用且高度可定制的骨架屏组件,通过简单的 API 调用即可实现骨架屏效果。
Tree-shaking(树摇)
Tree shaking 是指在打包过程中去除不被使用的代码,以减少最终生成的的体积
Tree shaking 的核心思想是通过静态代码分析的方式,识别出没有被使用的模块成员(函数、变量、类等),从而可以在打包过程中将这些未使用的部分剔除掉
Tree shaking 的好处很明显,首先可以大大减少 JavaScript 文件的大小,减少网络传输和加载时间;其次,减少了未使用代码的包含,减小了浏览器解析和执行 JavaScript 的成本;最后,还可以提供更好的用户体验,特别是在移动端或网络条件不佳的情况下。
vue2 vue3 中的 route区别
new Router 变成 createRouter
Vue Router 不再是一个类,而是一组函数。现在你不用再写 new Router(),而是要调用 createRouter
新的 history 配置取代 mode
mode: 'history' 配置已经被一个更灵活的 history 配置所取代。根据你使用的模式,你必须用适当的函数替换它:
"history":createWebHistory()"hash":createWebHashHistory()"abstract":createMemoryHistory()
移动了 base 配置
现在,base 配置被作为 createWebHistory (其他 history 也一样)的第一个参数传递: