响应式原理
Vue2
-
基于Object.defineProperty的数据劫持
- 通过
Object.defineProperty
来拦截对象属性的getter
和setter
从而实现对数据的监听 - Vue在初始化时,会调用
observe()
方法遍历data
中的所有属性,并使用Object.defineProperty
进行数据劫持 - 每个属性都会被添加
getter
和setter
- 访问数据时,
getter
负责依赖收集(Dep) - 修改数据时,
setter
触发依赖更新(通知Watcher)
- 通过
-
依赖收集与派发更新
-
通过发布-订阅模式来实现依赖收集和视图更新
-
Dep(依赖收集)
- 构造器里会有一个存储依赖的数组
subs
- 还有添加依赖的
addSub
方法,和通知更新的notify
方法
- 构造器里会有一个存储依赖的数组
-
Watcher(观察者)
- 有一个更新视图的
update
方法
- 有一个更新视图的
-
每创建一个响应式数据,就会创建一个dep实例
-
每当有其他地方引用这个响应式数据,就会创建一个watcher实例,并且dep实例会调用addSub方法,将watcher实例传入,收集依赖
-
响应式数据更新时,dep实例就会调用notify方法,遍历所有收集的依赖(watcher实例),并且调用watcher实例的update方法进行视图更新
-
-
-
缺陷
- 深层嵌套对象性能开销大:递归遍历data
- 对象属性的新增、删除无法检测
- 直接通过索引修改数组无法检测(通过重写数组方法实现响应式)
Vue3
-
基于Proxy对响应式数据进行代理,代理的是一个对象,所以不管是
ref
还是reactive
创建一个响应式数据都是返回一个对象 -
优势
- 代理整个对象,不需要像
Object.defineProoerty
对对象的每个属性进行监听 - 除了get、set,还有has、delete等10几种,能监听到对象属性的新增、删除,通过索引修改数组元素
- 代理整个对象,不需要像
-
原理和Vue2类似,也是发布订阅模式,只是具体细节不同
- 同样是创建响应式数据时(如:通过
ref
),创建一个dep实例 - 每当有其他地方引用这个响应式数据时,调用dep里的
track
方法收集依赖 - 当响应式数据改变时,调用trigger方法,trigger方法里再调用notify进行更新
- 同样是创建响应式数据时(如:通过
双向绑定
概念
- 数据发生变化时视图自动更新,用户操作视图时,数据也会自动更新,数据和视图能自动保持同步
- Vue的双向绑定主要通过
v-model
指令实现的,底层原理依赖于事件监听和Vue的响应式系统
原理
- 《响应式原理》
- 事件监听(input,change)
应用
- 表单输入
- 自定义组件
diff算法
概念
- diff算法是一种用于比较两个树形结构差异的算法。
- 目标是找出两个树之间的最小变化量,从而减少操作次数,提高更新DOM的效率。
- 在Vue中,diff算法被用于比较新旧DOM树的差异,然后将这些差异应用于真实DOM上。
比较流程
只会进行同层比较
-
如果新节点不存在,旧节点存在,则销毁旧节点
-
如果新节点存在,旧节点不存在,则创建元素
-
否则,比对新旧节点
-
如果节点相同,直接复用
-
如果节点类型不同,则创建元素
-
如果类型相同
- 如果是文本节点,直接更新内容
- 如果是其他节点,则更新属性
- 再去比较子节点
-
Vue3
-
判断节点是否相同,相同则直接
return
-
如果新旧节点类型不同,使用
unmount
直接销毁旧节点(及其子节点) -
根据新节点类型进行判断
-
文本节点
- 类型不同,直接创建
- 类型相同,更新内容
-
注释节点
-
静态节点
-
组件节点
-
元素节点
-
类型不同,直接创建
-
类型相同
-
根据patchFlag,进行判断
- 如果是更改属性(class、style),直接更新
- 如果是文本节点,更新内容
-
再对子节点,做一次完全的diff(双端比较)
-
-
-
特点
-
同层比较
- Vue的diff算法只会比较同一层级的节点,不会跨层比较
- 因为跨层比较的节点通常意味着整个子树的替换,开销较大
-
双端比较
- 会从新旧两颗虚拟DOM树的头部和尾部开始比较,尝试找出相同的节点
-
节点复用
- 如果节点相同,直接返回
- 如果节点类型相同,会复用节点,只更新变化的属性,而不是创建新的节点
-
key属性:为节点添加唯一的key,能帮助diff算法快速找到相同的节点
diff算法的优化
- 双端比较算法优化
- 静态标记
虚拟DOM
概念
虚拟 DOM (Virtual DOM) 是一种编程概念,它用一个 JavaScript 对象来描述 DOM 树,Vue、React都使用了虚拟DOM优化页面渲染性能。
优势
-
当我们需要频繁更新页面时,频繁操作DOM会导致页面回流、重绘,从而影响页面性能
-
如果有虚拟DOM,我们修改数据时,Vue会先更新虚拟DOM树,然后将新旧DOM树进行比较(diff算法),找出差异部分,将差异部分更新到真实DOM上
-
使用对象来描述真实DOM,每当数据变化时,Vue通过Diff算法比较新旧虚拟DOM,找出最小变更,并更新真实DOM
- 同层级比较
- 使用key作为唯一标识,避免不必要的DOM操作,提高性能
- 节点复用:如果节点类型相同,会尝试复用节点,只更新变化的属性
-
key的作用
nexttick
概念
- nexttick是Vue中的一个异步方法,用于在DOM更新完成后执行对应的回调函数
Vue中DOM更新机制
- Vue的响应式更新是异步的,数据变化后,不会立即更新DOM
- 而是会在下一次事件循环,合并多次数据更新,再执行DOM操作
- 避免频繁更新DOM,减少回流和重绘,提升页面性能
实现原理
- Promise,MutationObserver,setTimeout
-
const callbacks = [] // 存储回调函数 let pending = false // 标记是否正在等待执行 function flushCallbacks() { pending = false const cpoies = callbacks.slice(0) callbacks.length = 0 cpoies.forEach(cb => cb()) } function nexttick(cb) { callbacks.push(cb) if (!pending) { pending = true Promise.resolve().then(flushCallbacks) } }
使用场景
- 新增/删除元素,获取列表高度
自定义指令
概念
- 自定义指令是 Vue 提供的一个指令的扩展功能,允许开发者创建自己的指令
- 这些指令可以绑定在DOM元素上,并在元素的生命周期钩子上执行特定的操作
- 一般用于需要直接操作 DOM 的场景
补充
-
Vue自己有一些内置指令:v-on,v-bind,v-if,v-show,v-for,v-model
-
注册自定义指令分为:全局注册,局部注册
-
注册自定义指令,可以获取到
- 被绑定的元素
- 绑定信息:值、参数、修饰符、...
- 元素的生命周期钩子:created、mounted、updated、unmounted
场景
- 权限控制:v-hasPermi
- 表单验证
- 高亮指定字符串
生命周期
概念
- Vue生命周期是指Vue实例/组件从创建到销毁的这个过程
- 这个过程中,提供了一系列生命周期钩子函数,以便在不同阶段处理对应的逻辑(数据初始化,操作DOM,销毁监听器)
钩子函数
-
Vue2
-
beforeCreate
- 数据初始化之前,此时无法访问data、computed、methods
- 用途:一般不会使用
-
created
- 实例创建完成,可以访问到实例的data、computed、methods,模板还未编译
- 用途:初始化数据,异步请求
-
beforeMount
- 模板编译完成,DOM还未挂载
- 用途:操作DOM前的工作
-
mounted
- 实例已经挂载到DOM上,可以访问到真实DOM
- 用途:echarts、antv/g6图表库的初始化工作
-
beforeUpdate
- 数据更新之后,视图重新渲染之前
- 用途:
-
updated
- 数据更新后,视图重新渲染后
- 用途:访问更新后的DOM
-
beforeDestroy
- 实例销毁之前
- 用途:清除定时器,清除事件监听
-
destroyed
- 实例销毁后
- 用途:
-
-
Vue3
- onBeforeMount
- onMounted
- onBeforeUpdate
- onUpdated
- onBeforeUnmount
- onUnmounted
组件通信
概念
- 在Vue中,组件是构建用户页面的基本单元。
- 组件之间通常需要相互传递数据。
通信方式
- 父向子:props、$ref
- 子向父:emit
- 祖先后代:provide、inject
- eventBus
- Vue官方状态管理库:Vuex、Pinia
watch、computed、watchEffect
概念
- 用来监听组件实例中的响应式数据
- 当数据变化时,会触发相应的回调函数
- 非常适合处理需要在数据变化时,执行异步操作或复杂逻辑的场景
用法
- 触发watch后,可获取到原来的数据和变化后的数据
- 可以通过配置deep属性,深度监听对象,数组
- 通过配置immediate属性,在监听开始时,立即执行回调函数
应用场景
- 表单校验
- 复用页面,监听路由变化
对比
-
computed
- 返回计算的值,避免过多表达式,不适合做异步操作
- 有缓存机制,无论页面调用几次,都只会执行一次,只有当响应式数据变化时才会重新计算
- 场景:根据优惠/折扣计算价格,翻转分割字符串
-
watchEffect
- 不用显示指定依赖的数据属性,自动推断需要监听的依赖
Vue2和Vue3区别
-
OptionsAPI、CompositionAPI
-
OptionsAPI
- 使用选项来组织逻辑代码
-
CompositionAPI
- 写法更加灵活
- 更好的代码组织,便于维护和重用
-
-
响应式系统
-
Object.defineProperty
-
设计目的是为了对对象的属性进行更精细化的控制(可写、可枚举),vue2利用它的get、set实现了响应式
-
缺点
- 无法监听对象属性的新增、删除
- 无法监听通过索引修改数组
-
-
Proxy
- 对整个对象进行代理,监听、拦截整个对象
- 提供多种捕获器(get、set、has、delete)
-
-
编译优化
- Vue3 对于模板编译进行了优化,支持静态提升
-
更好的TS支持
- 提供更好的类型推导,类型检查
-
其他
- 生命周期
- 模板不需要根标签
- Suspense、Teleport传送DOM结构到指定元素
hooks
概念
- hooks源于React,将状态和方法组合在一起,方便代码复用的一种方式。
- Vue3中,引入类似于hooks的CompositionAPI,让开发者能更好地复用逻辑
- 很好地解决了Vue2中OptionsAPI使用mixin,导致代码来源难以追溯的问题
相关库
Vue-router
概念
- 是Vue官方的路由管理库
- 通过不同的URL访问不同的组件,从而构建单页面应用(SPA)
- SPA通过JavaScript动态更新页面内容,而无需加载整个页面
核心功能
-
路由映射:通过路由配置,将URL映射到Vue组件
-
导航:支持页面之间的跳转,维护浏览器历史记录
-
路由守卫:提供路由跳转前后的钩子,方便权限验证,数据加载顺序等
-
嵌套路由:定义子路由,适合复杂的页面布局
-
动态路由:用户个人中心
-
history模式和hash模式
-
history
-
利用原生的historyAPI,监听popstate事件
-
部署时,需进行配置
-
用户访问example.com/about,Vue Router 处理路由,渲染
About.vue
组件 -
如果用户刷新,浏览器会把这个路径当做静态资源,导致404
-
解决方法:配置Nginx
- 先尝试查找路径对应的静态资源
- 如果找到,则返回
- 未找到,则返回
index.html
,然后让Vue Router处理 -
server { listen 80; server_name example.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri /index.html; # 关键:如果找不到文件,就返回 index.html } error_page 404 /index.html; # 处理 404 }
-
-
-
hash
- 利用url上的hash当做路由,监听hashchange事件(router4.x版本改为history同样的方式)
- 路由跳转时,直接修改hash
- hash值变化时,浏览器不会向服务器发送请求,路由完全交由前端处理
- 不利于SEO
-
路由组件
- 渲染匹配的组件
- 创建链接导航
Vuex
概念
- Vue的全局状态管理库,采用单一状态树,所用状态都存储在
store
对象里
核心概念
-
State
- 存储应用的全局状态
-
Getters
- state的计算属性
-
Mutations
- 修改state的方法
-
Actions
- 处理异步任务
-
Modules
- 将store拆分为多个模块
Pinia
概念
- Vue3官方推荐的新一代状态管理库
核心概念
- State
- Getters
- Actions
优点
-
更符合CompositionAPI的写法
-
更好的TS支持
-
体积更小,性能更好
-
更简单的API
- 不需要Mutations去修改state,可以直接修改,更加灵活
- 每个store都是一个独立的模块,无需Modules