Vue
对Vue的理解
Vue 是一个构建数据驱动的渐进性框架,目标是通过API实现 响应数据绑定 和 视图更新
Vue的两大核心
- 数据驱动
- 组件系统
Vue生命周期
Vue实例从创建到销毁的过程
生命周期的作用
生命周期中有多个事件钩子,能让开发者在控制整个vue实例的过程时更容易形成良好的逻辑判断
Vue 2 的8大生命周期钩子函数:
- beforeCreate
- created
- beforeMount
- mounted
- beforeUpdate
- updated
- beforeDestroy
- destroyed
Vue3的6大生命周期钩子函数:
beforeMount、onMounted、beforeUpdate、onUpdated、beforeUnmount、onUnmounted
用setup代替了beforeCreate、Created;destory改名成了unmount
created
和mounted
created | mounted | |
---|---|---|
调用时机 | 模板渲染成 HTML 前 | 模板渲染成 HTML 后 |
常见作用 | 初始化属性值,再渲染成视图 | 操作DOM节点 |
合适的调用接口时期 | 组件渲染必须的,且请求耗时较短 | 组件渲染不必要的,或者请求耗时较长 |
beforeDestroy()
当组件被销毁时(例如调用了 vm.$destroy()
方法),Vue.js 将会依次触发 beforeDestroy()
生命周期钩子函数
- 清除定时器和监听器:在组件销毁前清除定时器和监听器,避免内存泄漏和不必要的资源占用。
clearInterval(this.timer); // 清除定时器
this.timer = null; // 将定时器变量置为 null,释放内存
this.$off('eventName'); // 清除特定事件的监听器
this.$off(); // 清除所有事件的监听器
-
取消订阅事件:如果组件订阅了外部事件,需要在组件销毁前取消订阅,以避免在组件已销毁后继续接收事件。
-
清理非 Vue 实例的资源:例如关闭 WebSocket 连接、清理 Web Worker、释放资源等。
-
取消异步任务:如果组件中存在未完成的异步任务,需要在销毁前取消这些任务,以确保不会产生不必要的副作用。
-
其他清理工作:例如重置状态、清空缓存、解除绑定等。
总的来说,beforeDestroy()
生命周期钩子函数是在组件即将销毁时执行的最后一个钩子函数,用于执行一些清理和准备工作,确保组件销毁时的状态和资源得到正确处理,以提高页面性能并避免潜在的问题。
v-model
用于表单数据的双向绑定,其实是语法糖,背后做了两个操作:
- v-bind绑定value属性
- v-on绑定input事件
Vue响应式(双向数据)原理
通过 数据劫持 结合 发布订阅模式 的方式来实现
- Vue2:通过
Object.defineProperty()
来劫持各个属性的setter
、getter
,在数据变动时发布消息给订阅者,触发相应的监听回调 - Vue3:数据劫持的方式更改为 ES6 中的
Proxy 对象
,更加灵活且性能更好 - Object.defineProperty()只能监听属性的读写,而Proxy可以监听对象的所有操作,包括删除属性、枚举属性等。
虚拟DOM
虚拟DOM相对于浏览器所渲染出来的真实 DOM,在 React,Vue 等技术出现之前, 改变页面展示内容只能遍历查询 DOM 树找到需要修改的 DOM,然后修改样式行为或者结构,来更新UI,但是这种方式相当消耗计算资源,每次查询DOM几乎都需要遍历整棵DOM树,因此建立一个与真实DOM树对应的虚拟DOM对象(JavaScript对象),以对象嵌套的方式来表示DOM树,那么每次DOM的更改就变成了对象属性的更改,这样一来就能通过diff算法查找变化要比查询真实的DOM树的性能开销小
Vue渲染优先级
Vue通过内部函数_init
处理,优先级:render()>template>el选项
- render()
Vue.component('my-component', {
render: function (createElement) {
return createElement('h1', 'Hello Vue!')
}
})
- template
Vue.component('my-component', {
template: '<h1>Hello Vue!</h1>'
})
- el选项
new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
<template>
渲染过程
key的作用
为了高效的更新虚拟DOM
给虚拟DOM的每个节点 VNode 添加唯一 id,让其可以依靠 key,更快速准确地拿到 oldVnode 中对应的 VNode
【设置key不推荐使用index或者随机数,因为增删子项的时候损耗性能较大】
v-if
和v-show
- v-if在DOM层面决定元素是否存在,会引起重排重绘,变成true时触发前4个生命周期,变成false时触发beforeDestroy、destroyed
- v-show在CSS层面决定是否将元素渲染出来(实际上该元素一直存在),不触发生命周期, 属于display 的切换(block/none)
v-if
和v-for
优先级
v-if | v-for | |
---|---|---|
Vue 2 | 优先 | |
Vue 3 | 优先 |
解决方案 :
- 父元素使用v-if,子元素使用v-for
- 使用计算属性
computed
computed() {
list() {
return [1, 2, 3].filter(item => item !== 2);
}
}
修饰符
.stop:阻止事件冒泡
.trim:去空格
.sync:简化子组件向父组件传值,写在父组件里面,监听子组件的传值
data
是个函数并且返回一个对象 / data
为什么要用return
因为一个组件可能会多处调用,每次调用会执行data函数并返回新的数据对象,因此这样可以避免多处调用之间的数据污染
Vue 如何监听键盘事件
- @keyup.方法
- addEventListener
computed
-
缓冲变化(Batching Changes):
当多个依赖项同时变化时,Vue 会将这些变化缓冲到同一个事件循环里,然后在下一个事件循环才进行计算一次,从而避免重复计算。 -
惰性求值(Lazy Evaluation):
即使定义了computed
属性,如果没有任何地方引用它(例如在模板、方法或其他computed
属性中),那么它的计算函数不会执行,因为 Vue 的响应式系统只会在属性值被实际需要时计算它
组件
组件通信
父子 | 祖孙/隔代 | 兄弟 | |
---|---|---|---|
props / $emit | √ | ||
ref与 $parent / $children | √ | ||
$attrs / $listeners | √ | ||
provide / inject | √ | ||
EventBus($emit / $on) | √ | √ | √ |
Vuex、Pinia | √ | √ | √ |
localStorage/Cookie等都可以
- 父子的传参及方法调用:props / $emit、$parent / $children、$ref
- 祖孙的传参:provide / inject API、$attrs / $listeners
- 兄弟的传参:Vuex、事件总线bus.js(Vue2)、mitt插件(Vue3)
- 路由的传参:query、params
使用$ref的弊端
因为$ref是操作真实DOM,所以组件嵌套过深/频繁调用子组件会造成性能问题
-
组件嵌套过深: 如果在嵌套层次很深的组件树中频繁使用
$ref
来访问子组件的真实 DOM,可能会导致性能下降。因为每次访问都需要遍历组件树,直到找到目标组件,这会增加额外的计算开销。 -
频繁调用子组件: 如果在父组件中频繁地使用
$ref
来操作子组件,特别是在循环中或者在大量数据的列表中使用,可能会造成性能问题。因为每次操作都会涉及到真实 DOM 的更新和重绘,这会消耗大量的计算资源。
另外,直接操作真实 DOM 也会破坏了 Vue 的响应式机制,因为 Vue 的响应式是基于虚拟 DOM 的,直接操作真实 DOM 可能会导致视图和数据的不一致。
父子组件的生命周期顺序
父组件created -> 子组件created -> 子组件mounted -> 父组件mounted
为何如此设计:父组件需要在created拿到初始化数据,通过props传递给子组件;父组件可能有多个子组件,所以需要等待所有子组件mounted
同理,更新 / 销毁也是如此:
父组件beforeUpdate / beforeDestroy-> 子组件beforeUpdate / beforeDestroy-> 子组件updated / destroyed-> 父组件updated / destroyed
动态组件
使用<component :is="判断条件"></component>
删除数组用 delete 和 Vue.delete 的区别
- delete:只是被删除数组成员变为 empty / undefined,其他元素键值不变
- Vue.delete:直接删了数组成员,并改变了数组的键值
(对象是响应式的,确保删除能触发更新视图,这个方法主要用于避开 Vue 不能检测到属性被删除的限制)
计算属性computed
和属性检测watch
cpmputed:当且仅当计算属性依赖的 data 改变时才会自动计算
computed | watch | |
---|---|---|
首次运行 | √ | × (可以添加{immediate:true}实现首次运行) |
默认依赖 | 深度(推荐使用) | 浅度 |
调用时 | 在模板渲染 | 只需修改元数据 |
合适性 | 筛选、不可异步(因为有return) | 开销较大、异步操作 |
特征 | 根据页面变化而变化(用于计算) | 监听页面状态而变化(用于监听) |
场景 | 购物车结算功能 | 商品是否选中 |
Vue组件封装过程
// 创建组件构造器
var MyComponent = Vue.extend({
data() {
return {
message: 'Hello, Vue!'
};
},
template: '<div>{{ message }}</div>'
});
// 注册组件
Vue.component('my-component', MyComponent);
// 创建 Vue 实例并挂载到页面上
new Vue({
el: '#app'
});
v-for
后使用this.$refs
报错domundefined
组件初始化到第一次渲染完成的mounted周期里,只是渲染了组件模板的静态数据,并没有初始化动态绑定的dom,所以在mounted周期里面操作获取不到dom
解决方法:
- 把this.$nextTick放在获取到v-for绑定的数据并赋值之后,也就是触发响应式更新之后再进行操作
mounted() {
this.$nextTick(() => {
// 在 DOM 渲染完成后执行操作
console.log(this.$refs);
});
}
- 把操作dom的操作放到updated生命周期里,但是这样每次更新视图都会触发该操作
updated() {
// 在组件数据更新后执行操作
console.log(this.$refs);
}
this.$nextTick()
对DOM的操作最好都放在这里, 因为像created()生命周期时执行的话, 会报错domundefined, 该DOM还未渲染
process.nextTick()
和this.$nextTick()
process.$nextTick()
是在所有微任务前执行,是 Node.js 中的一个机制
this.$nextTick()
是在所有微任务后执行,是 Vue.js 中的方法,它用于等待 Vue 组件的 DOM 更新完成后再执行回调。它通常会在 Vue 组件中使用,确保在 DOM 更新后执行操作
scoped CSS
的实现原理(Vue2)
- 通过PostCSS给所有dom都添加了唯一的动态属性
- 通过PostCSS也给css选择器额外添加对应的属性选择器,来选择组件中的dom
module CSS
的实现原理(Vue 3)
-
局部作用域: 在 Vue 3 中,通过在
<style>
标签中添加module
属性,可以将样式声明为局部作用域的。这意味着这些样式只会应用于当前组件的 DOM 元素,不会影响到其他组件或全局样式。 -
自动生成唯一标识符: 在编译过程中,Vue 3 会自动生成一个唯一标识符(通常是哈希值),并将其作为 CSS 类名的一部分。这个唯一标识符会与当前组件的样式关联起来,确保样式只应用于当前组件的 DOM 元素。
-
样式模块化: Vue 3 的
module
样式允许在样式表中使用类似于 CSS Modules 的语法,例如使用:global
来声明全局样式,或者使用:local
来声明局部样式。这样可以更加灵活地管理样式,并且可以避免命名冲突和样式污染。 -
动态导出类名: 在组件的 JavaScript/TypeScript 代码中,可以通过导入样式模块并使用其导出的类名来应用样式。这样可以确保类名的一致性,并且可以在模板中动态地应用样式。
插槽
- 默认插槽(Default Slot): 这是最基本的插槽类型。当组件内部没有具名插槽时,所有没有被包裹在具名插槽中的内容都会被放置在默认插槽中。
<template>
<div>
<slot></slot>
</div>
</template>
- 具名插槽(Named Slots): 通过给插槽设置名称,可以在父组件中将内容插入到特定的插槽中。具名插槽使得父组件可以在一个单独的插槽中插入不同的内容。
<template>
<div>
<slot name="header"></slot>
<slot></slot>
<slot name="footer"></slot>
</div>
</template>
在父组件中使用:
<template>
<MyComponent>
<template v-slot:header>
<h1>This is header</h1>
</template>
<p>This is main content</p>
<template v-slot:footer>
<p>This is footer</p>
</template>
</MyComponent>
</template>
- 作用域插槽(Scoped Slots): 作用域插槽允许父组件向子组件传递数据。子组件可以在插槽中接收来自父组件的数据,并根据这些数据来渲染内容。
<template>
<div>
<slot :user="user"></slot>
</div>
</template>
在父组件中使用:
<template>
<MyComponent>
<template v-slot="{ user }">
<p>{{ user.name }}</p>
<p>{{ user.age }}</p>
</template>
</MyComponent>
</template>
这样,user
对象就会从父组件传递到子组件,并在子组件的作用域中可用。
Vue3
ref():监听简单数据类型 reactive():监听复杂数据类型
ref():适用任何类型,会返回一个包裹对象,需要使用.value才能拿到里面的值【更通用常见】
reactive():只适用对象(包括数组、Map、Set)
reactive响应的注意点
import { reactive, ref } from 'vue';
// 这个时候你就会发现当前数据不会具有响应:原因是因为当前的 proxy对象 已经被替换为 原始对象
let play = reactive({ a: 0 })
play = { a: 1 }
console.log(play); //{a: 1}
// 解决1 :在重新替换的时候给新的值也加上 reactive()
let play = reactive({ a: 0 })
play = reactive({ a: 1 })
console.log(play); //Proxy {a: 1}
// 解决2 :使用 ref 代替 reactive
// 原因:ref 的 .value 属性也是响应式的。同时,当值为对象类型时,会用 reactive() 自动转换它的 .value
let play = ref({ a: 0 })
play.value = { a: 1 }
console.log(play);
// 以下的解构也不会具有响应式
let n = play.a
n++ //不影响原始的 play
let { a } = play
a++ // 不会影响原始的 play
Vue优化
- 组件拆分
- 使用keep-alive
- 优化渲染性能
- 合理使用
v-if
、v-show
- 使用
v-for
避免手动操作DOM - 合理使用computed和缓存重复计算
- 状态管理优化
- 使用Vuex管理状态,避免组件间传递大量props
- 合理使用Getter和Mutation,保持状态管理的可维护性
- 图片懒加载
- 路由懒加载
- 使用骨架屏
- 使用SSR(服务器端渲染)
localstorage vuex和pinia的异同
vue3为什么要出hooks 跟普通的封装函数有什么区别
-
状态管理: Vue 3 的 Hooks 具有更强大的状态管理能力,可以直接使用响应式状态,无需手动进行状态管理。而普通的封装函数可能需要通过参数传递状态或者通过闭包来管理状态。
-
生命周期钩子: 在 Vue 3 中,Hooks 可以模拟类组件中的生命周期钩子,比如
onMounted
,onUpdated
,onUnmounted
等,使得在函数式组件中可以处理生命周期相关的逻辑。 -
逻辑复用: Hooks 的设计目的是为了更好地复用逻辑。使用 Hooks 可以将组件中的状态和副作用逻辑提取到可复用的函数中,然后在组件中进行调用,而普通的封装函数可能只能实现逻辑的封装,但不够灵活和易用。
-
性能优化: Vue 3 的 Hooks 在内部做了一些性能优化,比如利用了 JavaScript 的闭包特性来保持状态的独立性,从而避免了一些潜在的性能问题。
// 自定义hook
import { ref, onMounted, onUnmounted } from 'vue';
export function useCounter(initialValue = 0) {
const count = ref(initialValue);
function increment() {
count.value++;
}
function decrement() {
count.value--;
}
// 在组件挂载时增加计数器值
onMounted(() => {
console.log('Component mounted');
});
// 在组件卸载时打印计数器值
onUnmounted(() => {
console.log('Component unmounted. Count:', count.value);
});
return {
count,
increment,
decrement
};
}
<template>
<div>
<h2>Counter: {{ counter.count }}</h2>
<button @click="counter.increment">Increment</button>
<button @click="counter.decrement">Decrement</button>
</div>
</template>
<script>
import { useCounter } from './useCounter';
export default {
setup() {
const counter = useCounter();
return {
counter
};
}
};
</script>
Vue CLI
src目录每个文件夹和文件的用法
- assets文件夹是放静态资源;
- components是放组件;
- router是定义路由相关的配置;
- app.vue是一个应用主组件;
- main.js是入口文件
static和assets的区别
原理:webpack 如何处理静态资源
static | assets | |
---|---|---|
内容 | 类库 | 项目资源 |
资源 | 直接引用 | 被 webpack 打包 |
(static放别人家的资源,assets放自己家的资源)
引入第三方库
- 绝对路径直接引入
- 在 webpack 中配置
alias
[ˈeɪliəs] - 在 webpack 中配置
plugins
[ˈplu:genz]
Vue-Router
router、routes、route
router
: 路由对象
routes
: 路由配置项
route
: 当前路由信息
hash模式和history模式
hash模式 | history模式 | |
---|---|---|
浏览器支持版本 | 所有 | 支持 HTML5 新 API 的现代浏览器 |
URL中是否带 # | √ | × |
刷新 / # 后面的hash变化 | 仅根据hash重新加载 | 重新往服务器发起请求 |
适用场景 | 兼容性要求较高 、不需要考虑 SEO 和页面刷新时的状态保持 | 对SEO 有较高要求、接受需要服务器端额外配置 |
路由实现原理
- 利用URL中的hash(“#”)
- 利用History interface在HTML5中新增的方法
Hash(#) 模式的底层原理
- URL 中的 Hash(#):
- Hash是URL的一部分,位于
#
符号之后。这部分的变化不会影响服务器的请求,因为#
后面的内容是不会被发送到服务器的。这为前端路由提供了便利,可以在不刷新页面的情况下改变视图。
- Hash是URL的一部分,位于
- 监听 Hash 变化:
- 浏览器提供了
hashchange
事件,这个事件在URL的hash部分发生变化时被触发。通过监听这个事件,前端应用可以知道用户导航到了哪一个路由。
- 浏览器提供了
- 事件监听器:
- 在Vue-Router中,通过
window.addEventListener('hashchange', callback)
来添加事件监听器。当用户点击链接或者通过JavaScript直接修改location.hash时,hashchange
事件会被触发,回调函数会执行。
- 在Vue-Router中,通过
- 路由映射:
- Vue-Router在初始化时,会读取定义的路由配置,这些配置将hash值映射到组件。当hash值发生变化时,Vue-Router通过匹配算法找到对应的路由配置,然后渲染相应的组件。
- 前进/后退按钮:
- 浏览器的历史记录管理包括了hash的变化。当用户点击前进或后退按钮时,浏览器会改变当前的hash值,从而触发
hashchange
事件,Vue-Router根据事件的回调来更新视图。
- 浏览器的历史记录管理包括了hash的变化。当用户点击前进或后退按钮时,浏览器会改变当前的hash值,从而触发
示例代码
以下是一个简单的hash模式路由监听的示例:
// 定义路由变化时的回调函数
function onHashChange() {
const hash = window.location.hash;
// 根据hash值切换视图
switch (hash) {
case '#/home':
// 渲染首页
break;
case '#/about':
// 渲染关于页面
break;
// 其他路由...
default:
// 默认路由
break;
}
}
// 监听hash变化
window.addEventListener('hashchange', onHashChange);
// 初始化时也需要调用一次,以确保加载正确的视图
onHashChange();
在实际的Vue-Router中,这个过程更加复杂,它涉及到了路由守卫、异步组件加载、路由懒加载等高级功能,但基本原理与上述描述相符。
传参
- name 传参
- URL 传参
<router-link>
的to传参- 用 path 匹配路由,通过 query 传参
二级路由 / 嵌套路由
使用路由导航<router-link>
和路由容器<router-view>
,配置children
路由守卫触发流程
- 不同组件(A组件跳转到B组件)
keep-alive
keep-alive用于保存组件的渲染状态
不希望组件被重新渲染影响用户体验和降低性能,而是希望组件可以缓存下来,维持当前的状态,这时候就可以用到keep-alive
组件
(通常搭配生命周期activated
使用)
keep-alive是一个抽象组件:它自身不会渲染一个DOM元素,也不会出现在父组件链中;使用keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
简单来说就是:应用遇到多个组件共享状态时,使用vuex。
vuex的流程
vuex属性
mapAction:State , Getter , Mutation , Action , Module
你对 Vuex 属性的理解很到位。下面我会稍微扩展一下:
-
state(状态):Vuex 中的 state 是存储应用程序中的所有变量和状态的地方。它类似于组件中的 data 属性,但是它是全局的,可以在应用程序的任何地方访问。
-
getter(获取器):Getter 允许组件从 store 中获取数据,并对数据进行一些处理后返回。它可以看作是 store 中的计算属性,用于派生出一些状态。
-
mutation(变更):Mutation 是 Vuex 中唯一允许修改 state 的地方。它是同步的函数,用于修改 state 中的数据。由于 mutation 必须是同步的,因此它们应该尽量保持简单和高效。
-
action(动作):Action 可以包含任意异步操作,例如向服务器发送请求。它类似于 mutation,但是可以包含任意异步操作。Action 是通过提交 mutation 来间接修改 state 的。
-
modules(模块):Modules 允许将 store 分割成模块化的结构,每个模块都有自己的 state、getter、mutation 和 action。这样可以使得应用程序的结构更清晰,方便管理和维护。Modules 也可以嵌套,形成复杂的层次结构。
页面刷新后store的state不存在
可以安装插件让其实现持久化
ajax 写在 methods 中还是 actions 中
- 仅在请求组件内使用:写在组件的 methods 中
- 在其他组件复用:写在 vuex 的 actions 中
(包装成 promise 返回,在调用处用 async await 处理返回的数据)
Vue项目
首屏加载优化
你列出了一些优化首屏加载速度的方法,下面我会对每个方法进行简要的解释:
-
服务器渲染(SSR):
- 通过在服务器端生成 HTML 再发送到前端进行解析,可以减少客户端渲染的时间,提高页面加载速度和 SEO 效果。
-
骨架屏(Skeleton Screen):
- 骨架屏是一种页面加载优化的技术,它会在页面加载时先展示页面的大致框架,然后再逐步加载细节内容,从而提升用户的加载体验。
-
图片懒加载:
- 图片懒加载是一种延迟加载图片的技术,当图片进入可视区域时再进行加载,可以减少首屏加载时的网络请求和页面渲染时间。
-
Vue-Router 路由懒加载:
- 使用 Vue-Router 的路由懒加载功能,可以将页面组件按需加载,只在需要时才加载相应的组件,从而减少首屏加载时的资源请求和时间。
-
使用异步组件动态导入:
- Vue.js 支持使用异步组件来实现动态导入,可以将组件按需加载,只在需要时才加载组件的代码,从而提高页面加载速度。
-
分包:
- 将页面中的资源拆分为多个包,按需加载,可以减少首屏加载时的资源大小和加载时间,提高页面的加载速度。
通过使用以上方法,可以有效地优化页面的首屏加载速度,提升用户的加载体验和页面性能。
Webpack
构建流程
Webpack和Vite
打包的方式以及首次加载时间会有一些不同
Webpack: (全打包)
- Webpack是一个传统的打包工具,它会将所有模块打包成一个或多个 bundle 文件,这些文件通常包含了整个应用程序的代码和依赖。这意味着首次加载时需要下载和解析所有的代码,因此首次加载时间可能会较长。
Vite: (模块打包)
- Vite采用了一种不同的开发模式,称为ES模块(ESM)原生开发。在开发模式下,Vite不会将所有代码打包成一个大的 bundle 文件,而是会保持模块的原始状态,每个模块都是一个独立的文件。这样,在首次加载时,浏览器只需要请求并加载当前页面所需的模块,而不需要下载整个应用程序的代码。这通常会导致更快的首次加载时间。
不过,在生产环境下,Vite也可以进行打包,将所有模块打包成一个或多个 bundle 文件,以便于部署。这时,首次加载时间可能会比开发模式长一些,因为需要下载和加载更多的代码
工作遇到的问题
组件循环依赖问题
- 放在合适的生命周期如created
- 使用组件懒加载
// ComponentA.vue
export default {
created() {
import('./ComponentB.vue').then(ComponentB => {
// 处理 ComponentB
});
}
}
- 把循环依赖的组件设置成全局
// main.js
import Vue from 'vue';
import ComponentA from './ComponentA.vue';
import ComponentB from './ComponentB.vue';
Vue.component('ComponentA', ComponentA);
Vue.component('ComponentB', ComponentB);