vue 双向数据绑定原理
1:vue2响应式原理
总结:
1:初始化组件时候先遍历组件中data的所有的属性,然后使用Object.defineProperty 把这些属性全部转为getter/setter
2:在getter中进行收集依赖Watcher;收集依赖需要设置一个Dep类进行存储,负责添加或者删除相关依赖的和通知相关的依赖操作
3:当用户访问或者设置某个属性时,会触发对应的getter/setter方法, 4:set方法有个通知机制,只有一修改,马上同就通知watcher 5:watcher就会马上告知虚拟Dom树,说哪个变量发生了更改 6:虚拟dom就会利用diff算法生成一棵新的Dom树,拿两棵dom树进行比较,发现不一样的节点就把新的节点更新到dom树上,同时更新页面上
vue常见的修饰符有哪些
事件修饰符
once、stop 、self、 prevent、 captrue
v-model 修饰符
trim number lazy
lazy就是输入框改变,这个数据就会改变,lazy这个修饰符会在光标离开input框才会更新数据:
键盘修饰符
enterenter
.tab
.delete
.esc
.space
.up
.down
.left
.right
其他
native
native修饰符是加在自定义组件的事件上,保证事件能执行
执行不了
<My-component @click="shout(3)"></My-component>
可以执行
<My-component @click.native="shout(3)"></My-component>
passive
当我们在监听元素滚动事件的时候,会一直触发onscroll事件,在pc端是没啥问题的,但是在移动端,会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符
<div @scroll.passive="onScroll">...</div>
.sync
当父组件传值进子组件,子组件想要改变这个值时,可以这么做
父组件
<Child :money="total" v-on:update:money="total = $event"/> 原来
<!-- <Child :money.sync="total"/> 缩写 -->
子组件
<button @click="$emit('update:money', money-100)">
父组件里
<children :foo="bar" @update:foo="val => bar = val"></children>
子组件里
this.$emit('update:foo', newValue)
复制代码
sync修饰符的作用就是,可以简写:
父组件里
<children :foo.sync="bar"></children>
子组件里
this.$emit('update:foo', newValue)
vue 中的组件和插件区别
编写形式、注册形式、使用场景
vue执行生命周期
beforeCreate > created> beforeMount >mount> beforeUpdate>updtate> beforeDestroyed> destroyed>
vue父子组件生命周期的执行顺序
加载过程:
父beforeCreate
父created
父beforeMount
子breaforeCreate
子created
子breforeMount
子mounted
父mounted
更新过程:
父 breforeUpdate
子 breforeUpdate
子 updated
父 updated
销毁过程
父 beforeDestroy
子 beforeDestroy
子 destroyed
父 destroyed
你都做过哪些Vue的性能优化?
1)编码阶段
- 尽量减少data中的数据,data中所有的属性都会遍历增加getter和setter 属性,会收集对应的watcher v-if和v-for不能连用
- SPA 页面采用keep-alive缓存组件
- 使用v-if替代v-show
- key保证唯一
- 路由懒加载和异步组件
- 防抖、节流
- 按需导入第三方模块
- 长列表可滚动可视区加载动态加载
- 图片懒加载
2)打包
- 代码压缩
- 引入第三方资源用CDN
- sourceMap优化
- 多线程打包
- 抽离公共文件
3)体验
- 服务器开启gzip配置
- 骨架屏
- PWA(渐进式)
4)SEO优化
- 服务器开启SSR
- 预渲染
Vue的SSR是什么?有什么好处?
SSR就是服务端渲染- 基于
nodejs serve服务环境开发,所有html代码在服务端渲染 - 数据返回给前端,然后前端进行“激活”,即可成为浏览器识别的html代码
SSR首次加载更快,有更好的用户体验,有更好的seo优化,因为爬虫能看到整个页面的内容,如果是vue项目,由于数据还要经过解析,这就造成爬虫并不会等待你的数据加载完成,所以其实Vue项目的seo体验并不是很好
vue 首屏加载优化
- 对于第三方js库的优化,分离打包使用CDN资源
- vue-route懒加载
- 图片资源的压缩,icon资源使用雪碧图:tinypng.com
- 前端配置gzip压缩,并且服务端使用nginx开启gzip,用来减小网络传输的流量大小。
- webpack相关配置优化
- 前端页面代码层面的优化
- (1)合理使用v-if和v-show
- (2)合理使用watch和computed
- (3)使用v-for的key, 保证唯一id, 避免使用index,v-for不要和v-if同时使用
- (4)定时器的销毁。可以在beforeDestroy()生命周期内执行销毁事件;也可以使用$once这个事件侦听器,在定义定时器事件的位置来清除定时器
MVVM、MVC、MVP的区别
. MVVM是什么?和MVC有何区别呢?
MVC
- Model(模型):负责从数据库中取数据
- View(视图):负责展示数据的地方
- Controller(控制器):用户交互的地方,例如点击事件等等
- 思想:Controller将Model的数据展示在View上
MVVM
-
View (视图) :
- 就是我们的 DOM,也就是模板(Template)渲染后的结果。
- 例如:
<div id="app">{{ message }}</div>
-
ViewModel (视图模型) :
- 就是 Vue 的实例。
- 它是 Vue 的核心,负责连接 View 和 Model。它通过数据绑定将 Model 的数据显示到 View 上,通过DOM 监听将 View 的交互事件映射到 Model 的更新上。
-
Model (模型) :
- 就是 Vue 实例中定义的 数据对象。
- 通常是一个普通的 JavaScript 对象,它可能来自本地的
data函数,也可能来自后端 API 的响应。 - 例如:
{ message: 'Hello Vue!' }
html
<!-- View (模板) -->
<div id="app">
<input type="text" v-model="message"> <!-- 双向绑定 -->
<p>{{ message }}</p> <!-- 单向绑定 -->
</div>
<script>
// Model (普通JS对象)
const modelData = {
message: 'Hello Vue!'
};
// ViewModel (Vue实例)
const vm = new Vue({
el: '#app',
data: modelData // 将Model注入到ViewModel中
});
</script>
Vue MVVM 的核心原理:响应式系统与虚拟DOM
Vue 实现 MVVM 的原理主要建立在两大核心之上:响应式系统 和 虚拟DOM。
1. 响应式系统 - 数据变化的追踪
这是 Vue 的“发动机”。它的目标是:当数据(Model)发生变化时,ViewModel 能自动知道,并通知 View 进行更新。
Vue 2 和 Vue 3 的实现方式不同,但目标一致。
Vue 2 的实现:Object.defineProperty
-
初始化/数据劫持:
- 当 Vue 实例创建时,它会遍历
data函数返回对象的所有属性。 - 使用
Object.defineProperty为每个属性添加 getter 和 setter。这个过程叫做数据劫持。
- 当 Vue 实例创建时,它会遍历
-
依赖收集:
- 在 getter 中:当模板编译或计算属性计算时,如果读取了这个属性,就会触发 getter。Vue 会将当前的这个“依赖”(一个 Watcher 实例,代表一个需要更新的组件或表达式)收集起来,存入一个叫做 Dep 的依赖管理器中。
- 简单说:谁用到了我这个数据,我就把谁记住。
-
派发更新:
- 在 setter 中:当属性被修改时,会触发 setter。
- setter 会通知之前收集的所有依赖(Watcher):“我变了!”
- 然后 Watcher 就会执行更新操作,最终触发组件的重新渲染。
Vue 3 的实现:Proxy
Object.defineProperty 有局限性(无法检测对象属性的添加/删除,对数组索引修改监听不佳)。Vue 3 使用了 Proxy 来创建响应式对象。
Proxy可以直接代理整个对象,而不是单个属性。- 它可以拦截包括属性添加、删除在内的多达13种操作,功能更强大,性能也更好。
- 基本原理类似:在
get陷阱中收集依赖,在set陷阱中触发更新。
2. 虚拟DOM - 高效的视图更新
如果每次数据变化都直接操作真实DOM,会非常消耗性能,因为真实DOM操作是昂贵的。Vue 引入了虚拟DOM来解决这个问题。
-
什么是虚拟DOM:
- 它是一个轻量的 JavaScript 对象,是对真实 DOM 的抽象描述。它包含了标签名、属性、子节点等信息。
-
工作流程:
- 初次渲染:Vue 首先将模板编译成 渲染函数。执行渲染函数会生成一个虚拟 DOM 树,然后根据这个虚拟 DOM 树创建真实的 DOM。
- 数据变化时:
a. 数据变化触发组件的重新渲染,再次执行渲染函数,生成一个新的虚拟 DOM 树。
b. Vue 会将新的虚拟 DOM 树与旧的虚拟 DOM 树进行对比(这个过程叫做 Diff 算法)。
c. Diff 算法会找出两棵树之间最小差异。
d. 最后,Vue 只将有差异的部分应用到真实的 DOM 上,从而完成视图的高效更新。
Computed 和 Watch 的区别
-
watch: 用于声明在数据更改时调用的侦听回调
- 在某个值发生改变时触发某些操作 如异步请求
- 不支持缓存,数据变化就执行回调
-
computed: 用于声明要在组件实例上暴露的计算属性。
- 复杂的计算,依赖某个、多个数据,计算出新数据
- 支持缓存,但是依赖数据一改变就会重写计算
- 必须有返回值
- 可以写get 和set
- 不能有异步操作,有异步操作是无意义的
6. Computed 和 Methods 的区别
可以将同一函数定义为一个 method 或者一个计算属性。对于最终的结果,两种方式是相同的
不同点:
- computed: 计算属性是基于它们的依赖进行缓存的,只有在它的相关依赖发生改变时才会重新求值;
- method 调用总会执行该函数。
过滤器的作用,如何实现一个过滤器
- 过滤器就是视图渲染时候可以通过管道符处理一些格式很麻烦的事情问题,依赖收集等一大推降低性能的问题
v-if、v-show、v-html 的原理
-
v-if会调用addIfCondition方法,生成vnode的时候会忽略对应节点,render的时候就不会渲染;
-
v-show 会生成vnode,render的时候也会渲染成真实节点,只是在render过程中会在节点的属性中修改show属性值,也就是常说的display;
-
v-html会先移除节点下的所有子节点,调用html方法,通过addProp添加innerHTML属性,归根结底还是设置innerHTML为v-html的值。
v-if和v-show的区别
- 编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换;
- 编译条件:v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译; v-show是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且DOM元素保留;
- 性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;
- 使用场景:v-if适合运营条件不大可能改变;v-show适合频繁切换。
slot是什么?有什么作用?原理是什么?
实现原理:当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.$slot中,默认插槽为vm.$slot.default,具名插槽为vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到slot标签,使用$slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
插槽
将父组件前后标签中的HTML文件渲染插入模板中使用。
当组件执行渲染函数时候,遇到slot标签,使用$slot中的内容进行替换**
1:匿名插槽
2:具名插槽
slot 是带有name的
多个具名插槽,插槽的位置不是使用插槽的位置而定的,是在定义的时候的位置来替换的
3:作用域插槽
就是用来传递数据的插槽
插槽作用域(数据传递):子组件的插槽传给父组件(显示20)
可以把 :default 去掉,仅限于默认插槽
具名缩写
v-slot:header可以缩写成#header 注意:如果我们父组件需要用到子组件的user,即使是没有名的插槽也要#default="{user}",不能写成#="{user}" 注意:默认插槽的缩写语法不能和具名插槽混用
data为什么是一个函数而不是对象
对象是引用类型,多个实例引入同一个对象,如果一个实例改变其属性,其他的实例也改变
Vue封装组件中是想要复用组件,那就需要每个组件都有自己的数据,这样组件之间才不会相互干扰。
所以我们通过 一个 函数执行返回了一个新的全新的数据对象。
对keep-alive的理解
keep-alive: 是vue的一个内置组件,它能在vue组件切换过程中中将状态保留在内存中,防止重复渲染DOM
keep-alive 有以下几个属性:
1.include:包含的组件(可以为字符串,数组,以及正则表达式,只有匹配的组件会被缓存)
2.exclude 排除组件(可以为字符串,数组,以及正则表达式,任何匹配的组件都不会被缓存)
3.max缓存组件的最大值(类型为字符或者数字,可以控制缓存组件的个数)
// 只缓存组件name为a或者b的组件
<keep-alive include="a,b">
<component />
</keep-alive>
// 组件name为c的组件不缓存(可以保留它的状态或避免重新渲染)
<keep-alive exclude="c">
<component />
</keep-alive>
// 如果同时使用include,exclude,那么exclude优先于include, 下面的例子只缓存a组件
<keep-alive include="a,b" exclude="b">
<component />
</keep-alive>
// 如果缓存的组件超过了max设定的值5,那么将删除第一个缓存的组件
<keep-alive exclude="c" max="5">
<component />
</keep-alive>
配合router使用
router-view也是一个组件,如果直接被包在keepalive里面,那么所有路径匹配到的视图组件都会被缓存,如下:
<keep-alive>
<router-view>
<!-- 所有路径匹配到的视图组件都会被缓存! -->
</router-view>
</keep-alive>
1.使用 include (exclude例子类似)
//只有路径匹配到的 name 为 a 组件会被缓存
<keep-alive include="a">
<router-view></router-view>
</keep-alive>
使用 meta 属性
// routes 配置
export default [
{
path: '/',
name: 'home',
component: Home,
meta: {
keepAlive: true // 需要被缓存
}
}, {
path: '/profile',
name: 'profile',
component: Profile,
meta: {
keepAlive: false // 不需要被缓存
}
}
]
<keep-alive>
<router-view v-if="$route.meta.keepAlive">
<!-- 这里是会被缓存的视图组件,比如 Home! -->
</router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive">
<!-- 这里是不会被缓存的视图组件,比如 Profile! -->
</router-view>
注意:
1.当匹配条件同时在 include 与 exclude 存在时,以 exclude 优先级最高(当前vue 2.4.2 version)。比如:包含于排除同时匹配到了组件A,那组件A不会被缓存。
2·包含在 keep-alive 中,但符合 exclude ,不会调用activated和 deactivated。
keep-alive作用
可以将组件缓存起来在需要的时候重新使用,节约资源和提高性能,特别是需要频繁切换组件 如:Tab切换、路由切换
keep-alive如何刷新
生命周期钩子函数
activated: 当keepp-alive包括的组件再次渲染的时候触发 deactivated:组件销毁时候触发
强制更新方法
this.$forceUpdate()
keep-alive 原理
keep-alive中运用了LRU(Least Recently Used)算法。
- 获取
keep-alive包裹着的第一个子组件对象及其组件名; 如果 keep-alive 存在多个子元素,keep-alive要求同时只有一个子元素被渲染。所以在开头会获取插槽内的子元素,调用getFirstComponentChild获取到第一个子元素的VNode。 - 根据设定的黑白名单(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例(
VNode),否则开启缓存策略。 - 根据组件ID和tag生成缓存Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该key在
this.keys中的位置(更新key的位置是实现LRU置换策略的关键)。 - 如果不存在,则在
this.cache对象中存储该组件实例并保存key值,之后检查缓存的实例数量是否超过max设置值,超过则根据LRU置换策略删除最近最久未使用的实例(即是下标为0的那个key)。最后将该组件实例的keepAlive属性值设置为true。
LRU (least recently used)缓存策略
LRU 缓存策略∶ 从内存中找出最久未使用的数据并置换新的数据。 LRU(Least rencently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是 "如果数据最近被访问过,那么将来被访问的几率也更高" 。 最常见的实现是使用一个链表保存缓存数据,详细算法实现如下∶
- 新数据插入到链表头部
- 每当缓存命中(即缓存数据被访问),则将数据移到链表头部
- 链表满的时候,将链表尾部的数据丢弃。
setTimeout和setImmediate到底谁先执行,本文让你彻底理解Event Loop
- JS是单线程的 ,所谓的"JS是单线程的"只是指JS的主运行线程只有一个
- Chrome的内核为例,他不仅是多线程的,而且是多进程的
Event Loop
所谓Event Loop,就是事件循环,其实就是JS管理事件执行的一个流程,具体的管理办法由他具体的运行环境确定。目前JS的主要运行环境有两个,浏览器和Node.js。这两个环境的Event Loop还有点区别,我们会分开来讲。
运行环境有两个浏览器、Node.js
常见宏任务 和 常见微任务
常见宏任务有:
script(可以理解为外层同步代码)setTimeout/setIntervalsetImmediate(Node.js)- I/O
- UI事件
postMessage
常见微任务有:
Promiseprocess.nextTick(Node.js)Object.observeMutaionObserver
总结
-
JS所谓的“单线程”只是指主线程只有一个,并不是整个运行环境都是单线程
-
JS的异步靠底层的多线程实现
-
不同的异步API对应不同的实现线程
-
异步线程与主线程通讯靠的是Event Loop
-
异步线程完成任务后将其放入任务队列
-
主线程不断轮询任务队列,拿出任务执行
-
任务队列有宏任务队列和微任务队列的区别
-
微任务队列的优先级更高,所有微任务处理完后才会处理宏任务
-
Promise是微任务 -
Node.js的Event Loop跟浏览器的Event Loop不一样,他是分阶段的
-
setImmediate和setTimeout(fn, 0)哪个回调先执行,需要看他们本身在哪个阶段注册的,如果在定时器回调或者I/O(Input/output 指一个系统的输入和输出)回调里面,setImmediate肯定先执行。如果在最外层或者setImmediate回调里面,哪个先执行取决于当时机器状况。 -
process.nextTick不在Event Loop的任何阶段,他是一个特殊API,他会立即执行,然后才会继续执行Event Loop
宏任务和微任务场景题目
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
const p = new Promise((resolve) => {
console.log(3);
setTimeout(() => {
console.log(4);
resolve();
}, 100);
});
p.then(() => {
console.log(5);
setTimeout(() => {
console.log(6);
}, 0);
});
console.log(7);
1372456 注意,和上面的p.then区别,就是 promise中的reslove是在setTimeout后执行,上面的不是
$nextTick 原理及作用
什么是nextTick:
vue nextTick 其本质是对 JavaScript 执行 事件循环 Event Loop 原理的一种应用。 nextTick 是 Vue 内部的异步队列的调用方法,也是给开发者在实际项目中使用这个方法来满足实际应用中对 DOM 更新数据时机的后续逻辑处理
俗话总结: 就是你放在$nextTick 当中的操作不会立即执行,而是等数据更新、DOM更新完成之后再执行,这样我们拿到的肯定就是最新的了。
原理:
本质是为了利用js的 Promise 、MutationObserver、setImmediate、setTimeout这些异步回调任务队列来实现 Vue 框架中自己的异步回调队
- 如果是同步更新,则多次对一个或多个属性赋值,会频繁触发 UI/DOM 的渲染,可以减少一些无用渲染
vue如何检测到DOM更新完毕呢?
能监听到DOM改动的API:MutationObserver
- 理解MutationObserver
MutationObserver是HTML5新增的属性,用于监听DOM修改事件,能够监听到节点的属性 、文本内 容、子节点等的改动,是一个功能强大的利器。
//MutationObserver基本用法
var observer = new MutationObserver(function(){ //这里是回调函数
console.log('DOM被修改了!');
});
var article = document.querySelector('article');
observer.observer(article);
使用场景
- created中获取DOM的操作需要使用它
- 在数据改变后,处理监测dom变化逻辑
.
$set():是Vue 中给 data 中的对象属性添加一个新的属性时会发生什么?如何解决?
问题
<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.obj.b = 'obj.b'
console.log(this.obj)
}
}
}
</script>
在Vue实例创建时,obj.b并未声明,因此就没有被Vue转换为响应式的属性,自然就不会触发视图的更新
解决 :需要使用Vue的全局 api $set():
addObjB () (
this.$set(this.obj, 'b', 'obj.b')
console.log(this.obj)
}
$set 的实现原理是:
- 如果目标是数组,直接使用数组的 splice 方法触发相应式;
- 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)
1.判断目标值是否为有效值,不是有效值直接停止
2.判断是否为数组,并且key值是否为有效的key值
如果是数组,就选择数组的长度和key值取较大值作为数组的新的length值,并且替换目标值
splice方法,重写了,所以执行splice,会双向数据绑定
3.判断目标值是否为响应式的__ob__
如果是vue实例,直接不行
如果不是响应式的数据,就是普通的修改对象操作
如果是响应式数据,那就通过Object.defineProperty进行数据劫持
4.通知dom更新
21. 对象新属性无法更新视图,删除属性无法更新视图,为什么?怎么办?
- 原因:
Object.defineProperty没有对对象的新属性进行属性劫持 - 对象新属性无法更新视图:使用
Vue.$set(obj, key, value),组件中this.$set(obj, key, value) - 删除属性无法更新视图:使用
Vue.$delete(obj, key),组件中this.$delete(obj, key)
直接arr[index] = xxx无法更新视图怎么办?为什么?怎么办?
- 原因:Vue没有对数组进行
Object.defineProperty的属性劫持,所以直接arr[index] = xxx是无法更新视图的 - 使用数组的splice方法,
arr.splice(index, 1, item) - 使用
Vue.$set(arr, index, value)
为什么只对对象劫持,而要对数组进行方法重写?
因为对象最多也就几十个属性,拦截起来数量不多,但是数组可能会有几百几千项,拦截起来非常耗性能,所以直接重写数组原型上的方法,是比较节省性能的方案
22. Vue template 到 render 的过程
vue的模版编译过程主要如下:template -> ast -> render函数
Vue data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗
不会,Vue 实现响应式并不是数据发生变化之后 DOM 立即变化 Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。 如果同一个watcher被多次触发,只会被推入到队列中一次
简述 mixin、extends 的覆盖逻辑
mixins接收对象数组(可理解为多继承)
extends接收的是对象或函数(可理解为单继承)。
mixin 和 mixins 区别
mixin用于全局混入,会影响到每个组件实例,通常插件都是这样做初始化的mixins应该是最常使用的扩展组件的方式
minix
// 定义一个 mixin
const myMixin = {
data() {
return {
message: 'Hello from mixin!'
};
},
methods: {
greet() {
console.log(this.message);
}
}
};
// 在组件中使用 mixin
export default {
mixins: [myMixin], // 将 mixin 混入组件
created() {
this.greet(); // 输出: Hello from mixin!
}
};
mixins
// 定义多个 mixin
const mixin1 = {
data() {
return {
message: 'Hello from mixin1!'
};
}
};
const mixin2 = {
data() {
return {
message: 'Hello from mixin2!'
};
}
};
// 在组件中使用 mixins
export default {
mixins: [mixin1, mixin2], // 混入多个 mixin
created() {
console.log(this.message); // 输出: Hello from mixin2!(后面的 mixin 覆盖前面的)
}
};
extend 有什么作用
这个 API 很少用到,作用是扩展组件生成一个构造器,通常会与 $mount 一起使用。
vue和react区别
相同点:
- 使用虚拟dom
- 组件化开发
- 单向数据流
- 支持服务端渲染(ssr)
不同点
- react使用jsx(js语法扩展——JSX书写),vue是template
- 数据变化的时候,vue是自动的(初始化已响应式处理) react是手动的(setState)
- react是单向绑定,vue是双向绑定
- react 是redux ,vue是vuex
- react高阶组件(高阶函数)扩展,vue是用minxs来扩展
Vue的优点和缺点
优点:渐进式、组件化、轻量级,虚拟dom,响应式,单页面路由,数据与视图分开,
缺点:单页面不利于seo,不支持IE8以下,首屏加载时间长
assets和static的区别
相同点:
assets 和 static 两个都是存放静态资源文件
不相同点:
assets中放置的静态资源文件进行打包上传,所谓打包简单点可以理解为压缩体积,代码格式化,打包后放到static上
static不打包压缩
建议
项目中 template需要的样式文件js文件等都可以放置在 assets如:如iconfoont.css
而第三方文件已经经过处理放到static上
delete和Vue.delete删除数组的区别
-
delete只是被删除的元素变成了empty/undefined其他的元素的键值还是不变。 -
Vue.delete直接删除了数组 改变了数组的键值。 -
删除属性无法更新视图:使用
Vue.$delete(obj, key),组件中this.$delete(obj, key)
Vue模版编译原理
模板编译又分三个阶段,解析parse,优化optimize,生成generate,最终生成可执行函数render。
-
解析阶段:使用大量的正则表达式对template字符串进行解析,将标签、指令、属性等转化为抽象语法树AST。
-
优化阶段:遍历AST(抽象语法树),找到其中的一些静态节点并进行分析、标记,方便在页面重渲染的时候进行diff比较时,直接跳过这一些静态节点,减少运行时开销。
-
生成阶段:将最终的AST(抽象语法树)转化为render函数,渲染函数用于生成虚拟 DOM。。
对SSR的理解
是服务端渲染,也就是将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端
SSR的优势:
- 更好的SEO
- 首屏加载速度更快
SSR的缺点:
- 开发条件会受到限制,服务器端渲染只支持beforeCreate和created两个钩子;
- 更多的服务端负载。
SPA 单页面的理解,它的优缺点分别是什么?
优点
- 减轻服务器的压力,减少不必要的重复渲染
- 前后端职责分离,架构清晰
缺点
- 初始加载较慢,耗时长,需要首屏加载优化
- 对项目的SEO不利
- 不能使用浏览器前后进退的功能
template和jsx的有什么分别?
jsx 是React模板的js语法扩展, 具有更高的灵活性,在复杂的组件中,更具有优势
而template 虽然显得有些呆滞。但是 template 在代码结构上更符合视图与逻辑分离的习惯,更简单、更直观、更好维护。
vue初始化页面闪动问题
[v-cloak] { display: none;}
根元素加上`style="display: none;" :style="{display: 'block'}"`
<div class="#app" v-cloak>
<p>{{value.name}}</p>
</div>
复制代码
然后,在css里面要添加
[v-cloak] {
display: none;
}
哪个生命周期请求异步数据
created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
建议created调用异步请求
- 能更快获取到服务端数据,减少页面加载时间,用户体验更好;
- SSR不支持 beforeMount 、mounted 钩子函数,放在 created 中有助于一致性。
keep-alive 中的生命周期哪些
deactivated、activated
同时,beforeDestroy 和 destroyed 就不会再被触发了,因为组件不会被真正销毁。
组件通信
props(常用)
props和$emit(常用)
.sync(语法糖)
model(单选框和复选框场景可以使用)
$attr和$listeners(组件封装用的比较多)
provide和inject(高阶组件/组件库使用比较多)
eventBus(小项目中使用就好)
Vuex(中大型项目推荐使用)
$parent和$children(推荐少用)
$root(组件树的根,用的少)
子组件和父组件
props / $emit
父子组件和非父子组件
如果项目过大,使用这种方式进行通信,后期维护起来会很困难。
通eventBus事件总线($emit / $on)
(1)创建事件中心管理组件之间的通信
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
(2)发送事件$emit 假设有两个兄弟组件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)接收事件 $on 在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>
依赖注入(provide / inject)
用于父子组件之间的通信。当然这里所说的父子不一定是真正的父子,也可以是祖孙组件,在层数很深的情况下,可以使用这种方法来进行传值。就不用一层一层的传递了。
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)
复制代码
注意: 依赖注入所提供的属性是非响应式的。
// 父组件
<template>
<mtd-date-picker type="month" placeholder="选择时间" value-format="yyyy-MM" v-model="month" />
<Test />
</template>
<script>
import Test from './components/test.vue';
export default {
components: { Test },
data() {
return {
month: '2021-10',
};
},
provide() {
return {
getApp: this.getApp,
};
},
methods: {
getApp() {
return {
month: this.month,
// 其他需要暴露的属性
};
},
},
}
</scritp>
// 子组件 components/test.vue
<template>
<div>
<div>子组件app:{{ getApp().month }}</div>
</div>
</template>
<script>
export default {
inject: ['getApp']
}
</scritp>
ref / $refs
$parent / $children
- 使用
$parent可以让组件访问父组件的实例(访问的是上一级父组件的属性和方法) - 使用
$children可以让组件访问子组件的实例,但是,$children并不能保证顺序,并且访问的数据也不是响应式的。
Father组件
<template>
<div id="father">
<Child/>
</div>
</template>
<script>
import Child from './Child.vue'
export default {
name: 'father',
data () {
return {
msg: 'hello'
}
},
created () {
this.$nextTick(() => {
console.log(this.$children)
})
},
components: {
Child
}
}
</script>
复制代码
Child组件
<template>
<div class="child">
父组件的值:{{$parent.msg}}
<br>
<input type="text" @change="change">
</div>
</template>
<script>
export default {
name: 'Child',
created () {
console.log(this.$parent)
},
methods: {
change(e) {
this.$parent.msg = e.target.value
}
}
}
</script>
$attrs/$listeners
父组件
<template>
<div id="app">
<Child1 :words1="text1" :words2="text2" :words3="text3" v-on:event1="goEvent1" v-on:event2="goEvent2"></Child1>
</div>
</template>
<script>
import Child1 from "./components/Child1"
export default {
name: "App",
data() {
return {
text1: 1,
text2: 2,
text3: 3
}
},
methods: {
goEvent1() {
console.log("child 提交成功")
},
goEvent2(value) {
console.log(value)
}
},
components: {
Child1,
}
}
</script>
<style>
html,
body {
height: 100%;
}
#app {
height: 100%;
}
</style>
子组件 Child1.vue
```
<template>
<div class="mainWrapper">
<p>props: {{words1}}</p>
<p>$attrs: {{$attrs}}</p>
<button @click="submit()">提交</button>
<hr>
<child2 v-bind="$attrs" v-on="$listeners"></child2>
<!-- 通过$listeners将父作用域中的v-on事件监听器,传入child2,使得child2可以获取到app中的事件 -->
</div>
</template>
<script>
import Child2 from "./Child2"
export default {
name: "Child1",
props: ["words1"],
data() {
return {}
},
inheritAttrs: true,
components: { Child2 },
methods: {
submit() {
this.$emit("event1", "child1 提交事件")
}
}
}
</script>
<style scoped>
.mainWrapper {
height: 100px;
}
</style>
```
孙组件:Child2.vue
<template>
<div>
<div class="child-2">
<p>props: {{words2}}</p>
<p>$attrs: {{$attrs}}</p>
<input v-model="inputValue" name="" id="" @input="goInput">
</div>
</div>
</template>
<script>
export default {
name: 'Child2',
props: ['words2'],
data() {
return {
inputValue: ''
};
},
inheritAttrs: false,
mounted() {
},
methods: {
goInput () {
this.$emit('event2', this.inputValue);
}
}
}
</script>
1. 效果
- 可以看到父组件App.vue中通过v-bind给Child1传递了三个值,在子组件中未被Props接收的值都在`$attrs`中,其也可以通过v-bind将值传递给Child1的内部组件Child2,同时也可以使用`$listeners`将父作用域中的v-on事件监听器,传入child2
inheritAttrs 属性以及作用
- 当一个组件设置了inheritAttrs: false后(默认为true),
那么该组件的非props属性(即未被props接收的属性)将
不会在**组件根节点上生成html属性**,以为为对比图
inheritAttrs:false
**inheritAttrs:true **
路由
一:Vue-Router 的懒加载如何实现
非懒加载:
import List from '@/components/list.vue'
const router = new VueRouter({
routes: [
{ path: '/list', component: List }
]
})
(1)方案一(常用):使用箭头函数+import动态加载(ES2020提案 引入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)
}
]
})
(3)方案三:使用webpack的require.ensure技术,也可以实现按需加载。 这种情况下,多个路由指定相同的chunkName,会合并打包成一个js文件。
// r就是resolve
const List = r => require.ensure([], () => r(require('@/components/list')), 'list');
// 路由也是正常的写法 这种是官方推荐的写的 按模块划分懒加载
const router = new Router({
routes: [
{
path: '/list',
component: List,
name: 'list'
}
]
}))
二:路由的hash和history模式的区别
hash模式
-
URL 特征:URL 中带有
#,例如http://localhost:8080/#/home,#后面的部分称为 hash 值。 -
原理:
- hash 值的变化不会触发浏览器向服务器发送请求(hash 仅在客户端生效);
- 监听浏览器的
hashchange事件(当 hash 值变化时触发); - 当 hash 变化时,vue-router 根据配置的路由规则,匹配对应的组件并渲染到
<router-view>中。
-
底层 API:
window.onhashchange = function() {}或window.addEventListener('hashchange', callback)。
window.onhashchange = function(event){
console.log(event.oldURL, event.newURL);
let hash = location.hash.slice(1);
}
history模式
-
URL 特征:URL 无
#,与传统后端路由 URL 一致,例如http://localhost:8080/home。 -
原理:
- 利用 HTML5 History API 提供的
pushState()和replaceState()方法(这两个方法可以修改 URL 但不会发送 HTTP 请求); - 监听浏览器的
popstate事件(当用户点击浏览器的前进 / 后退按钮时触发,注意:pushState/replaceState不会触发popstate); - 当 URL 变化时,vue-router 匹配路由规则并渲染对应组件。
- 利用 HTML5 History API 提供的
-
注意点:history 模式需要后端配置支持(因为直接刷新页面时,浏览器会向服务器发送对应 URL 的请求,服务器需配置所有路由都指向
index.html,否则会返回 404)。
修改历史状态
pushState() 和 replaceState() 方法,
window.history.pushstate()
window.history.replaceState()
切换历史状态:
forward()、back()、go()三个方
window.history.forward() 前进
window.history.back() 后退
window.history.go() 跳转
路由有哪些模式呢?又有什么不同呢?
- hash模式:通过
#号后面的内容的更改,触发hashchange事件,实现路由切换 - history模式:通过
pushState和replaceState切换url,实现路由切换,需要后端配合
当用户在 Vue 项目中点击<router-link>或调用$router.push()时,Vue Router 的导航流程是怎样的?
VueRouter 的导航是异步的,完整流程如下:
-
触发导航:用户点击
<router-link>(本质调用push方法)或手动调用$router.push('/path')。 -
调用导航守卫(全局 / 路由独享 / 组件内守卫):
- 首先触发全局前置守卫(
router.beforeEach); - 然后触发路由独享守卫(
beforeEnter); - 再触发组件内守卫(
beforeRouteEnter); - 若守卫中存在异步操作(如接口请求),会等待异步完成后再继续。
- 首先触发全局前置守卫(
-
解析路由:根据目标路径匹配路由规则,解析出对应的组件、参数等。
-
加载组件:
- 若为同步组件:直接加载组件;
- 若为异步组件(路由懒加载) :通过
import()动态加载组件代码块,加载完成后缓存。
-
更新 DOM:将匹配到的组件渲染到
<router-view>中,完成页面更新。 -
触发后续守卫:执行全局后置守卫 (
router.afterEach)、组件内的beforeRouteUpdate/beforeRouteLeave(若有)。 -
导航完成:整个路由导航流程结束
Vue Router 的路由懒加载(异步加载)原理是什么?有什么作用?
1. 原理
路由懒加载基于ES6 的动态 import () 语法(webpack/Rollup 等构建工具会将其解析为代码分割点)和 Vue 的异步组件机制:
-
传统路由配置:直接导入组件,打包时组件代码会被合并到主包(
app.js)中:js
import Home from './views/Home.vue' const routes = [{ path: '/home', component: Home }] -
懒加载配置:使用
import()动态导入组件,打包时 webpack 会将每个异步组件分割为独立的代码块(chunk):js
const Home = () => import('./views/Home.vue') const routes = [{ path: '/home', component: Home }] -
当用户导航到该路由时,浏览器才会异步加载对应的代码块,加载完成后渲染组件。
2. 作用
- 减小首屏加载体积:首屏只需加载核心代码块,无需加载所有路由的组件代码,提升首屏加载速度;
- 按需加载资源:用户只加载自己需要的路由代码,节省带宽和内存;
- 优化构建产物:将代码分割为多个小的 chunk,便于缓存(若某组件更新,仅需重新加载该 chunk)。
3. 进阶:批量懒加载(命名 chunk)
可将多个路由组件打包到同一个 chunk 中,减少请求数:
js
const Home = () => import(/* webpackChunkName: "group-home" */ './views/Home.vue')
const About = () => import(/* webpackChunkName: "group-home" */ './views/About.vue')
history 模式下,刷新页面出现 404 的原因是什么?如何解决?
1. 原因
- history 模式的 URL 是真实的路径(如
/home),当用户刷新页面时,浏览器会向服务器发送GET /home的请求; - 若服务器未配置对应路由的处理逻辑,会认为该路径不存在,返回 404 错误。
- 而 hash 模式下,刷新页面时浏览器只会请求
index.html(#后的部分不会发送到服务器),因此不会出现 404。
2. 解决方法
-
后端配置(核心):
-
Nginx:配置
try_files $uri $uri/ /index.html;,让所有请求都指向index.html;nginx
server { listen 80; server_name localhost; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; # 关键配置 } } -
Apache:配置
.htaccess文件; -
Node.js(Express) :使用
connect-history-api-fallback中间件。
-
-
前端兜底:可在路由配置中添加 404 路由,匹配所有未定义的路径:
js
const routes = [ // 其他路由 { path: '*', component: () => import('./views/404.vue') } // Vue Router 3.x // { path: '/:pathMatch(.*)*', component: () => import('./views/404.vue') } // Vue Router 4.x ]
Vue Router 如何处理路由参数的传递与解析?(底层原理)
核心答案:
Vue Router 支持动态路由参数、query 参数、params 参数,其解析原理如下:
-
动态路由参数(如
/user/:id) :- 原理:使用路径匹配正则解析 URL 中的参数,将
:id部分提取为$route.params.id; - 当导航到
/user/123时,vue-router 会将123解析为params.id,并传递给组件。
- 原理:使用路径匹配正则解析 URL 中的参数,将
-
query 参数(如
/user?id=123) :- 原理:使用
URLSearchParamsAPI 解析 URL 中?后的部分,将其转换为对象$route.query; - 例如
/user?id=123&name=test会被解析为{ id: '123', name: 'test' }。
- 原理:使用
-
params 参数(命名路由) :
-
原理:通过命名路由跳转时,
params会被嵌入到路径中(需配合动态路由),若未配置动态路由,params会丢失(刷新页面后消失); -
示例:
js
// 路由配置 { path: '/user/:id', name: 'user', component: User } // 跳转 $router.push({ name: 'user', params: { id: 123 } }) // 路径为/user/12
-
Vue Router 的<router-view>和<router-link>的工作原理是什么?
核心答案:
-
<router-link>:- 本质是 Vue 的组件,默认渲染为
<a>标签; - 原理:点击时阻止
<a>标签的默认跳转行为(event.preventDefault()),转而调用$router.push()方法触发路由导航; - 支持
to属性(目标路径)、replace属性(调用$router.replace())、active-class属性(匹配路由时的激活类名)等。
- 本质是 Vue 的组件,默认渲染为
-
<router-view>:-
本质是 Vue 的功能性组件(Functional Component),用于渲染匹配到的路由组件;
-
原理:
- vue-router 会在全局维护一个路由匹配的状态(当前匹配的组件、参数等);
<router-view>会订阅路由状态的变化,当路由状态更新时,重新渲染对应的组件;- 支持嵌套路由:父路由的
<router-view>渲染父组件,子路由的<router-view>渲染子组件,形成嵌套结构。
-
三:如何获取页面的hash变化
(1)监听$route的变化
方法一:通过 watch
// 监听,当路由发生变化的时候执行
watch:{
$route(to,from){
console.log(to.path);
}
},
或者
// 监听,当路由发生变化的时候执行
watch: {
$route: {
handler: function(val, oldVal){
console.log(val);
},
// 深度观察监听
deep: true
}
},
或者
// 监听,当路由发生变化的时候执行
watch: {
'$route':'getPath'
},
methods: {
getPath(){
console.log(this.$route.path);
}
}
或者 组件路由构造函数 beforeRouteUpdate;
方法二: vue-router 的钩子函数
beforeRouteEnter beforeRouteUpdate beforeRouteLeave
<script>
export default {
name: 'app',
// 监听,当路由发生变化的时候执行
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当钩子执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
</script>
(2)window.location.hash读取#值
window.location.hash 的值可读可写,读取来判断状态是否改变,写入时可以在不重载网页的前提下,添加一条历史访问记录。
四:$route 和$router 的区别
- $route 是“路由信息对象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数
- $router 是“路由实例”对象包括了路由的跳转方法,钩子函数等。
router 实例
导航守卫
router.beforeEach((to, from, next) => {
/* 必须调用 `next` */
})
router.beforeResolve((to, from, next) => {
/* 必须调用 `next` */
})
router.afterEach((to, from) => {})
动态导航到新路由
router.push
router.replace
router.go
router.back
router.forward
window.location.reload(); // 刷新
window.history.go(1); // 前进
window.history.go(-1); // 后退
window.history.forward(); // 前进
window.history.back(); // 后退 + 刷新
【扩展2】 history.back 与 history.go的区别:
history.back(-1); // 直接返回当前页的上一页,数据全部消息,是个新页面
history.go(-1); // 也是返回当前页的上一页,不过表单里的数据全部还在
//跳转
this.$router.push({
name: "goodsDetail",
query: {
goods_id: idValue,
sell_member_id: this.sellMemberId
}
});
this.$route当前路由,获取和当前路由有关的信息
fullPath: "" // 当前路由完整路径,包含查询参数和 hash 的完整路径
hash: "" // 当前路由的 hash 值 (锚点)
matched: [] // 包含当前路由的所有嵌套路径片段的路由记录
meta: {} // 路由文件中自赋值的meta信息
name: "" // 路由名称
params: {} // 一个 key/value 对象,包含了动态片段和全匹配片段就是一个空对象。
path: "" // 字符串,对应当前路由的路径
query: {} // 一个 key/value 对象,表示 URL 查询参数。跟随在路径后用'?'带的参数
//获取当前路由url的某个参数值
if (this.$route.query.isapp == 1) {
this.isapp = 1;
}
五:如何定义动态路由?如何获取传过来的动态参数?
(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 获取传递的值
六:Vue-router 路由钩子
全局路由钩子
router.beforeEach 进入路由之前
router.beforeResolve 全局解析守卫(2.5.0+)在 beforeRouteEnter【组件路由】 调用之后调用
router.afterEach 进入路由之后
- 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()
}
}
]
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
...
{
path: '/admin',
component: AdminDashboard,
meta: { requiresAuth: true, title: '管理后台' },
beforeEnter: (to, from, next) => {
// 检查用户是否登录
const isAuthenticated = checkAuth()
if (isAuthenticated) {
console.log('权限验证通过,允许进入管理后台')
next()
} else {
console.log('未登录,跳转到登录页')
next('/login?redirect=' + to.fullPath)
}
}
},
{
path: '/profile/:userId',
component: UserProfile,
beforeEnter: (to, from, next) => {
// 验证用户ID格式
const userId = to.params.userId
if (!/^\d+$/.test(userId)) {
console.log('用户ID格式错误')
next('/404') // 跳转到404页面
} else if (parseInt(userId) < 1) {
console.log('用户ID不存在')
next('/404')
} else {
console.log('用户ID验证通过')
next()
}
}
}
]
// 检查认证状态的函数
function checkAuth() {
return localStorage.getItem('userToken') !== null
}
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
组件路由钩子
- beforeRouteEnter 进入路由前
- beforeRouteUpdate 路由复用同一个组件时
场景:1重新加载数据:响应路由参数变化更新数据
2 同一组件不同参数间的切换
- beforeRouteLeave 离开当前路由时
场景:表单未保存提示
- 清理定时器或事件监听器
- 确认用户操作
beforeRouteEnter (to, from, next) {
// 在路由独享守卫后调用 不!能!获取组件实例 `this`,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用 可以访问组件实例 `this`
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用,可以访问组件实例 `this`
}
beforeRouteUpdate 案例
在当前路由改变,但该组件被复用时调用。
export default {
beforeRouteUpdate(to, from, next) {
// 可以访问 this
console.log('路由参数变化:', to.params.id)
// 重新加载数据
this.loadData(to.params.id)
next()
},
methods: {
loadData(id) {
// 根据新ID加载数据
console.log('加载ID为', id, '的数据')
}
}
}
beforeRouteLeave 案例
在离开当前组件对应的路由时调用。
export default {
data() {
return {
hasUnsavedChanges: false
}
},
beforeRouteLeave(to, from, next) {
if (this.hasUnsavedChanges) {
const answer = window.confirm(
'您有未保存的更改,确定要离开吗?'
)
if (answer) {
next()
} else {
next(false) // 取消导航
}
} else {
next() // 继续导航
}
}
}
触发钩子的完整顺序:
将路由导航、keep-alive、和组件生命周期钩子结合起来的,触发顺序,假设是从a组件离开,第一次进入b组件:
1 beforeRouteLeave:路由组件的组件离开路由前钩子,可取消路由离开。
2 beforeEach: 路由全局前置守卫,可用于登录验证、全局路由loading等。
3 beforeEnter: 路由独享守卫
4 beforeRouteEnter: 路由组件的组件进入路由前钩子。
5 beforeResolve:路由全局解析守卫
6 afterEach:路由全局后置钩子
7 beforeCreate:组件生命周期,不能访问this。
8 created:组件生命周期,可以访问this,不能访问dom。
9 beforeMount:组件生命周期
10 deactivated: 离开缓存组件a,或者触发a的beforeDestroy和destroyed组件销毁钩子。
11 mounted:访问/操作dom。
12 activated:进入缓存组件,进入a的嵌套子组件(如果有的话)。
13 执行beforeRouteEnter回调函数next。
七:Vue-router跳转和location.href有什么区别
location.href 简单方便,页面跳转刷新 Vue-router 的push 方法,就是pushstate 静态跳转无刷新页面,按需加载,减少dom的消耗
八:params和query的区别
url地址显示:query更加类似于ajax中get传参,params则类似于post,前者在浏览器地址栏中显示参数,后者则不显示
刷新问题 query刷新不会丢失query里面的数据,params刷新会丢失 params里面的数据。
九:Vue-router 导航守卫有哪些
- 全局前置/钩子:beforeEach、beforeResolve、afterEach
- 路由独享的守卫:beforeEnter
- 组件内的守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
十:对前端路由的理解
-
1:早期前端页面A跳转页面B时候,都是伴着页面的刷新,dom请求消耗大,体验不好
-
2:后来出现了改变局部刷新问题,出现了ajax技术,以此同时也要出现 一种单页面应用(SPA), 单页应用页面出现极大了改善体验问题,但是也出现两一些问题
-
3:单页应用页面出来两大问题:SEO 对浏览器搜索引擎,和 前几后退问题页面历史记录问题
-
4:为了解决两种问题,服务端是无法解决,就一个URL、一套资源只靠前端,所以出现了前端路由
-
5: 前端路由用截用户的刷新操作,避免服务端盲目响应,返回不符合预期的资源内容, 感知 URL 的变化
vue 有那些标签
template ` router-view router-link
vueX
什么vuex
vuex 是实现组件全局状态(数据)管理的一种机制,可以方便的实现 组件之间数据共享
面试题总结:‘’ juejin.cn/post/684490…
一:局部引入使用
A:直接用mutation方法
B:异步用actions方法
区别:就是actions。可以异步操作(mutation,再操作state)** *其中这个异步可以调用接口
二:全局引入使用方法
方法一: mutation
方法二:Actions方法(推荐)
三:全局引入使用拆分(推荐)
使用显示:
使用提交修改
四:Getters:属性
项目案例
//index.js 文件
import state from './state.js';
import mutations from './mutations.js';
export default new Vuex.Store({
state :state,
// 接收dispatch 传过来的值
mutations :mutations
})
// mutations.js文件
export default {
// 评论列表的当前播放的dom对象
setPlayObj(state,ev){
//console.log('videoDomObj',ev)
state.nowPlayDomObj=ev;
},
}
// 操作提交vue文件
import {mapMutations} from 'vuex' //引入vuex
methods :{
playFun(){
this.setPlayObj('提交的videoDom对象')//提到vuex
}
...mapMutations(['setPlayObj'])
},
//state.js 文件
let nowPlayDomObj='';
export default {
nowPlayDomObj,
}
Redux 和 Vuex 有什么区别,它们的共同思想
Vuex有哪几种属性?
有五种,分别是 State、 Getter、Mutation 、Action、 Module
- state => 基本数据(数据源存放地)
- getters => 从基本数据派生出来的数据
- mutations => 提交更改数据的方法,同步
- actions => 像一个装饰器,包裹mutations,使之可以异步。
- modules => 模块化Vuex
Vuex和单纯的全局对象有什么区别
-
vuex是响应的,组件读取store的状态,当这个store改变时候,组件高效率的更新
-
不能直接改变store状态,而是通过提交commit mutation;
为什么vuex的mutation 中不能做异步操作?
- vuex 所有的状态更新唯一途径是mutation,异步操作Action 来提交mutation 方便跟踪每个状态的变化
- 如果mutation支持异步操作,就没办法状态是何时更新的,无法很好的进行跟踪;
. 如何在组件中批量使用Vuex的getter属性
使用mapGetters辅助函数, 利用对象展开运算符将getter混入computed 对象中
import {mapGetters} from 'vuex'
export default{
computed:{
...mapGetters(['total','discountTotal'])
}
}
如何在组件中重复使用Vuex的mutation
使用mapMutations辅助函数,在组件中这么使用
import { mapMutations } from 'vuex'
methods:{
...mapMutations({
setNumber:'SET_NUMBER',
})
}
然后调用`this.setNumber(10)`相当调用`this.$store.commit('SET_NUMBER',10)`
axios常用的请求方式有哪些?
主要有get,post,put,patch,delete
-
get
获取数据
-
post
提交数据(表单提交+文件上传)
-
put
更新数据(将所有数据均推放到服务端)
-
patch
更新数据(只将修改的数据推送到后端)
-
delete
删除数据
单向数据流和双向数据绑定区别
Vue 的数据流本质上是单向数据流,而我们平时说的双向数据绑定,只是Vue的一个语法糖,也就是说,Vue在数据流方面既可以实现单向数据流也可以实现双向数据绑定。
如何设置动态class,动态style?
- 动态class对象:
<div :class="{ 'is-active': true, 'red': isRed }"></div> - 动态class数组:
<div :class="['is-active', isRed ? 'red' : '' ]"></div> - 动态style对象:
<div :style="{ color: textColor, fontSize: '18px' }"></div> - 动态style数组:
<div :style="[{ color: textColor, fontSize: '18px' }, { fontWeight: '300' }]"></div>
v-if和v-for不建议用在同一标签?
在Vue2中,v-for优先级是高于v-if的,咱们来看例子
<div v-for="item in [1, 2, 3, 4, 5, 6, 7]" v-if="item !== 3">
{{item}}
</div>
建议使用computed来解决这个问题:
<div v-for="item in list">
{{item}}
</div>
computed() {
list() {
return [1, 2, 3, 4, 5, 6, 7].filter(item => item !== 3)
}
}
不需要响应式的数据应该怎么处理?
// 方法一:将数据定义在data retrun 之外
data () {
this.list1 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list2 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list3 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list4 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list5 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
return {}
}
// 方法二:Object.freeze()
data () {
return {
list1: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list2: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list3: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list4: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list5: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
}
}
watch有哪些属性,分别有什么用?
watch: {
obj: {
handler () { // 执行回调
// do something
},
deep: true, // 是否进行深度监听
immediate: true // 是否初始执行handler函数
}
}
审查元素时发现data-v-xxxxx,这是啥?
是vue文件中的css 添加scoped标记产生的,因为保证文件中的css不相互影响
给每个组件做了唯一标记
@hook
“如何实现父组件监听子组件生命周期“ 以下是关于该需求的两个实现方式的理解
方法一
使用$emit
// 父组件
<template>
<div>
<Child
@mounted="onMounted"
@updated="onUpdated"
@beforeDestroy="onBeforeDestroy"
></Child>
</div>
</template>
// 子组件
...
mounted () {
this.$emit('mounted')
}
updated () {
this.$emit('updated')
}
beforeDestroy () {
this.$emit('beforeDestroy')
}
方法二
使用@hook:
// 父组件
<template>
<div>
<Child
@hook:mounted="onMounted"
@hook:updated="onUpdated"
@hook:beforeDestroy="onBeforeDestroy"
></Child>
</div>
</template>
// 子组件
<!--无-->
//父组件
<rl-child @hook:mounted="childMountedHandle"
/>
method () {
childMountedHandle() {
// do something...
}
},
复制代码
拓展
拓展1 助hook:我们还可以进行一些用法的拓展,这些拓展有时候可以提升我们代码的简洁性。
在编写组件时,我们往往需要在各个生命周期里都针对某个业务逻辑做一些处理,业务散落在各个生命周期钩子里:
<script type="text/ecmascript-6">
export default {
mounted () {
// 挂载时执行一些业务A相关逻辑
// 挂载时执行一些业务B相关逻辑
}
updated () {
// 更新时执行一些业务A逻辑
// 更新时执行一些业务B逻辑
// 更新时执行一些业务C逻辑
}
beforeDestroy () {
// 销毁时执行一些业务A逻辑
// 销毁时执行一些业务C逻辑
}
}
</script>
<script type="text/ecmascript-6">
export default {
created() {
this.$on('hook:mounted', () => {
挂载时执行一些业务A相关逻辑
})
this.$on('hook:updated', () => {
挂载时执行一些业务A相关逻辑
})
this.$once('hook:beforeDestroy', () => {
挂载时执行一些业务A相关逻辑
})
}
}
</script>
拓展2 将定时器赋值给一个全局变量或者绑定到this上,然后在另一个生命周期里获取并执行销毁操作。
// 优化前
<script type="text/ecmascript-6">
export default {
data() {
return {
timer:null
}
}
mounted () {
this.timer = setInterval(() => {
// todo
}, 1000);
}
beforeDestroy () {
clearInterval(this.timer)
}
}
</script>
// 优化后
<script type="text/ecmascript-6">
export default {
mounted () {
const timer = setInterval(() => {
// todo
}, 1000);
this.$once('hook:beforeDestroy', function () {
clearInterval(timer)
})
}
}
</script>
provide和inject是响应式的吗?
// 祖先组件
provide(){
return {
// keyName: { name: this.name }, // value 是对象才能实现响应式,也就是引用类型
keyName: this.changeValue // 通过函数的方式也可以[注意,这里是把函数作为value,而不是this.changeValue()]
// keyName: 'test' value 如果是基本类型,就无法实现响应式
}
},
data(){
return {
name:'张三'
}
},
methods: {
changeValue(){
this.name = '改变后的名字-李四'
}
}
// 后代组件
inject:['keyName']
create(){
console.log(this.keyName) // 改变后的名字-李四
}
Vue的el属性和$mount优先级?
比如下面这种情况,Vue会渲染到哪个节点上
new Vue({
router,
store,
el: '#app',
render: h => h(App)
}).$mount('#ggg')
复制代码
复制代码
这是官方的一张图,可以看出
el和$mount同时存在时,el优先级>$mount
相同的路由组件如何重新渲染?
开发人员经常遇到的情况是,多个路由解析为同一个Vue组件。问题是,Vue出于性能原因,默认情况下共享组件将不会重新渲染,如果你尝试在使用相同组件的路由之间进行切换,则不会发生任何变化。
方法一:
const routes = [
{
path: "/a",
component: MyComponent
},
{
path: "/b",
component: MyComponent
},
];
如果依然想重新渲染,怎么办呢?可以使用
key
<template>
<router-view :key="$route.path"></router-view>
</template>
方法二:
方法三:
组件构子函数 beforeRouteUpdate
export default {
beforeRouteUpdate(to, from, next) {
// 可以访问 this
console.log('路由参数变化:', to.params.id)
// 重新加载数据
this.loadData(to.params.id)
next()
},
methods: {
loadData(id) {
// 根据新ID加载数据
console.log('加载ID为', id, '的数据')
}
}
}
何将获取data中某一个数据的初始状态?
data() {
return {
num: 10
},
mounted() {
this.num = 1000
},
methods: {
howMuch() {
// 计算出num增加了多少,那就是1000 - 初始值
// 可以通过this.$options.data().xxx来获取初始值
console.log(1000 - this.$options.data().num)
}
}
vue 样式穿透
- vue常用的组件库,有scope的css样式都会另外加上其他的字符串,造成样式隔离;
- 将父组件的样式将渗透到子组件中;
解决办法:
>>>只作用于css;::v-deep只作用于sass;/deep/只作用于less;去掉scoped;
为什么Vue生命周期函数不能使用剪头函数书写
这里我们以生命周期beforeCreate为例:
通过看源码查看,找到调用位置,使用了callHook
我们接下来看看这个方法,使用了call\
vue指令
v-for /v-show / v-if/ v-else /v-else-if / v-once / v-text/ v-html / v-on /
v-bind/ v-model/
v-bind 缩写
<!-- 完整语法 -->
<a v-bind:href="url">...</a>
<!-- 缩写 -->
<a :href="url">...</a>
<!-- 动态参数的缩写 (2.6.0+) -->
<a :[key]="url"> ... </a>
v-on 缩写
<!-- 完整语法 -->
<a v-on:click="doSomething">...</a>
<!-- 缩写 -->
<a @click="doSomething">...</a>
<!-- 动态参数的缩写 (2.6.0+) -->
<a @[event]="doSomething"> ... </a>
diff原理
在 Vue.js 中,虚拟 DOM(Virtual DOM)的生成和更新主要发生在以下生命周期钩子中:
1. beforeUpdate 和 updated
beforeUpdate:在数据变化后,虚拟 DOM 重新渲染之前触发。updated:在虚拟 DOM 重新渲染并应用到真实 DOM 之后触发。- 说明:当组件的数据发生变化时,Vue 会生成新的虚拟 DOM 树,并与旧的虚拟 DOM 树进行对比(Diff 算法),然后更新真实 DOM。这个过程发生在
beforeUpdate和updated之间。
2. beforeMount 和 mounted
beforeMount:在虚拟 DOM 渲染为真实 DOM 之前触发。mounted:在虚拟 DOM 渲染为真实 DOM 并挂载到页面之后触发。- 说明:在组件首次渲染时,Vue 会生成虚拟 DOM,并将其转换为真实 DOM。这个过程发生在
beforeMount和mounted之间。
3. render 函数
render:Vue 的核心渲染函数,用于生成虚拟 DOM。- 说明:无论是首次渲染还是更新渲染,Vue 都会调用
render函数生成虚拟 DOM 树。render函数是虚拟 DOM 生成的关键步骤。
虚拟 DOM 的生命周期流程
-
首次渲染:
- 调用
render函数生成虚拟 DOM。 - 将虚拟 DOM 转换为真实 DOM 并挂载到页面。
- 触发
mounted钩子。
- 调用
-
更新渲染:
- 数据变化后,调用
render函数生成新的虚拟 DOM。 - 对比新旧虚拟 DOM(Diff 算法)。
- 更新真实 DOM。
- 触发
updated钩子。
- 数据变化后,调用
代码示例
export default {
data() {
return {
message: 'Hello, Vue!'
};
},
beforeMount() {
console.log('beforeMount: 虚拟 DOM 即将渲染为真实 DOM');
},
mounted() {
console.log('mounted: 虚拟 DOM 已渲染为真实 DOM');
},
beforeUpdate() {
console.log('beforeUpdate: 数据变化,虚拟 DOM 即将重新渲染');
},
updated() {
console.log('updated: 虚拟 DOM 已重新渲染并更新真实 DOM');
},
render(h) {
console.log('render: 生成虚拟 DOM');
return h('div', this.message);
}
};
总结
- 虚拟 DOM 的生成:主要在
render函数中完成。 - 虚拟 DOM 的更新:发生在
beforeUpdate和updated之间。 - 虚拟 DOM 的挂载:发生在
beforeMount和mounted之间。 - 虚拟 DOM 的生成主要发生在
render函数执行时,而render函数的调用是在beforeMount和mounted生命周期之间。
Vue 通过虚拟 DOM 的 Diff 算法高效地更新真实 DOM,从而提升性能。
component和 router-view 标签区别
<component> 是 Vue 的通用动态组件,而 <router-view> 是 Vue Router 的路由专用占位符
1. <component>:动态组件
<component> 是 Vue 内置的一个组件,其核心是 :is 属性。你可以把它理解为一个“万能组件壳”,它内部具体渲染什么,由 :is 的值决定。
使用场景:当你需要根据组件的内部状态(如 Tab 切换、步骤向导、模态框内容切换)来动态改变显示哪个组件时。
示例:Tab 切换
vue
<template>
<div>
<button @click="currentTab = 'Home'">Home</button>
<button @click="currentTab = 'About'">About</button>
<!-- 动态组件:根据 currentTab 的值渲染对应的组件 -->
<component :is="currentTab" />
</div>
</template>
<script>
import Home from './Home.vue'
import About from './About.vue'
export default {
components: { Home, About },
data() {
return {
currentTab: 'Home' // 控制显示哪个组件
}
}
}
</script>
在这个例子中,切换哪个标签页完全由组件内部的 currentTab 数据控制,与浏览器 URL 无关。
2. <router-view>:路由视图
<router-view> 是 Vue Router 库提供的组件,它是一个占位符。Vue Router 会根据当前访问的 URL 路径,去查找路由配置表,然后将匹配到的组件渲染到 <router-view> 的位置。
使用场景:构建单页面应用(SPA),根据不同的 URL 显示不同的页面级组件。
示例:路由配置
// router.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from './views/Home.vue'
import About from './views/About.vue'
import User from './views/User.vue'
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
{ path: '/user/:id', component: User }
]
const router = createRouter({
history: createWebHistory(),
routes
})
vue
<!-- App.vue -->
<template>
<div id="app">
<nav>
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
</nav>
<!-- 路由出口:当路径是 /about 时,About 组件会被渲染到这里 -->
<router-view />
</div>
</template>
在这个例子中,当你点击 <router-link> 时,URL 改变,Vue Router 自动将 About 组件渲染到 <router-view> 中。整个过程是由 URL 驱动的。
component和动态路由 addRouter 区别
<component> 和 addRoute 虽然都涉及"动态"的概念,但它们在 Vue 应用中的作用层次和目的完全不同。
核心区别概览
| 特性 | <component :is="..."> | router.addRoute() |
|---|---|---|
| 层级 | 组件层面的动态性 | 路由层面的动态性 |
| 作用时机 | 运行时动态切换组件 | 运行时动态添加路由规则 |
| 影响范围 | 局部组件渲染 | 全局路由配置 |
| 使用场景 | Tab 切换、条件渲染、组件动态加载 | 权限路由、插件扩展、模块懒加载 |
| 数据驱动 | 组件内部数据或计算属性 | 用户权限、应用状态、异步配置 |
router.addRoute() - 路由级动态配置
这是在路由配置层面的动态性,用于在运行时修改整个应用的路由表。
示例:基于权限的动态路由
// 主路由配置
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: Home },
{ path: '/login', component: Login }
// 初始没有管理后台路由
]
})
// 在用户登录后动态添加路由
function setupAdminRoutes(user) {
if (user.role === 'admin') {
router.addRoute({
path: '/admin',
component: AdminLayout,
children: [
{ path: 'dashboard', component: Dashboard },
{ path: 'users', component: UserManagement }
]
})
// 或者在现有路由中添加子路由
router.addRoute('main', {
path: 'settings',
component: Settings
})
}
}
// 登录成功后调用
login().then(user => {
setupAdminRoutes(user)
// 现在可以导航到 /admin/dashboard 了
})
特点:
- 修改的是全局路由配置
- 影响整个应用的导航能力
- 通常基于异步逻辑(如权限验证、功能模块加载)