如有错误,欢迎评论区指正(>v<)
ref和reactive区别(字节一面)
ref | reactive |
创建响应式的基本数据类型 | 创建一个响应式的对象或数组 |
基于 Object.defineProperty() | 基于 Proxy |
vue的双向绑定(字节一面)
原理:主要通过数据劫持和发布-订阅者模式实现,也就是说数据发送变化视图跟着改变,视图改变数据也会发送改变
核心:
数据劫持:Vue使用Object.defineProperty方法对数据对象的每个属性进行劫持,重写其getter和setter。当属性被访问或修改时,会触发相应的逻辑,从而监控数据变化
发布-订阅者模式:当数据变化时,发布者(数据对象)会通知所有订阅者(视图和其他依赖该数据的部分)进行更新。Vue利用Dep类来管理这些订阅者,并在数据变化时通知它们进行更新
实现过程
Observer:在Vue实例初始化时,通过Object.defineProperty重定义data中的所有属性,监听属性变动。当数据变化时,setter会被触发,通知订阅者进行更新
Watcher:Watcher负责监听数据变化并更新视图。它通过Dep类管理订阅者,并在数据变化时通知所有订阅者进行视图更新
Compiler:Compiler解析模板指令,将模板中 的变量替换成数据,并绑定更新函数。当数据变化时,Compiler通知Watcher进行视图更新
具体步骤
初始化数据:在Vue实例初始化时,通过Object.defineProperty重定义data中的所有属性,设置getter和setter来监听属性变动
依赖收集:在渲染视图时,访问数据对象的属性会触发getter,将当前的观察者(通常是渲染函数)添加到依赖列表中
视图更新:当数据变化时,setter会触发Dep类的notify方法,通知所有订阅者进行视图更新
Object.defineProperty(obj, prop, descriptor)
obj:要在其上定义属性的对象
prop:要定义或修改的属性的名称
descriptor:属性的描述符对象,包含以下属性:(具体改变的方法)
- value:属性的值,默认为undefined
- writable:属性是否可写,默认为false
- enumerable:属性是否可枚举,默认为false
- configurable:属性是否可配置(即是否可以通过delete删除或重新配置),默认为false
- get和set:获取和设置属性值的函数,使用访问器描述符时使用
实现过程
1.初始化的时候,把data中绑定的数据给他显示到输入框和文本节点里
2.输入框的内容改变,data里对应属性的值也要发生改变
3.data的值改变,那么文本节点的内容也要发生改变 model->view
model->view
修改输入框,改变了vm实例的属性,1-1的关系
data里的数据对应节点,是1对多的关系,利用发布-订阅者模式来解决(发布-订阅者模式就是专门处理一对多的关系,让多个订阅者同时监听一个发布者)
Dep发通知,订阅者触发update,更新视图
文档碎片 documentfragment
把n多个节点放入这个容器里,最后再把容器放回到文档里面,让浏览器只回流了1次
vue中出现数据更新视图不更新的原因是什么?
响应式系统,基于Object.defineproperty()实现,可以追踪对象属性的变化,但是无法检测到某些特殊情况下的变化
- 直接新增或删除对象的属性
vue无法检测到对象新增、删除的属性,因为它只对初始化时存在的属性进行响应式处理
解决:使用 Vue.set
或 this.$set
方法来添加响应式属性
- 直接替换数组索引或长度
vue2没有办法检测到通过索引直接修改数组元素或修改数组长度的操作
解决:使用数组方法(如 push
、pop、splice
等)来修改数组,确保 Vue 能够追踪变化
VUE2和VUE3的区别
区别 | VUE2 | VUE3 |
响应式系统 | Object.defineproperty() | proxy() |
API | 选项式API data methods created | 组合式API setup ref reactive... |
生命周期 | beforeCreate created | 加了on onBeforeCreate |
支持fragment | 要求组件只能有一个根节点 | 支持多个根节点(fragment) |
hooks | mixins | hooks |
teleport组件 | 编写可以出现在父组件之外的弹窗、模态框 to目标位置 | |
支持tree-shaking | 不支持 | 去除一些没有使用的代码的技术(按需打包) |
获取组件实例 | this | getCurrentInstance |
VUE3优化
静态树提升,如果有个静态节点树,vue在编译的时候会把它标记为静态,后续的更新中,vue不会再重新渲染这一部分
引入patchflag,vue在更新的时候,只关注已经确定发生改变的部分
mixins和hooks区别
mixins | hooks |
共享同一个对象,可能导致命名冲突 | 不会有命名冲突问题,每个hooks都有自己的作用域 |
使组件实例难以理解,一个组件可能从多个mixins继承属性和方法,使得组件逻辑复杂难懂 | 可以单独和组合使用,使得逻辑更加清晰模块化,易于理解和维护 |
不支持TypeScript的类继承 | 与TypeScript兼容 |
mixins:
// mixins.js
export default{
data(){
return {
msg:'hello world'
}
},
created(){
console.log('123456')
},
methods:{
getters(){
console.log(this.msg)
}
}
}
//index.vue
<template>
<div>
<p>{{msg}}</p>
<p @click='getters'>获取</p>
</div>
</template>
<script>
import mixins from './mixins'
export default{
mixins:[mixins],
}
</script>
hooks:
// hooks.js
import {ref} from 'vue'
export function hooks(){
const error = ref(null)
function validate(value){
if(!value){
error.value = '这个必须有'
}esle{
error.value = null
}
}
return { error,validate }
}
// index.vue
<template>
<form @submit='onSubmit'>
<input v-model='email' />
<button type='submit'>提交</button>
<p v-if='error'>{{error}}</p>
</form>
</template>
<script>
import {hooks} from './hooks'
export default{
setup(){
const {error,validate} = hooks
function onSubmit(){
validate(email.value)
if( error.value ){
//报错 提示信息
}else{
//提交表单
}
}
return {error , onSubmit}
}
}
</script>
nextTick
官方:nextTick是等待下一次DOM更新刷新的工具方法
当修改了响应式数据,vue不会立刻更新DOM,而是把更新操作放进异步队列中,把组件更新函数保存在队列中,在同一事件循环中发生的所有数据变更会异步的批量更新(Promise.then放入微任务队列中)。当前事件循环结束之后,清空执行栈再处理异步任务,这样可以避免频繁的操作DOM
nextTick是确保DOM更新完成之后执行回调函数,可拿到更新后的dom
原理:利用事件循环,Vue优先选择微任务(当前环境不支持微任务时会回退至宏任务),当DOM更新完成后才会执行nextTick的回调,那么nextTick也就可以拿到更新后的数据
场景:
- 获取更新后的DOM元素(ex: created中想要获取DOM)
- DOM更新后初始化一些第三方库,比如:图表、动画库
- 响应式数据变化后获取DOM更新后的状态(ex: 获取列表更新后的高度)
v-if和v-show区别
v-if: 通过创建/删除dom元素来控制显隐
v-show: 通过修改css属性display来控制显隐
VUE的跨域怎么解决的?
env和pro开头的文件确定了生产环境(上线)www.baidu.com和开发环境http://39.100...所请求的域名
生产环境不存在跨域问题,服务器和服务器之前之前不存在跨域问题
产生跨域原因:因为浏览器的同源策略(协议+域名+端口号完全一致)
本质:把需要代理的域名(本地域名)代理到服务器上,就不存在跨域了
JSONP(仅get请求) CORS(后端配置)
方法:
JSONP(script标签不受跨域限制,但仅能进行get请求)
CORS跨域
需要前端后端同时支持,CORS请求在浏览器上完成,关键在于服务器
简单请求:HEAD GET POST
请求头字段有限制,请求由浏览器直接发出,在请求头添加Origin字段(说明请求来自哪个源:协议+域名+端口号),服务端验证
服务端需要配置字段:Access-Control-Allow-Origin(允许的源,*代表都允许)
如果响应数据头信息包含Access-Control-Allow-Origin字段,则允许跨域;如果Orign指定的域名不在许可范围之内,服务器会返回一个正常的HTTP回应,浏览器发现没有上面的Access-Control-Allow-Origin头部信息,就知道出错了。
非简单请求
非简单请求是对服务器有特殊要求的请求,比如请求方法为DELETE或者PUT等。非简单请求的CORS请求会在正式通信之前进行一次HTTP查询请求,称为预检请求OPTIONS。浏览器会询问服务器,当前所在的网页是否在服务器允许访问的范围内,以及可以使用哪些HTTP请求方式和头信息字段,只有得到肯定的回复,才会进行正式的HTTP请求,否则就会报错。
发起OPTIONS请求询问,头信息中的关键字段是Orign,还包含Access-Control-Request-Method、Access-Control-Request-Headers字段,服务器根据头信息这三个字段进行判断
服务端需要配置字段(至少):Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Hesders
如果返回的头信息在中有Access-Control-Allow-Origin这个字段就是允许跨域请求,如果没有,就是不同意这个预检请求,就会报错
注意:只要服务器通过了预检请求,在以后每次的CORS请求都会自带一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段
组件传值(父->子、子->父、兄弟组件传值、跨组件传值)
父 --> 子
// 父组件
<template>
<div>
<ChildComponent :message="parentMessage" /> //这里数据绑定除了String类型,可以赋其他常量
</div>
</template>
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
const parentMessage = ref('Hello from parent');
</script>
// 子组件
<template>
<div>
<p>{{ message }}</p>
</div>
</template>
<script setup>
import { defineProps } from 'vue';
const props = defineProps({
message: String
});
//或者 const props = defineProps(['message'])
</script>
子 -> 父
子组件可以通过 $emit
触发自定义事件,并将数据传递给父组件。父组件监听这些事件并执行相应的处理逻辑
// 父组件
<template>
<div>
<ChildComponent @sendMessage="handleMessage" /> //传入函数并监听
<p>{{ message }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
const message = ref('');
const handleMessage = (data) => {
message.value = data;
};
</script>
// 子组件
<template>
<button @click="sendMessage">Send Message</button>
</template>
<script setup>
import { defineEmits } from 'vue';
const emit = defineEmits(['sendMessage']); // 1
const sendMessage = () => {
emit('sendMessage', 'Hello from child'); // 2
};
</script>
兄弟组件传值
通过共享的事件总线(Event Bus)实现
// eventBus.js
import { defineComponent } from 'vue';
export const eventBus = defineComponent({
methods: {
emit(eventName, data) {
this.$emit(eventName, data);
},
on(eventName, callback) {
this.$on(eventName, callback);
}
}
});
//兄弟组件1
<template>
<button @click="sendMessage">Send Message</button>
</template>
<script setup>
import { eventBus } from './eventBus.js';
const sendMessage = () => {
eventBus.emit('sendMessage', 'Hello from sibling 1');
};
</script>
// 兄弟组件2
<template>
<div>
<p>{{ message }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { eventBus } from './eventBus.js';
const message = ref('');
eventBus.on('sendMessage', (data) => {
message.value = data;
});
</script>
全局状态管理(如 Vuex)实现
// store.js
import { createStore } from 'vuex';
export default createStore({
state: {
message: ''
},
mutations: {
setMessage(state, message) {
state.message = message;
}
}
});
// 兄弟组件1
<template>
<button @click="sendMessage">Send Message</button>
</template>
<script setup>
import { useStore } from 'vuex';
const store = useStore();
const sendMessage = () => {
store.commit('setMessage', 'Hello from sibling 1');
};
</script>
// 兄弟组件2
<template>
<div>
<p>{{ message }}</p>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useStore } from 'vuex';
const store = useStore();
const message = computed(() => store.state.message);
</script>
跨层级组件传值
祖先组件可以通过 provide
提供数据,后代组件可以通过 inject
注入这些数据
// 祖先组件
<template>
<div>
<GrandChildComponent />
</div>
</template>
<script setup>
import { provide, ref } from 'vue';
import GrandChildComponent from './GrandChildComponent.vue';
const ancestorMessage = ref('Hello from ancestor');
provide('ancestorMessage', ancestorMessage);
</script>
// 后代组件
<template>
<div>
<p>{{ message }}</p>
</div>
</template>
<script setup>
import { inject } from 'vue';
const message = inject('ancestorMessage');
</script>
任意组件之间
使用 eventBus ,其实就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。
父子组件生命周期的执行顺序
父:
beforeCreate
created
beforeMount
子:
beforCreate
created
beforeMount
mounted
父:
mounted
VUE生命周期
VUE2 | Vue3 |
beforeCreate | setup |
created | |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeDestroy | onBeforeUnmount |
destroy | onUnmounted |
VUE的路由模式
hash
开发中默认的模式,使用锚点技术重写URL访问路径(纯静态技术),在原有的URL路径后拼接/#/xx,解决了单页面应用的页面划分,能够在不触发网页重新加载的情况下切换URL路径。hash值变化对应的URL都会被浏览器记录下来,这样浏览器就能实现页面的前进和后退
原理: onhashchange()事件,window会监听hash值变化,按照规则加载相应代码
缺陷:hash模式的URL路径只能存在一个#,当嵌套的子应用和主应用都使用hash模式时,在定义URL路径上存在困难。视觉上不美观(逼死强迫症)
history
VueRouter中常用的一种路由模式,history模式使用的URL路径不存在#(不使用锚点技术),视觉上更美观。采用history对象中的pushState()函数重写URL路径
原理:利用点击事件,使用history.pushState重写页面路径,再加载对应DOM对象
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>页面跳转</title>
<style>
.page {
width: 400px;
height: 400px;
}
.about {
background-color: antiquewhite;
display: none;
}
.index {
background-color: aquamarine;
display: none;
}
</style>
</head>
<body>
<!-- 定义路由菜单 -->
<a href="javascript:jump('/index')">跳转到index页面</a>
<a href="javascript:jump('/about')">跳转到about页面</a>
<!-- 定义页面结构 -->
<div class="page index">我是首页</div>
<div class="page about">我是关于页面</div>
<script type="text/javascript">
// 跳转函数
function jump(path) {
// 重写URL路径为超链接传入的名称
window.history.pushState(null, 'page', path);
// 获取所有页面组件
var pages = document.querySelectorAll('.page');
// 获取指定跳转的目标页面对象
var newPage = document.querySelector(path.replace('/', '.'))
console.log(newPage);
// 隐藏其他页面
pages.forEach(item => item.style.display = 'none');
// 展示跳转的页面
newPage.style.display = 'block';
}
</script>
</body>
</html>
缺陷:重写后的新路径中并不包含原有HTML物理文件的访问地址,所以在重写URL路径后,一旦刷新网页会造成404无法访问
解决:上线时,服务器端(配置)将url中的路径重定向为可以找到的物理文件地址
computed和watch区别
computed | watch |
具有只读的响应式的返回值(相当于一个响应式数据的派生数据) | 侦测变化,执行回调 |
具备缓存性 | 无缓存 |
传递对象,既可读又可写 | 传递对象,设置deep(观察深层次变化)、immediate(初始化时立即执行)选项 |
$route和$router区别
$route: “路由信息对象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数
$router: “路由实例”对象包括了路由的跳转方法,钩子函数...
vuex和pinia区别
区别/状态管理 | vuex | pinia |
API | 需要定义state 、getters 、mutations、 actions | 更简洁,直接通过 actions 修改状态 |
类型支持 | 需要配置支持ts | 原生支持ts, |
模块管理 | 需要通过模块注册和嵌套来管理状态,更复杂(模块化需额外配置modules) | 可以直接在模块中定义状态和方法,更方便(自动模块化store即模块) |
插件支持 | 较为成熟的插件生态系统 | 相对较少 |
pinia为什么比vuex快?
- pinia的设计更加轻量
- pinia的内存使用更加高效(占用内存更小)
- 更好的响应式性能:Pinia 基于 Vue 3 的 Composition API 构建,利用了 Vue 3 的新响应式系统。这使得 Pinia 在处理大型状态时更加流畅,减少了滞后现象
虚拟DOM、真实DOM
定义:VUE中的虚拟DOM(Virtual DOM)是一种在内存中表示真实DOM的轻量级JavaScript对象模型,可以用来描述真实DOM的结构和状态 ,真实DOM:浏览器提供的原生DOM(documen object model),每次的修改都会触发重绘和回流
作用:避免直接频繁操作真实DOM,提高性能
实现过程:JS对象来模拟DOM树,更新的时候用Diff算法对比新旧虚拟DOM树,然后把差异点应用到真实DOM上
具体实现:
// 创建虚拟DOM(旧虚拟DOM树)
const virtualDOM = {
tag:'div',
attrs:{id:'app'},
children:[
{
tag:'p',
attrs:{},
children:['hello,world!']
}
]
}
// 更新虚拟DOM(新虚拟DOM树)
const virtualDOM = {
tag:'div',
attrs:{id:'app'},
children:[
{
tag:'p',
attrs:{},
children:['更新的数据']
}
]
}
Diff算法
定义:用来优化DOM更新的核心机制。当你使用Vue进行数据绑定和组件更新时,diff算法帮助Vue高效地识别哪些部分的实际DOM需要被改变,从而只更新必要的部分,而不是整个页面或组件。
核心思想:通过比较新旧虚拟DOM树的差异,找到最小变化应用到真实DOM上
特点:
- 同层级比较
- 基于key值的节点复用(标签属性tag和key值相同,input要加type属性,均相同则节点相同)
- 优化策略(提升diff算法的策略 ):首尾指针、双端比较
diff的时机:当组件创建时以及依赖的属性或数据发生变化时,会运行一个函数,该函数会做两件事:
- 运行_render生成一棵新的虚拟DOM树(vnode tree)
- 运行_update,传入虚拟DO树的根节点,对新旧两棵树进行对比,最终完成对真实DOM的更新
// vue构造函数
function Vue(){
// ...其他代码
var updateComponenet = () => {
this._update(this._render())
}
new Watcher(updateComponent)
// ...其他代码
}
diff算法就发生在_update函数的运行过程中
_update函数:收到一个vnode参数(新生成的虚拟dom树),同时_update函数通过当前组件的_vnode属性拿到旧虚拟dom树,_update函数首先给组件的_vnode重新赋值,让它指向新树,然后判断旧树是否存在:
- 不存在:第一次加载组件,通过内部patch函数(diff)直接(深度|广度)遍历新树,为每个节点生成真实DOM,挂载到每个节点的elm属性上
- 存在:之前已经渲染过该组件,通过patch函数对新旧两棵树进行对比,完成对所有真实DOM的最小化处理,让新树的节点对应合适的真实DOM
首尾指针、双端比较:(自己理解)
1.新首-旧首比较:相同,都移至下一位继续比较;不同,新尾-旧尾比较
2.新尾-旧尾比较:相同,都移至上一位继续比较;不同,新尾-旧头比较
3.新尾-旧头比较:相同,新节点指向真实dom对应节点且真实dom节点移至对应位置,新尾前移,旧头后移,继续比较;不同,新头-旧尾比较
4.新头-旧尾比较:相同,新节点指向真实dom对应节点且真实dom节点移至对应位置,新头后移,旧尾前移;不同,以新头为准值,在旧dom上寻找是否有该节点,有则新节点指向真实dom对应节点且真实dom节点移至对应位置,无则在真实dom节点对应位置添加新节点
新头在新尾后则结束循环,判断旧树指针指向是否正常,正常则循环(头指针->尾指针),销毁循环内还存在的所有真实dom节点
旧头-新头比较,若相同则将新节点指向真实dom,更新变化的数据 (若有子节点,继续深度优先遍历) --> 旧头、新头后移一位继续比较,若不同 --> 比较旧尾-新尾,若相同则旧尾新尾向前移一位继续比较,若不同 --> 比较旧头-新尾,若相同新尾节点指向真实dom并移动真实dom节点至相对新节点的真实dom节点位置,两个相同节点各自继续向'前'移动 --> 旧头-新头比较,不同,旧尾-新尾比较,不同,旧头-新尾比较,不同,新头-旧尾比较,不同(4次比较) --> 以新头为基准,在旧dom树中寻找是否存在,若存在,该新节点指针指向真实节点并移至对应位置,新头后移一位 --> 新头-旧头不同,新尾-旧尾不同,在旧dom树中寻找是否有该节点,没有,则在真实dom对应位置中新增节点,新头后移,头指针在尾指针后结束循环;判断旧树指针指向是否正常,正常则循环(头指针->尾指针),销毁循环内还存在的所有真实dom节点
key值的作用
每个节点都有唯一的key,那么vue就可以根据key快速的判断节点是否复用,如果没有设置key值VUE默认用index索引值。
总结:
虚拟DOM就是提供了一种高效的 机制来描述和更新真实DOM
diff算法就是通过同层比较和最小化DOM操作,来优化性能和视图渲染
key是vue里用来标识节点身份的重要属性,它可以优化vue在diff算法中能更高效的识别节点,避免不必要的DOM操作,如果不设置key或用index,可能会导致性能下降和逻辑出错。
key值都可以使用在哪些地方?
唯一标识:key值的主要作用是为每一个虚拟DOM节点提供一个唯一标识。在Vue.js中,当我们使用v-for指令循环渲染列表时,key值能够帮助Vue区分每个列表项,而不仅仅是根据顺序来判断。
优化渲染性能:特别是在列表的顺序发生变化时。Vue.js使用diff算法来比较新旧虚拟DOM,当key值存在时,Vue能够更高效地判断哪些元素需要更新、添加或删除,从而减少不必要的DOM操作。
保持组件状态: 当组件在列表中被重新排序或替换时,使用key值可以帮助vue.js正确识别和匹配组件,确保每个组件的状态不会丢失。