前言
本文从 vue 核心特性、原理和使用三个方面整理了相关知识点,并不断更新补充。
Vue 核心特性
数据驱动(MVVM)
MVVM
表示的是 Model-View-ViewModel
Model:模型层,负责处理业务逻辑以及和服务器端进行交互;View:视图层:负责将数据模型转化为UI展示出来,可以简单的理解为 HTML 页面;ViewModel:视图模型层,用来连接 Model 和 View,是Model 和 View 之间的通信桥梁。
组件化
组件化就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在Vue
中每一个.vue
文件都可以视为一个组件。
组件化可以降低整个系统的耦合度,在保持接口不变的情况下,替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现;组件化调试方便,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,以逻辑会比分析整个系统要简单;提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级。
指令
指令是带有 v- 前缀的特殊属性作用,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM,Vue 自带的常用指令有 v-if
、v-for
、v-bind
、v-on
、v-model
等,开发者也可以通过 directives
选项创建自定义指令,或者在 <script setup>
中以 v
开头的驼峰式命名的变量用作一个自定义指令。
javascript
export default {
setup() { /*...*/ },
directives: {
// 在模板中启用 v-focus
focus: { /* ... */ } }
}
<script setup>
// 在模板中启用 v-focus
const vFocus = {
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus />
</template>
自定义指令的常用场景有:通过节流来防止表单重复提交、图片懒加载、权限校验等。
Vue 原理
Vue 实例的挂载过程
new Vue
的时候调用会调用 _init
函数,定义 $set
、$get
、$delete
、$watch
等方法,定义 $on
、$off
、$emit
、$off
等事件,定义 _update
、$forceUpdate
、$destroy
生命周期;
调用 $mount
进行页面的挂载,挂载的时候主要是通过 mountComponent
方法;
定义 updateComponent
更新函数;
执行render
生成虚拟DOM
;
_update
调用 patch
,将 vnode
转换为真实 DOM
,并且更新到页面中。
data 属性是函数和是对象的区别
vue
实例的时候定义 data
属性既可以是一个对象,也可以是一个函数。组件中定义 data
属性,只能是一个函数,如果为组件 data
直接定义为一个对象则会得到警告信息。因为对象的内存地址是共用的,函数返回的对象内存地址并不相同,vue
组件可能会有很多个实例,采用函数返回一个全新data
形式,可以使每个实例对象的数据不会受到其他实例对象数据的污染。
双向绑定/响应式原理
1. vue2.x
vue2.x
响应式采用的是软件设计模式中的观察者模式,创建 observer 类观察数据,构造函数中对对象和数组类型分别处理,对象数据通过 Object 构造器上的 api defineProperty 实现,defineProperty 可以在对象上定义/修改一个属性,通过此方法对对象进行监听,通过 get/set 劫持读写,在修改的同时通知视图更新,defineProperty api 有缺点,对象层级深时需要递归到底监听,一次性计算量大,无法监听新增/删除属性;对数组类型,通过 Object.create 方法创建新对象使其原型指向 Array 的 prototype(扩展新属性不会影响数组原型),重写 push 等方法,在更新视图的同时执行真正数组原型上的方法。
2. vue3.x
vue3.x
使用了两个响应式函数,ref()
和 reactive()
。ref()
可以生成值类型响应式,reactive()
生成引用类型响应式。ref 中定义一个对 value 属性做 getter 和 setter 劫持的对象并返回,get 部分就是执行 track 函数做依赖收集然后返回它的值,set 部分就是设置新值并且执行 trigger 函数派发通知;reactive 中 使用了 new Porxy(target,config)
传入目标对象和代理配置,返回代理对象,与 defineProperty
相比,深度监听是在 get 里,不需要递归到底,只在访问时递归处理。
nextTick 原理
$nextTick
的原理是利用JS 事件循环机制,当监听到数据发生变化的时候不会立即去更新DOM,而是开启一个任务队列,并缓存在同一事件循环中发生的所有数据变更,将多次数据更新合并成一次,减少操作DOM的次数从而减少性能的消耗。$nextTick
会在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,可以获取更新后的 DOM。$nextTick()
会返回一个 Promise
对象,可以使用 async/await
完成相同作用的事情。
Mixin
本质其实就是一个 js 对象,它可以包含我们组件中任意功能选项,如data
、components
、methods
、created
、computed
等等,需要注意的是有不同的合并策略:替换型策略有props
、methods
、inject
、computed
,就是将新的同名参数替代旧的参数;合并型策略是data
, 通过set
方法进行合并和重新赋值;队列型策略有生命周期函数和watch
,原理是将函数存入一个数组,然后正序遍历依次执行;叠加型有component
、directives
、filters
,通过原型链进行层层的叠加。
Vue 中 key 的原理
key 是给每一个 vnode 的唯一id,也是 dif f的一种优化策略,可以根据 key,更准确, 更快的找到对应的 vnode 节点。当我们在使用 v-for
时,需要给单元加上 key
,如果不用 key,Vue 会采用就地复地原则:最小化 element 的移动,并且会尝试尽最大程度在同适当的地方对相同类型的 element,做 patch 或者 reuse。如果使用了 key,Vue 会根据 keys 的顺序记录 element,曾经拥有了 key 的 element 如果不再出现的话,会被直接 remove 或者 destoryed。
keep-alive 原理
keep-alive
是 vue
中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM
。keep-alive
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们 keep-alive
可以设置以下props
属性: include
- 字符串或正则表达式。只有名称匹配的组件会被缓存;exclude
- 字符串或正则表达式。任何名称匹配的组件都不会被缓存;max
- 数字。最多可以缓存多少组件实例。
keep-alive
在 mounted
钩子函数中观测 include
和 exclude
的变化,通过 cache
对象存储需要缓存的组件,通过 render
函数渲染组件,在 render
函数中,首先获取组件的 key
值,拿到key
值后去 cache
对象中去寻找是否有该值,如果没有则以组件 vnode
为值,存入 cache
对象中,如果有则表示该组件有缓存,直接从缓存中拿 vnode
的组件实例。
虚拟 DOM
1. 什么是虚拟 DOM
虚拟 DOM (Virtual DOM
)是一层对真实 DOM
的抽象,在 Javascript
对象中,虚拟 DOM
表现为一个 Object
对象。并且最少包含标签名 (tag
)、属性 (attrs
) 和子元素对象 (children
) 三个属性,不同框架对这三个属性的命名可能会有差别。创建虚拟 DOM
就是为了更好将虚拟的节点渲染到页面视图中,所以虚拟 DOM
对象的节点与真实 DOM
的属性一一照应。
2. 虚拟 DOM 有哪些作用
用传统的原生 api 或 jQuery
去操作 DOM
时,浏览器会从构建 DOM
树开始从头到尾执行一遍流程。比如在一次操作时,需要更新10个 DOM
节点,浏览器收到第一个更新 DOM
请求后,并不知道后续还有9次更新操作,因此会马上执行流程,最终执行10次流程,而通过 VNode
,同样更新10个DOM
节点,虚拟 DOM
不会立即操作DOM
,而是将这10次更新的 diff
内容保存到本地的一个js
对象中,最终将这个 js
对象一次性 attach
到 DOM
树上,避免大量的无谓计算
虚拟 DOM 优势一个是 diff 算法,减少 JavaScript 操作真实 DOM 的带来的性能消耗。另一个是抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,可以是近期很火热的小程序,也可以是各种 GUI。
diff 算法
diff
算法是一种通过同层的树节点进行比较的高效算法,其有两个特点:比较只会在同层级进行, 不会跨层级比较;在 diff 比较的过程中,循环从两边向中间比较。
diff
算法在很多场景下都有应用,在 vue
中,作用于虚拟 dom
渲染成真实 dom
的新旧 VNode
节点比较,diff
整体策略为:深度优先,同层比较。在 vue2.x
中采用双端比较,新旧 VDOM 分别设置 startIndex 和 endIndex 向中间比较,直到双端相遇。在 vue3.x
中先双端比较,然后寻找最长递增子序列,这个子序列是不用变更的。
vue-router 原理
vue-router 有三种路由模式,分别为 hash 模式、history 模式、 memoryHistory 模式。
1. hash 模式
hash 模式下使用 createWebHashHistory
配置,使用 hash 模式下会在浏览器网页路由当中使用哈希字符(#)对 url 进行切割并且匹配哈希字符后的字符进行判断路由匹配与跳转处理。由于这部分 URL 从未被发送到服务器,所以它不需要在服务器层面上进行任何特殊处理。改变 hash 可以监听到 hashchange
事件,vue-router 在其中处理逻辑。
2. history 模式
history 模式下使用 createWebHistory
配置,这个模式下路由的 url 与以前传统的一个网页文件一个访问路径类似,但是应用本质上还是一个 SPA。需要注意服务器的资源匹配方式。使用 window.onpopstate
对浏览器地址进行监听。对浏览器 history api 中的 pushState()
、replaceState()
进行封装,当方法调用,会对浏览器的历史栈进行修改。从而实现 URL 的跳转而无需加载页面。
3. memoryHistory 模式
memoryHistory 模式是提供在 SSR 情况下的服务端使用,通过 createMemoryHistory
能够创建一个基于内存的历史记录。这个历史记录的主要目的是处理 SSR。它在一个特殊的位置开始,这个位置无处不在。如果用户不在浏览器上下文中,它们可以通过调用 router.push() 或 router.replace() 将该位置替换为启动位置。
axios 原理
axios
是一个基于 promise
的 HTTP
库,支持 promise
所有的 API,可以从浏览器中创建 XMLHttpRequests
或者从 node.js 创建 http 请求。简单来说,axios的基本原理就是:通过 XMLHttpRequest
实现一个 ajax
,再通过 promise
对象来对结果进行处理。
vuex 原理
vuex 是一个专为 vue 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
vuex 的功能是依靠 vue 的,vuex 在每个 vue 实例上添加 $store
属性,可以让每个实例访问到 vuex 数据信息;在每个 vue 实例的 data 属性上添加上 state
,这样 state
就是响应式的;收集 options 中传入的所有的 getters
、mutaions
、actions
存放到对象中;当 dispatch
的时候去匹配到 store
类中存放的 actions
方法名字,然后去执行;当 commit
的时候去匹配到 store
类中存放的 mutations
方法名字,然后去执行;这其实就是一个发布订阅模式。
Vue 使用
生命周期
Vue 生命周期总共可以分为8个阶段:创建前后、挂载前后、更新前后、销毁前后,以及一些特殊场景的生命周期。在组合式 api 中,setup() 代替了 created 和 beforeCreated。
生命周期 (Options API) | 生命周期 (Composition API) | 描述 |
---|---|---|
beforeCreate | setup() | 在实例初始化完成、props 解析之后、data() 和 computed 等选项处理之前立即调用,通常用于插件开发中执行一些初始化任务 |
created | setup() | 组件实例已经完全创建,此时挂载阶段还未开始,因此 $el 属性仍不可用,常用于异步数据获取 |
beforeMount | onBeforeMount() | 组件挂载之前,组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点 |
mounted | onMounted() | 组件挂载到实例上去之后调用,可用于获取访问数据和dom元素 |
beforeUpdate | onBeforeUpdate() | 组件数据发生变化,DOM 更新之前调用 |
updated | onUpdated() | 组件的任意 DOM 更新后被调用 |
beforeUnmount | onBeforeUnmount() | 组件实例卸载之前 |
unmounted | onUnmounted() | 组件实例卸载之后,可以在这个钩子中手动清理一些副作用,例如计时器、DOM 事件监听器或者与服务器的连接 |
activated | onActivated() | keep-alive 缓存的组件激活时调用 |
deactivated | onDeactivated() | keep-alive 缓存的组件被移除时调用 |
errorCaptured | onErrorCaptured() | 在捕获了后代组件传递的错误时调用 |
v-if 和 v-for 的优先级
v-for
优先级比 v-if
高,所以永远不要把 v-if
和 v-for
同时用在同一个元素上,带来性能方面的浪费(每次渲染都会先循环再进行条件判断)。如果避免出现这种情况,则在外层嵌套 template
(页面渲染不生成 dom
节点),在这一层进行v-if判断,然后在内部进行v-for循环;如果条件出现在循环内部,可通过计算属性 computed
提前过滤掉那些不需要显示的项。
提高 SAP 应用首屏加载速度
减少首屏渲染时间的方法有很多,总的来讲可以分成两大部分 :资源加载优化和页面渲染优化。
1. 减小入口文件体积
常用的手段是路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加。在 vue-router
配置路由的时候,采用动态加载路由的形式,以函数的形式加载路由,这样就可以把各自的路由文件分别打包,只有在解析给定的路由时,才会加载路由组件。
2. 静态资源本地缓存
后端返回资源采用 HTTP
缓存,设置 Cache-Control
,Last-Modified
,Etag
等响应头或者采用 Service Worker
离线缓存。
前端合理利用 localStorage
。
3. UI 框架按需加载
在日常使用 UI 框架,例如 element-UI
、 antd
,根据文档配置按需引入。
4. 资源压缩
图片资源虽然不在编码过程中,但它却是对页面性能影响最大的因素,对于所有的图片资源,我们可以进行适当的压缩。对页面上使用到的icon
,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上,用以减轻http
请求压力。项目拆完包之后,可以再用 gzip
做一下压缩。
5. 使用SSR
SSR(Server side ),也就是服务端渲染,组件或页面通过服务器生成html字符串,再发送到浏览器,vue
应用可以使用 Nuxt.js
实现服务端渲染。
在 Vue2.x 中给对象添加新属性页面不刷新
vue2
是通过 Object.defineProperty
实现数据响应式,当访问对象属性或者设置属性值的时候都能够触发 setter
与 getter
,但为对象添加新属性的时,却没有经过Object.defineProperty
设置成响应式数据,也就无法触发拦截。
可采取下面三种解决方案实现数据与视图同步更新:
-
Vue.set( target, propertyName/index, value )
-
Object.assign() 创建一个新的对象,合并原对象和混入对象的属性
-
$forcecUpdated 强制 vue 实例重新渲染
组件通信
整理 vue
中8种常规的通信方案
1. 通过 props
传递
适用于父组件传递数据给子组件,子组件设置 props
属性或者使用 defineProps()
,定义接收父组件传递过来的参数,父组件在使用子组件标签时通过字面量来传递值。
2. 通过 emit
触发自定义事件
适用于子组件传递数据给父组件,子组件通过 $emit
/ defineEmits
声明自定义事件,第一个参数作为事件名称,第二个参数作为传递数值。父组件绑定监听器获取到子组件传递过来的参数。
3. 使用 ref
父组件在使用子组件的时候设置ref
,父组件通过 $ref
/ ref().value
来获取子组件实例,通过子组件实例拿到对应的数据。
4. EventBus
适用于兄弟组件传值,创建一个中央事件总线 EventBus
挂载到 vue
实例上,兄弟组件通过$emit
触发自定义事件,$emit
第二个参数为传递的数值,另一个兄弟组件通过 $on
监听自定义事件。
5. $parent
或 $root
通过共同父组件 $parent
或者根组件 $root
搭建通信桥连,使用 on
和 emit
进行兄弟组件之间的传值,类似 EventBus
。
6. attrs
与 listeners
适用于祖先传递数据给子孙,$attrs
是一个包含了组件所有透传 attributes 的对象,包括父级作用域中不作为 prop
被识别且获取的特性绑定。子孙组件中这些透传进来的 attribute 可以在模板的表达式中直接用 $attrs
访问,如果子孙组件里有多个元素时,可以通过 v-bind="$attrs"
显式的绑定到需要的元素上。在 vue3.x
中 $listeners
已经被合并到 $attr
中。
7. Provide
与 Inject
在祖先组件定义 provide
属性,返回传递的值,在后代组件通过 inject
接收组件传递过来的值。
8. Vuex
适用于复杂关系的组件数据传递,Vuex
作用相当于一个用来存储共享变量的容器,state
用来存放共享变量的地方,getter
可以增加一个 getter
派生状态,(相当于store
中的计算属性),用来获得共享变量的值,mutations
用来存放修改 state
的方法,actions
也是用来存放修改state 的方法,不过 action
是在 mutations
的基础上进行。常用来做一些异步操作。
Slot 插槽
slot
本质上是返回 VNode
的函数。通过 slot
插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用,比如布局组件、表格列、下拉选、弹框显示内容等。slot
可以分为以下三种:默认插槽,具名插槽,作用域插槽。
Vue 常用的修饰符
在 vue
中,修饰符处理了许多 DOM
事件的细节,让我们不再需要花大量的时间去处理这些烦恼的事情,而能有更多的精力专注于程序的逻辑处理。vue
中修饰符分为以下五种:表单修饰符、事件修饰符、鼠标按键修饰符、键值修饰符、v-bind 修饰符。