内容主题中有vue2 和 vue3进行比较的内容,也有部分和ract对比的内容,比较重要的内容我都会写出代码,You can keep watching if you need to ,OR NO 出门右转,内容持续更新中,后续会收集或遇到好的问题...
- 12月29日 更新了
$atter和$listeners
1. vue3为什么要升级proxy,废弃defineProperty
defineProperty 无法监听数组索引和长度的变化,无法监听对象新增属性,必须在初始化时就遍历对象的所有层级
proxy 正好完美的避免了这些问题:不需要递归变量对象,在使用的时候才会收集依赖,懒收集
2. 路由守卫的使用
有全局路由守卫和组件内守卫,局部路由守卫
全局路由守卫前置 router.beforeEach,常用于检测用户是否登录,跳转大登录页面
全局路由守卫后置router.afterEach,常用于每次路由跳转完成记录用户访问日志
路由守卫用在路由中
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: (to, from) => {
// reject the navigation
return false
},
},
]
3. Composition API与React Hook很像,区别是什么
- 执行时机
- react 每次渲染都会执行
- vue setup 只会执行一次,响应式系统处理更新
- 响应式系统
- react 需要手动触发更新
- vue 自动追踪依赖更新
- 性能优化
- react 需要手动进行性能优化(useMemo,useCallback)
- vue 响应式系统自动优化
- Compositon API的调用不需要顾虑调用顺序,也可以在循环、条件、嵌套函数中使用,而react 只能在顶层作用域中调用hooks
4. 为什么Vue不使用React 的分片更新?
其实这个问题也可以叫做:为什么 Vue 不需要时间分片?对于这个问题其实尤雨溪也在英文社区里回答过,也有前端大牛翻译发布在公众号上,那么下面我也进行一下总结。
- 第一,首先时间分片是为了解决 CPU 进行大量计算的问题,因为 React 本身架构的问题,在默认的情况下更新会进行很多的计算,就算使用 React 提供的性能优化 API,进行设置,也会因为开发者本身的问题,依然可能存在过多计算的问题。
- 第二,而 Vue 通过响应式依赖跟踪,在默认的情况下可以做到只进行组件树级别的更新计算,而默认下 React 是做不到的(据说 React 已经在进行这方面的优化工作了),再者 Vue 是通过 template 进行编译的,可以在编译的时候进行非常好的性能优化,比如对静态节点进行静态节点提升的优化处理,而通过 JSX 进行编译的 React 是做不到的。
- 第三,React 为了解决更新的时候进行过多计算的问题引入了时间分片,但同时又带来了额外的计算开销,就是任务协调的计算,虽然 React 也使用最小堆等的算法进行优化,但相对 Vue 还是多了额外的性能开销,因为 Vue 没有时间分片,所以没有这方面的性能担忧。(时间分片本身就是性能开销)
- 第四,根据研究表明,人类的肉眼对 100 毫秒以内的时间并不敏感,所以时间分片只对于处理超过 100 毫秒以上的计算才有很好的收益,而 Vue 的更新计算是很少出现 100 毫秒以上的计算的,所以 Vue 引入时间分片的收益并不划算。
5. vue中的data为什么是一个函数?
Vue 中的 data 必须是个函数,因为当 data 是函数时,
组件实例化的时候这个函数将会被调用,返回一个对象,
计算机会给这个对象分配一个内存地址,实例化几次就分配几个内存地址,
他们的地址都不一样,所以每个组件中的数据不会相互干扰,改变其中一个组件的状态,其它组件不变。
简单来说,就是为了保证组件的独立性和可复用性,如果 data 是个函数的话,
每复用一次组件就会返回新的 data,类似于给每个组件实例创 建一个私有的数据空间,
保护各自的数据互不影响
解释的不错:https://blog.csdn.net/qq_42072086/article/details/108060494
6. MVC 和 MVVM的区别
MVC:M(model数据)、V(view视图),C(controlle控制器)
缺点是前后端无法独立开发,必须等后端接口做好了才可以往下走;
前端没有自己的数据中心,太过依赖后台
MVVM:M(model数据)、V(view视图)、VM(viewModel控制数据的改变和控制视图)
html部分相当于View层,可以看到这里的View通过通过模板语法来声明式的将数据渲染进DOM元素,
当ViewModel对Model进行更新时,通过数据绑定更新到View。
Vue实例中的data相当于Model层,而ViewModel层的核心是Vue中的双向数据绑定,
即Model变化时VIew可以实时更新,View变化也能让Model发生变化
MVVM与MVC最大的区别就是:它实现了View和Model的自动同步,
也就是当Model的属性改变时,我们不用再自己手动操作Dom元素,来改变View的显示,
而是改变属性后该属性对应View层显示会自动改变
7. v-model 原理
是采用数据劫持结合发布者-订阅者模式的方式,
通过Object.defineProperty()来劫持各个属性的setter,getter,
在数据变动时发布消息给订阅者,触发相应的监听回调从而达到数据和视图同步。
----------------------------------------
v-model只不过是一个语法糖而已,真正的实现靠的还是
v-bind:绑定响应式数据
触发oninput 事件并传递数据
-----------------------
https://blog.csdn.net/qq_43742385/article/details/114572773
<input
v-bind:value="searchText"
v-on:input="searchText = $event.target.value"
>
<!--
自html5开始,input每次输入都会触发oninput事件,
所以输入时input的内容会绑定到searchText中,于是searchText的值就被改变;
$event 指代当前触发的事件对象;
$event.target 指代当前触发的事件对象的dom;
$event.target.value 就是当前dom的value值;
在@input方法中,value => searchText;
在:value中,searchText => value;
-->
---------------
https://juejin.cn/post/7090841582173192199
通过 v-bind:value 绑定 username 变量,每次输入内容的时候触发input事件
通过事件对象参数 event.target.value 获得输入的内容,并且把这个内容赋值给username
此时更改username时input输入框会变化,更改input输入框时username变量会变,从而实现了v-model的双向绑定功能
vue2 和 vue3 的区别
- 默认触发更新方法不同
this.$emit('input',8888),vue3emit('update:modelValue',8888) - vue3 支持一个组件多个v-model
<!-- Vue3 的实现 -->
<template>
<!-- 父组件 -->
<custom-input v-model="message"/>
<!-- 等价于 -->
<custom-input
:modelValue="message"
@update:modelValue="message = $event"
/>
</template>
<!-- 子组件 -->
<script setup>
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const updateValue = (val) => {
emit('update:modelValue', val) // Vue3 使用 update:modelValue 事件
}
</script>
<!-- Vue2 的实现 -->
<template>
<!-- 父组件 -->
<custom-input v-model="message"/>
<!-- 等价于 -->
<custom-input
:value="message"
@input="message = $event"
/>
</template>
<!-- 子组件 -->
<script>
export default {
props: {
value: String
},
methods: {
updateValue(val) {
this.$emit('input', val) // Vue2 使用 input 事件
}
}
}
</script>
8. v-if 和 v-show的区别
v-if是通过添加和删除元素来进行显示或者隐藏
v-show是通过操作DOM修改display样式来修改元素的显示和隐藏
如果需要频繁的进行元素的显示和隐藏使用v-show性能更好
9. v-for中为什么要有key
key 可以提高虚拟DOM的更新效率。
在vue中,默认“就地复用”的策略,在DOM操作的时候,如果没有key 就会造成选项错乱
key 只能是字符串或者number,其他类型不可以
1. 虚拟DOM中key的作用:
key是虚拟DOM对象的标识,当数据发生变化时,
Vue会根据【新数据】生成【新的虚拟DOM】, f
随后Vue进行【新的虚拟DOM】与【旧的虚拟DOM】差异比较比较规则如
2. 比较规则:
1)旧虚拟DOM找到了与新虚拟DOM相同的key:
若虚拟DOM中内容没变,直接使用之前的真实DOM
若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM
3. 用index作为key可能会引发的问题:
1)若对数据进行:逆序添加、逆序删除等破坏顺序的操作,会产生没有必要的真实DOM更新==>界面效果没问题,但效率低
2)如果结构中还包含输入类的DOM,会产生错误的DOM更新 ==> 界面有问题
10. 打包后 dist 目录过大,解决办法?
- dist打包生成的文件中有 .map 文件,可以删除。在 vue.config.js文件中配置:productionSourceMap: false
- 组价和路由使用懒加载、按需引入等
- 对于文件和图片进行压缩。 安装压缩组件: compression-webpack-plugin\ 安装后进行导入配置:\ 最小化代码 minisize: true\ 分割代码: splitChunksl\ 超过限定值的文件进行压缩,threshold: 文件大小(字节为单位)
11. computed 和 watch 的区别
- computed是计算属性,watch是监听器,用来监听某一个值的变化进而触发相应的回调
- computed中的函数必须要有return返回、watch没有必须的要求返回return
- computed是第一次加载就触发一次,watch首次加载不会触发,如果需要首次加载需要设置immediate属性
- computed中的函数所依赖的属性没有发生变化,那么调用当前的函数的时候会从缓存中获取;而watch在每次监听值发生变化的时候都会执行回调。
// 6. 控制执行时机
watch(source, callback, {
flush: 'post' // 组件更新后执行
// flush: 'pre' // 组件更新前执行(默认)
// flush: 'sync' // 同步执行
})
// 7. 停止监听
const stopWatch = watch(count, () => {
console.log('watching...')
})
// 某个时刻停止监听
stopWatch()
使用场景:watch 比如购物车商品结算功能,computed 国际化中使用单位的换算时有使用
12. vue组件之间的数据传递
父 传 子
通过在父组件中的子组件身上绑定自定义属性,然后再子组件里使用props属性来接收即可\
爷传孙
vue2 可以直接 通过 this.atter
vue3 组合式api 通过useAtter() ,选项api 使用$atter
<!-- 爷爷组件 Grandfather.vue -->
<template>
<Father
:message="message"
:data="someData"
@customEvent="handleEvent"
/>
</template>
<script setup lang="ts">
const message = ref('来自爷爷的消息')
const someData = ref({ value: 'some data' })
const handleEvent = () => {
console.log('处理事件')
}
</script>
<!-- 父组件 Father.vue -->
<template>
<Grandson v-bind="$attrs" />
</template>
<script setup lang="ts">
// 重要:禁用 attributes 继承
defineOptions({
inheritAttrs: false
})
</script>
<!-- 孙子组件 Grandson.vue -->
<template>
<div>
<p>爷爷传来的消息: {{ attrs.message }}</p>
<p>爷爷传来的数据: {{ attrs.data.value }}</p>
<button @click="attrs.customEvent">触发爷爷的事件</button>
</div>
</template>
<script setup lang="ts">
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>
孙传爷
vue2 可以直接 通过 this.atter
vue3 组合式api 通过useAtter() ,选项api 统一使用$atter
子 传 父
1)第一种方式:通过父组件给子组件传递函数类型的props实现:子组件给父组件传递数据\ 父组件:\ \
子组件:\
2)第二种方式:通过父组件给子组件绑定一个自定义事件实现:子组件给父组件传递数据 $emit 事件传递
// 子组件
export default {
methods: {
handleClick() {
// 发送事件和数据
this.$emit('update', {
name: 'John',
age: 20
});
}
}
}
// 父组件
<child-component
@update="handleUpdate"
/>
export default {
methods: {
handleUpdate(data) {
console.log(data); // { name: 'John', age: 20 }
}
}
}
3)第三种方式:通过父组件给子组件绑定一个自定义事件实现:使用ref实现 ref 引用
// 父组件
<child-component ref="childRef" />
export default {
mounted() {
// 访问子组件的数据或方法
console.log(this.$refs.childRef.data);
this.$refs.childRef.someMethod();
}
}
// 子组件
export default {
data() {
return {
data: 'child data'
}
},
methods: {
someMethod() {
// ...
}
}
}
4)第四种方法使用v-model 方法传值
-------------------vue2------------------
// 1. 基础用法
// 父组件
<child-component v-model="value" />
// 子组件
export default {
props: {
value: String // 必须用 value 这个名字
},
methods: {
updateValue(newValue) {
this.$emit('input', newValue) // 必须用 input 事件
}
}
}
// 2. 自定义 prop 和事件名
// 子组件
export default {
model: {
prop: 'title', // 自定义 prop 名
event: 'change' // 自定义事件名
},
props: {
title: String
},
methods: {
updateTitle(val) {
this.$emit('change', val)
}
}
}
-------------------vue3------------------
// 父组件
<template>
<child-component v-model.capitalize="message" />
<p>父组件的值: {{ message }}</p>
</template>
<script setup>
import { ref } from 'vue'
const message = ref('')
</script>
// 子组件
<template>
<input
:value="modelValue"
@input="handleInput"
/>
</template>
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) }
})
const emit = defineEmits(['update:modelValue'])
const handleInput = (e) => {
let value = e.target.value
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
emit('update:modelValue', value)
}
</script>
全局事件总线:可以实现任意组件间的数据传递
main.js:将全局事件bus,挂载到Vue的原型上,这样所有的组件都可以使用\ \ 兄弟组件A:\
\ 兄弟组件B:\
\ 4. 消息订阅与发布\ 一种组件间的通信方式,适用于任意组件间通信。
使用步骤:
- 安装pubsub: npm i pubsub-js
- 引入: import pubsub from ‘pubsub-js’
- 接收数据: A组件想要接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身
mounted() {
this.pid = punsub.subscribe('xxx', (data)=>{
......
})
}
- 提供数据: pubsub.publish(‘xxx’, 数据)
- 最好在beforeDestory钩子中,用pubsub.unsubscribe(pid)取消订阅
vue原型 和 原型链
不管是普通函数还是构造函数,都有一个属性prototype,用来指向这个函数的原型对象。
同时原型对象也有一个constructor属性,用来指向这个原型对象的构造函数。
原型对象的作用:用来存储实例化对象的公有属性和方法
例如
function Person (name, age) {
this.name = name
this.age = age
this.say = function () {
console.log('我会说话')
}
}
let person1 = new Person('小明', 20)
console.log(person1.name) // 小明
console.log(person1.age) // 20
console.log(person1.say()) // 我会说话
如上:实例化出来的person1,就可以使用构造函数Person里面的属性和方法
总结
- 访问对象的一个属性,先在自身查找,如果没有,会访问对象的__proto__,沿着原型链查找,一直找到Object.prototype.proto。
- 每个函数都有prototype属性,会指向函数的原型对象。
- 所有函数的原型对象的__proto__,会指向Object.prototype。
- 原型链的尽头是Object.prototype.proto,为null。
具体可参考下方文章链接地址(写的非常详细易懂):blog.csdn.net/weixin_5650…
13. $nextTick的作用
首先我们要先明白一个道理:Vue的响应式并不是数据发生变化后,DOM立即跟着发生变化的,而是按一定的策略进行DOM更新的。
作用:nextTick,则可以在回调中获取更新后的 DOM,在下次 DOM 更新循环结束之后执行延迟回调
说白了就是:当数据更新了,在DOM更新后,自动执行该函数
什么时候用?
- Vue⽣命周期的created()钩⼦函数进⾏的DOM操作⼀定要放在Vue.nextTick()的回调函数中,原因是在created()钩⼦函数执⾏的时候,DOM 其实并未进⾏任何渲染,⽽此时进⾏DOM操作⽆异于徒劳,所以此处⼀定要将DOM操作的js代码放进Vue.nextTick()的回调函数中。
- 当项⽬中改变data函数的数据,想基于新的dom做点什么,对新DOM⼀系列的js操作都需要放进Vue.nextTick()的回调函数中
实现原理
在下一次dom更新循环结束之后执行延迟回调,nextTick使用宏任务和微任务,他会更具环境选择最适合的异步方法实现nexTick,依次
- promise (微任务)
- mutationObserver(微任务)来监视 DOM 变化
- setImmediate(宏任务)
- setTimeout(宏认为),如果以上都不行则采用setTimeout定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。
为什么需要这个api
因为vue是异步更新的,多次修改数据不会立即实现dom的更新,而是批量进行处理更新,所有就需要这个api做更新后的后续处理
14. Vue生命周期
命名变化
// Vue2 // Vue3
beforeCreate // setup()
created // setup()
beforeMount // onBeforeMount
mounted // onMounted
beforeUpdate // onBeforeUpdate
updated // onUpdated
beforeDestroy // onBeforeUnmount
destroyed // onUnmounted
组件生命周期
//渲染阶段
父 beforeCreate=>父create=>父beforeMounted=>子beforeCreate=>子create=>子beforeMounted=>
子mounted=>父Mounted=>
//更新阶段
父 beforeUpdate=>子beforeUpdate=> 子update=>父update
//销毁阶段
父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
15. react 为什么不能学习vue3 进行静态节点标记优化性能
静态节点标记优化
Vue3 的静态节点标记是在编译时标记出不会变化的节点,以减少运行时的比较:
// Vue3 模板
<template>
<div>
<h1>静态标题</h1> <!-- 静态节点 -->
<p>{{ message }}</p> <!-- 动态节点 -->
</div>
</template>
// 编译后(简化版)
const render = {
_hoisted_1: createVNode("h1", null, "静态标题"), // 静态提升
render() {
return createVNode("div", null, [
_hoisted_1, // 静态节点直接复用
createVNode("p", null, message) // 动态节点每次重新创建
])
}
}
React 不能使用静态标记的原因
- JSX 的动态性
// React 的 JSX
function App() {
return (
<div>
<h1>标题</h1>
<p>{message}</p>
</div>
)
// 每次渲染都会执行整个函数
// 无法在编译时确定哪些是静态的
}
- 函数组件的特性
function Component() {
// 每次渲染都会重新执行
const staticNode = <h1>标题</h1>;
return (
<div>
{staticNode}
<p>{message}</p>
</div>
);
}
主要原因:
- React 的函数组件每次更新都会重新执行
- JSX 是动态生成的,难以在编译时分析
- Vue 的模板是静态的,更容易在编译时分析
这就是为什么 React 不能像 Vue3 那样进行静态节点优化。
16. Vue底层实现原理
响应式系统
从初始化阶段开始,
new Observer实例化将数据改为响应式,将每个属性都赋予一个Dep(负责依赖收集的类(粉丝群)),到使用数据时:会创建一个Watcher负者刷新页面的操作,Dep 会收集当前的Watcher(加入粉丝群),用于数据变化时通知Watcher进行更新
数据变化时:数据改变需要调用数据的setter修改,Dep通知当前改变数据的所有Watcher(群发通知),Watcher 更新页面(粉丝行动)
用一个例子说明一下:
- Observer 就像经纪人
负责监控明星的动态
知道什么时候有新消息- Dep 就像粉丝群
记录谁关注了这个明星
明星有动态时通知粉丝- Watcher 就像粉丝
关注明星的动态
收到通知后更新自己的状态
// 1. 创建响应式数据
const data = {
message: 'hello'
}
// 2. Observer 将数据变成响应式
new Observer(data)
// 现在 data.message 变成了:
Object.defineProperty(data, 'message', {
get() {
// 收集使用这个数据的 Watcher
dep.depend()
return value
},
set(newVal) {
value = newVal
// 通知使用这个数据的 Watcher 更新
dep.notify()
}
})
// 3. 创建模板 Watcher
new Watcher(vm, () => {
// 这里使用了 message
document.getElementById('app').innerHTML = data.message
})
// 4. 当数据变化时
data.message = 'hi' // 触发更新
1. Observer 数据劫持 ----------- 负责将数据变为响应式
1. 判断是不是数组`isArray()`
1. 交给`observerArray()`
2. 是不是对象
1. 交给`this.walk()`
3. 判断完成后使用defineReactive ,将数据变为响应式
1. vue2 使用Object.defineProperty get set来判断是收集依赖还是更新,vue3 使用new proxy() get中的`track(target, key) // 收集依赖`,set中的` trigger(target, key) // 触发更新`
// Vue2 - Object.defineProperty
function defineReactive(obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.depend()
}
return val
},
set(newVal) {
val = newVal
dep.notify()
}
})
}
// Vue3 - Proxy
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
track(target, key) // 收集依赖
return res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
trigger(target, key) // 触发更新
return res
}
})
}
// A. Observer(数据劫持)
// 负责把数据变成响应式
class Observer {
constructor(data) {
// 1. 处理数组
if (Array.isArray(data)) {
// 数组需要特殊处理
this.observeArray(data)
}
// 2. 处理对象
else if (typeof data === 'object') {
this.walk(data)
}
}
}
2. Dep 依赖收集 ----------- 负责存储和通知依赖
1. 每个属性都有自己的`Dep`,dep.depend() 表示有正在在执行的Watcher收集为依赖,dep.notify() 通知所有收集的Wathcer ,更新
2. Dep 中存储了素有依赖这个属性的Watcher
// 2. Dep 存储了所有依赖这个属性的 Watcher
class Dep {
constructor() {
this.subs = [] // 存储 Watcher 的数组
}
}
// B. Dep(依赖收集器)
// 负责存储和通知依赖
class Dep {
constructor() {
this.subs = [] // 存储 Watcher
}
// 收集依赖
depend() {
if (Dep.target) {
this.subs.push(Dep.target)
}
}
// 通知更新
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
3. Watcher 订阅者----------- 负责页面刷新
Watcher 订阅者作为Observer和compile之间通讯桥梁
1. 实例化时执行get() 方法,get中将Dep.target 标记为当前的Watcher,然后执行数据的getter `message的getter`,之后将`Dep.target标记为null`将得到的结果返回,当数据变化时再次执行`update()`
// C. Watcher(订阅者)
// 负责更新页面
class Watcher {
constructor(vm, fn) {
this.vm = vm
this.getter = fn
this.get() // 立即执行一次
}
get() {
Dep.target = this // 标记当前 Watcher
this.getter() // 触发数据的 getter
Dep.target = null // 清除标记
}
}
模板编译 compile 使用的函数是compileToFunctions
function compileToFunctions(template) {
// 1. 解析模板
const ast = parse(template)
// 2. 优化静态节点
optimize(ast)
// 3. 生成代码
const code = generate(ast)
// 4. 创建渲染函数
return new Function(`with(this){return ${code}}`)
}
// 解析模板
function parse(template) {
// 解析 HTML 模板为 AST
// 处理指令、事件等
return ast
}
// 优化
function optimize(ast) {
// 标记静态节点
markStatic(ast)
// 标记静态根节点
markStaticRoots(ast)
}
// 生成代码
function generate(ast) {
// 将 AST 转换为渲染函数代码
return code
}
4. 解析模版
5. 优化静态节点
6. 生成代码
7. 创建渲染函数 `updateComponent `
虚拟dom的计算
// Vue2 - 基于类的 VNode
class VNode {
constructor(tag, data, children, text, elm, context) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.context = context
}
}
// Vue3 - 扁平化设计
const vnode = {
type: 'div',
props: { class: 'container' },
children: []
}
// 创建虚拟节点
function createElement(tag, data, children) {
return new VNode(tag, data, children)
}
function patch(oldVnode, vnode) {
// 比较新旧节点
// 更新 DOM
}
- VNode新旧节点的对比
- diff算法
- 批量更新
组件系统
- 组件创建
- 生命周期
- 组件通讯
17. mixin的使用有什么弊端
mixin 项目变的复杂的时候,多个组件间有重复的逻辑就会用到mixin多个组件有相同的逻辑,抽离出来
- 多个mixin可能会造成命名冲突,
- mixin和组件可能出现多对多的关系,使项目复杂度变高
- 使用场景:pc端新闻列表和详情页一样的右侧栏目,可以使用mixin进行混合
- 劣势:变量来源不明确,不利于阅读
18. vue3中使用代理的方式实现响应式性能在特殊情况下性能反而更差
1.频繁创建和销毁的小对象
// 不好的例子
function createItems() {
return new Array(1000).fill(0).map((_, index) => {
// 每个对象都会被 Proxy 包装
return reactive({
id: index,
value: Math.random()
})
})
}
// 更好的方式
function createItems() {
// 整体作为一个响应式对象
return reactive({
items: new Array(1000).fill(0).map((_, index) => ({
id: index,
value: Math.random()
}))
})
}
2. 大量数据的浅层响应
// Proxy 方式(每个属性访问都要经过代理)
const state = reactive({
list: new Array(10000).fill(0).map((_, i) => ({ id: i }))
})
// Object.defineProperty 方式(只在最外层做响应式)
const state = {
list: new Array(10000).fill(0).map((_, i) => ({ id: i }))
}
Object.defineProperty(state, 'list', {
get() { return list },
set(newVal) { list = newVal; trigger() }
})
3. 频繁读取的场景
// 不好的例子
function calculate() {
const proxy = reactive({ count: 0 })
for(let i = 0; i < 100000; i++) {
// 每次读取都要经过 Proxy
temp += proxy.count
}
}
// 更好的方式
function calculate() {
const proxy = reactive({ count: 0 })
// 解构出来,避免重复经过代理
const { count } = proxy
for(let i = 0; i < 100000; i++) {
temp += count
}
}
优化意见:
- 当只需要浅层响应式时 使用ShallowReactive({})
- 对不需要响应式数据时使用markRaw({})
19. vue的事件绑定是怎么实现的
原生事件绑定 最终是使用addEventListener 实现
直接绑定到DOM元素上,使用addEvetListener 进行监听事件,执行事件修饰符
// 1. 模板中的写法
<template>
<!-- 直接绑定 -->
<button @click="handleClick">点击</button>
<!-- 带参数 -->
<button @click="handleClick(123, $event)">点击</button>
<!-- 使用修饰符 -->
<button @click.stop.prevent="handleClick">点击</button>
</template>
// 2. 编译后的结果
{
on: {
click: function($event) {
// 处理修饰符
$event.stopPropagation()
$event.preventDefault()
// 调用方法
return handleClick.call(vm, 123, $event)
}
}
}
// 3. 最终会转换为原生 addEventListener
element.addEventListener('click', function(e) {
// 处理函数
})
vue 对事件绑定做了什么优化
- 事件代理(事件委托)
当循环添加点击事件,并不是所有的元素直接添加点击事件,而是通过判断是当前哪个元素,添加点击事件
// 1. 模板
<template>
<ul>
<li v-for="item in 1000" @click="handleClick">
{{ item }}
</li>
</ul>
</template>
// 2. Vue 会优化成
document.querySelector('ul').addEventListener('click', function(e) {
// 判断是否点击的是 li
if (e.target.tagName === 'LI') {
// 执行对应的处理函数
handleClick(e)
}
})
// 而不是给每个 li 都添加事件监听
// 避免了创建 1000 个事件监听器
- 缓存事件处理函数
定义的事件绑定时并不是每次都需要创建,而是通过缓存该事件函数
// 1. 事件处理函数的缓存
export default {
methods: {
handleClick(e) {
console.log('clicked')
}
},
render() {
// Vue 会缓存事件处理函数
const handler = this.$options.methods.handleClick.bind(this)
// 避免每次渲染都创建新的函数
return h('button', {
onClick: handler
})
}
}
// 2. 带参数的处理
<button @click="handleClick(123, $event)">
// Vue 会生成一个包装函数并缓存
const cachedHandler = e => {
return handleClick.call(vm, 123, e)
}
- 智能更新
智能更新也就是,当dom元素修改绑定元素时,并不需要重新绑定/解绑事件监听器,而是通过点击事件监听内部判断更新事件是调用哪个函数,这样就可以不同重新绑定元素的监听器了
// 1. 只在必要时更新事件监听
<button
v-if="visible"
@click="handleClick"
>
// Vue 会:
// - 只在 button 创建时添加事件监听
// - 只在 button 销毁时移除事件监听
// - visible 变化时自动处理事件绑定
// 2. 属性更新优化
<button @click="handler"> // 旧
<button @click="newHandler"> // 新
// 只更新事件处理函数的引用
// 不会重新添加/移除事件监听器
- 修饰符优化
// 1. 修饰符的编译优化
<button @click.stop.prevent="handleClick">
// 编译后的代码
function (e) {
e.stopPropagation()
e.preventDefault()
handleClick(e)
}
// 2. 常用修饰符的快速路径
<button @click.once="handleClick">
// Vue 会使用 addEventListener 的 once 选项
element.addEventListener('click', handler, {
once: true
})
- 批量更新
// 1. 事件处理中的状态更新
methods: {
handleClick() {
this.count++
this.message = 'clicked'
this.flag = true
}
}
// Vue 会将这些更新批量处理
// 只触发一次重渲染
自定义事件
通过vue的事件系统,通过$emit触发
事件系统:主要基于发布订阅模式(观察者模式)
// 1. 父组件
<template>
<!-- 监听子组件事件 -->
<child-component
@custom-event="handleCustomEvent"
@update:title="handleTitleUpdate"
/>
</template>
// 2. 子组件
<template>
<button @click="handleClick">触发事件</button>
</template>
<script>
export default {
methods: {
handleClick() {
// 触发自定义事件
this.$emit('custom-event', { data: 123 })
// 触发 update 事件
this.$emit('update:title', 'new title')
}
}
}
</script>
// 1. Vue 实例上的事件中心
class Vue {
constructor() {
// 事件中心
this._events = {}
}
// 注册事件
$on(event, fn) {
if (!this._events[event]) {
this._events[event] = []
}
this._events[event].push(fn)
return this
}
// 触发事件
$emit(event, ...args) {
const cbs = this._events[event]
if (cbs) {
cbs.forEach(cb => cb.apply(this, args))
}
return this
}
// 解绑事件
$off(event, fn) {
// 解绑逻辑
}
}
20. provide 和 inject,那他们的缺点是什么呢
- 数据来源不明确,降低代码可读性
- 默认非响应式,需要额外处理
- 容易造成命名冲突
- 维护和重构困难
- TypeScript 类型支持不友好
- 爷孙组件可以直接传值
21. Vue项目中常用的性能优化
让我系统地介绍 Vue 项目的性能优化方案:
1. 代码层面优化
// 1. v-if 和 v-show 的合理使用
<template>
<!-- 频繁切换用 v-show -->
<div v-show="isShow">频繁切换的内容</div>
<!-- 不频繁切换用 v-if -->
<div v-if="isVisible">不频繁切换的内容</div>
</template>
// 2. computed 和 watch 的合理使用
export default {
computed: {
// 数据计算优先用 computed
fullName() {
return this.firstName + ' ' + this.lastName
}
},
watch: {
// 异步或开销大的操作用 watch
searchQuery: {
handler: 'fetchData',
debounce: 300 // 防抖
}
}
}
// 3. v-for 优化
<template>
<!-- 添加 key -->
<div v-for="item in list" :key="item.id">
{{ item.name }}
</div>
<!-- 避免同时使用 v-if -->
<template v-for="item in list" :key="item.id">
<div v-if="item.show">{{ item.name }}</div>
</template>
</template>
2. 路由优化
// 1. 路由懒加载
const routes = [
{
path: '/user',
component: () => import('./views/User.vue')
}
]
// 2. 路由预加载
const routes = [
{
path: '/user',
component: () => import(
/* webpackPreload: true */
'./views/User.vue'
)
}
]
// 3. keep-alive 缓存
<template>
<keep-alive :include="['User', 'About']">
<router-view/>
</keep-alive>
</template>
3. 组件优化
// 1. 组件懒加载
components: {
'heavy-component': () => import('./HeavyComponent.vue')
}
// 2. 大数据列表优化
<template>
<virtual-scroller
:items="items"
:item-height="50"
>
<template v-slot="{ item }">
<div>{{ item.name }}</div>
</template>
</virtual-scroller>
</template>
// 3. 避免不必要的组件更新
export default {
data() {
return {
heavyData: {}
}
},
// 使用 Object.freeze 冻结不需要响应式的数据
created() {
this.heavyData = Object.freeze(heavyData)
}
}
4. Webpack 优化
// 1. 代码分割
module.exports = {
optimization: {
splitChunks: {
chunks: 'all'
}
}
}
// 2. 压缩资源
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin(),
new CssMinimizerPlugin()
]
}
}
// 3. Tree Shaking
module.exports = {
mode: 'production'
}
5. 图片优化
// 1. 懒加载
<template>
<img v-lazy="imgUrl"/>
</template>
// 2. 使用合适的图片格式
<picture>
<source type="image/webp" srcset="image.webp">
<img src="image.jpg" alt="图片">
</picture>
// 3. 小图片使用 base64
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 4 * 1024 // 4kb
}
}
}
]
}
}
6. 缓存优化
// 1. 浏览器缓存
module.exports = {
output: {
filename: '[name].[contenthash].js'
}
}
// 2. 组件缓存
<keep-alive>
<component :is="currentComponent"/>
</keep-alive>
// 3. API 缓存
const cache = new Map()
async function fetchData(key) {
if (cache.has(key)) {
return cache.get(key)
}
const data = await api.getData(key)
cache.set(key, data)
return data
}
总结优化方向:
- 代码层面
- 合理使用 v-if/v-show
- 优化 computed/watch
- 避免不必要的渲染
- 路由优化
- 懒加载
- 预加载
- 缓存
- 组件优化
- 懒加载
- 虚拟滚动
- 冻结数据
- 构建优化
- 代码分割
- 压缩资源
- Tree Shaking
- 资源优化
- 图片懒加载
- 合适的图片格式
- 资源压缩
- 缓存优化
- 浏览器缓存
- 组件缓存
- 数据缓存
这就像:
- 代码优化是调整发动机
- 路由优化是规划路线
- 资源优化是减轻负重
- 缓存优化是设置加油站
22. vue中.sync修饰符 & v-model的作用,用法以及实现原理
.sync修饰符是在vue2中使用的,在vue3中已经废除了,它用于实现双向数据绑定的语法糖,用于一般数据的数据双向绑定
- 使用
- 原理
- 通过v-bind 和 v-on:update 实现的双向绑定,并通过$emit 进行更新,源码中在template rander 阶段会编译成props绑定和 事件监听 监听
update:title
- 通过v-bind 和 v-on:update 实现的双向绑定,并通过$emit 进行更新,源码中在template rander 阶段会编译成props绑定和 事件监听 监听
// 父组件
<template>
// 1. 基本使用
<child-component :title.sync="pageTitle"/>
// 2. 等同于以下写法
<child-component
:title="pageTitle"
@update:title="pageTitle = $event"
/>
// 3. 多个属性使用
<child-component
:title.sync="pageTitle"
:content.sync="pageContent"
/>
</template>
// 子组件
export default {
props: ['title'],
methods: {
changeTitle() {
// 触发更新
this.$emit('update:title', '新标题')
}
}
}
--------------------------------------// 1. 编译阶段
// template 会被编译成 render 函数
// 2. .sync 会被展开为两部分
// a. props 绑定
{
props: {
title: {
type: String,
required: true
}
}
}
// b. 事件监听
{
on: {
'update:title': function(value) {
this.pageTitle = value
}
}
}
v-model 在vue2中一个组件只能有一个,vue3中能有多个,它用于实现数据双向绑定,一般用于表单元素
- 默认事件为
this.$emit('update:modelValue',val)vue3,this.$emit('input',val)是vue2 - 原理和.sync 类似
vue2 和 vue3 中的v-model的区别
让我详细对比 Vue2 和 Vue3 中 v-model 的变化:
1. 默认值的变化
// Vue2
<component v-model="value">
// 等同于
<component
:value="value"
@input="value = $event"
>
// Vue3
<component v-model="value">
// 等同于
<component
:modelValue="value"
@update:modelValue="value = $event"
>
2. 自定义 v-model 的变化
// Vue2
export default {
model: {
prop: 'title',
event: 'change'
},
props: {
title: String
}
}
// Vue3
// 不再需要 model 选项
export default {
props: {
modelValue: String
},
emits: ['update:modelValue']
}
3. 多个 v-model 支持
// Vue2
// ❌ 不支持多个 v-model
<component
v-model="value1"
:value2="value2"
@input2="value2 = $event"
>
// Vue3
// ✅ 支持多个 v-model
<component
v-model="value1"
v-model:title="value2"
v-model:content="value3"
>
4. 修饰符处理
// Vue2
export default {
props: {
value: String,
modelModifiers: {
default: () => ({})
}
}
}
// Vue3
export default {
props: {
modelValue: String,
modelModifiers: {
default: () => ({})
},
// 支持具名修饰符
titleModifiers: {
default: () => ({})
}
},
emits: ['update:modelValue', 'update:title']
}
5. 组件实现示例
// Vue2
<template>
<input
:value="value"
@input="$emit('input', $event.target.value)"
>
</template>
export default {
props: ['value']
}
// Vue3
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
</template>
export default {
props: ['modelValue'],
emits: ['update:modelValue']
}
主要变化总结:
- 命名规范的变化
- Vue2: value/input
- Vue3: modelValue/update:modelValue
- 配置方式的变化
- Vue2: 使用 model 选项
- Vue3: 直接使用 props 和 emits
- 功能的扩展
- 支持多个 v-model
- 更灵活的修饰符处理
- 更清晰的命名约定
- 使用方式的变化
- 更统一的命名规范
- 更直观的实现方式
- 更好的类型支持
这就像:
- Vue2 是固定的双向绑定方案
- Vue3 是更灵活的配置系统
- 提供了更多的自定义空间
vue-router
linkActiveClass 和 linkExactActiveClass 的区别和作用
让我用一个简单的例子来说明 linkActiveClass 和 linkExactActiveClass 的区别:
1. 基本区别
// 假设我们有这样的路由结构:
/user // 用户中心
/user/profile // 个人资料
/user/settings // 设置
// 当我们访问 /user/profile 时:
<router-link to="/user">用户中心</router-link>
// - 会添加 linkActiveClass(因为 /user/profile 包含 /user)
// - 不会添加 linkExactActiveClass(因为不完全匹配)
<router-link to="/user/profile">个人资料</router-link>
// - 会添加 linkActiveClass
// - 也会添加 linkExactActiveClass(因为完全匹配)
const router = createRouter({
history: createWebHistory(),
// 配置激活类名
linkActiveClass: 'active', // 部分匹配时的类名
linkExactActiveClass: 'exact-active', // 精确匹配时的类名
routes: [...]
})
简单来说:
linkActiveClass:模糊匹配,只要当前路径包含这个链接的路径就会激活linkExactActiveClass:精确匹配,只有当前路径完全相同时才会激活
就像:
linkActiveClass是判断是否在同一个部门linkExactActiveClass是判断是否在同一个具体位置
vue-router 几种钩子函数
- 全局守卫
router.beforeEach((to,from,next)=>{ next()//登录鉴权 })全局前卫守卫router.beforeResolve((to,from,next)=>{ next() })全局解析守卫,每个路由都会触发router.afterEach((to,from,next)=>{修改标题,统计分析})全局后置守卫
- 路由独享
const routes = [
{
path: '/users',
component: UserList,
beforeEnter: (to, from, next) => {
// 只在进入这个路由时触发
if (hasPermission) {
next()
} else {
next('/403')
}
}
}
]
- 组件内守卫
- 进入组件前 beforeRouterEnter 可以通过next 的回调函数调用组件实例
- 路由更新时 beforeRouterUpdate 也就是
user/1变为/user/2 - 离开组件前 beforeRouterLeave 可以通过访问this组件实例
export default {
// 1. 进入组件前
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被验证前调用
// 不能获取组件实例 this
fetchData().then(data => {
next(vm => {
// 通过 vm 访问组件实例
})
})
},
// 2. 路由更新时
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是组件被复用时调用
// 可以访问组件实例 this
next()
},
// 3. 离开组件前
beforeRouteLeave(to, from, next) {
// 导航离开该组件时调用
// 可以访问组件实例 this
if (this.hasUnsavedChanges) {
if (confirm('确定要离开吗?')) {
next()
} else {
next(false)
}
} else {
next()
}
}
}
vuex
vuex实现的是一个单向数据流,在全局拥有一个State存放数据,当组件要更改State中的数据时,必须通过mutation进行,actions无法直接修改数据可以通过mutation 间接修改数据
- state: 存放数据源的地方
- getters: 计算数据(相当于state的计算属性),对state 进行进一步的加工,导出
- mutation:同步,唯一能修改state数据源的地方,通过
commit('mutation函数名称')来触发 - actions:操作行为处理模块,由组件中的
**<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">$store.dispatch('action 名称', data1)</font>**来触发。然后由commit()来触发mutation的调用 , 间接更新 state。可以定义异步函数,并在回调中提交mutation,就相当于异步更新了state中的字段 - modules:模块化
持久化:
通过vuex-persist,将需要的数据单独进行持久化,再将组件返回的内容通过插件的形式存入Store中
import VuexPersistence from 'vuex-persist';
const vuexLocal = new VuexPersistence({
key: 'key_value_local', // 新的键名
storage: window.localStorage,
expiration: 15 * 24 * 60 * 60 * 1000, // 过期时间,默认30天
reducer: state => {
return {
clickLike:state.clickLike,
themes:state.themes,
locale:state.locale,
// loadingHome:state.loadingHome,
}
}
})
export default new Vuex.Store({
state:{
num:1
},
mutations:{
DAA_NUM:function(state,data){
state.num+=data
},
SUB_NUM:function(state,data){
state.num--
}
},
active:{
add({commit,state,dispatch},data){
commit('DAA_NUM',data)
dispatch('subtract',data)
}
subtract({commit,state},data){
const num = state.num
commit('SUB_NUM',data)
}
}
})
Pinia
- state:数据存放地
- actions:异步 调用state getters都可以通过this直接调用和修改
- getters: 计算数据(相当于state的计算属性)
1. 创建 Store
// stores/counter.js
import { defineStore } from 'pinia'
// 选项式写法
export const useCounterStore = defineStore('counter', {
// 状态
state: () => ({
count: 0,
name: 'Eduardo',
todos: []
}),
// 计算属性
getters: {
doubleCount: (state) => state.count * 2,
// 使用 this 访问其他 getter
doubleCountPlusOne() {
return this.doubleCount + 1
}
},
// 方法
actions: {
increment() {
this.count++
},
async fetchTodos() {
const todos = await api.getTodos()
this.todos = todos
}
}
})
// 组合式写法
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
2. 在组件中使用
<template>
<div>
<p>Count: {{ counter.count }}</p>
<p>Double: {{ counter.doubleCount }}</p>
<button @click="counter.increment">+</button>
<button @click="increment">+</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
// 获取 store 实例
const counter = useCounterStore()
// 解构(使用 storeToRefs 保持响应性)
const { count, doubleCount } = storeToRefs(counter)
const { increment } = counter
// 修改状态
function updateCount() {
// 1. 直接修改
counter.count++
// 2. $patch 方法
counter.$patch({
count: counter.count + 1,
name: 'John'
})
// 3. $patch 函数
counter.$patch((state) => {
state.count++
state.todos.push({ text: 'new todo' })
})
}
</script>
defineProperty 无法监听数组索引和长度的变化,无法监听对象新增属性,需要递归遍历所有对象,初始化时会全部收集完成
proxy 正好完美的避免了这些问题:不需要递归变量对象,在使用的时候才会收集依赖,懒收集