响应式✅
Vue2 和 Vue3 响应式系统的区别和原理?
答案
对比项 | Vue2 响应式 | Vue3 响应式 |
---|---|---|
实现原理 | Object.defineProperty | Proxy |
深度监听 | 递归遍历对象,性能较低 | Proxy 默认支持深度监听,性能更优 |
动态属性 | 需使用 Vue.set 手动添加 | 支持动态添加属性 |
数组监听 | 重写数组方法,无法监听索引和长度变化 | Proxy 默认支持索引和长度变化 |
支持数据结构 | 仅支持对象和数组 | 支持 Map、Set 等复杂数据结构 |
性能 | 初始化时递归遍历,性能较低 | 无需递归,性能更优 |
类型推导 | 类型推导较弱 | 更好的类型推导支持 |
API | data , computed , watch | reactive , ref , watchEffect 等 |
响应式系统核心逻辑包括
- 依赖收集 是通过首次执行渲染函数的时候,调用 setupRenderEffect 完成的依赖收集,依赖收集的核心原理就是基于一个全局变量记录当前的副作用函数,在读取数据的时候,判断当前是否存在副作用函数然后进行存储
- 副作用执行 依赖收集完成后,再后续数据更新的时候,会基于收集的依赖触发副作用函数执行
- 响应式优化 同步多次改变一个值,会 batch 更新,触发一次更新函数, 核心逻辑在 queueJob
一个简化版的响应式系统实现如下:
延伸阅读
- 深入响应系统 详细说明响应式的相关原理和设计
watch 和 computed 有什么区别吗
答案
本质上 watch 和 computed 都是用来绑定一个响应式数据的变化的执行函数。核心差异如下
特性 | watch | computed |
---|---|---|
触发方式 | 数据变化时触发,通过 immediate 在创建 watch 的时候触发 | 懒加载,只有数据变化,调用 .value 才会触发 computed 计算重新计算 |
触发时机 | 在 Vue 实际上 watch 后续状态变化默认是异步执行的,Vue 会 batch 多次的修改,然后通过 Promise.resolve().then 触发一次副作用函数 | 只有在数据变化时访问 .value 才会执行,且是同步触发 |
提示
如果直接调用 @vue/reactivity
wactch 函数在状态变化后是同步触发的,这个控制是通过一个 scheduler
配置来控制的。在 Vue 中使用 watch
函数时,也可通过修改 { flush: 'sync' }
来实现同步触发。详见 回调触发时机
ref 和 reactive 有何区别吗
答案
- 数据类型:
ref
主要解决原始类型无法进行响应式包裹问题,也可以用于数组对象监听,reactive
用于非原始类型 - 取值:
ref
返回一个对象,在所有非模版中操作需要通过.value
访问,你也可以使用unref
函数来解包, reactive 返回的是一个代理对象,可直接访问
watch 和 watchEffect 使用有何区别
答案
功能 | wacth | watchEffect |
---|---|---|
依赖追踪 | 需要手动指定依赖 | 自动追踪依赖变化,降低了手动追踪依赖的复杂度 |
触发时机 | 依赖变化时触发,可以通过 {immediate: true} 在申明时自动触发,默认异步 | 立即执行一次,后续依赖变化时触发,默认异步执行 |
适用场景 | 关注旧值的场景 | 只关注新值的场景 |
提示
watch 和 watchEffect 都支持如下配置
interface Option {
flush?: 'pre' | 'post' | 'sync' // 触发时机 默认:'pre'
onTrack?: (event: DebuggerEvent) => void // 侦听器 onTrack 在追踪依赖的时候执行,仅在开发模式下工作。
onTrigger?: (event: DebuggerEvent) => void // 侦听器 onTrigger 在触发依赖的时候执行,仅在开发模式下工作。
}
watchEffect 还提供了 watchPostEffect 函数 对应 flush: 'post'
的配置, wachtSyncEffect 对应 flush: 'sync'
的配置
延伸阅读
- watch watchEffect 对比 官方文档说明 watch 和 watchEffect 的区别
provide inject 的使用场景?
答案
provide
和inject
用于实现跨组件的状态共享。它允许祖先组件将数据“提供”给任意深度的后代组件,避免层层传递props
。- 在祖先组件中,通过
provide(key, value)
提供数据,在后代组件中通过inject(key)
注入数据。 value 可以是任意数据,也可以传递响应式数据。 - 注意事项
- provide 必须是同步调用,异步调用后续
inject
无法获取到数据 - key 可以采用
Symbol
类型,避免命名冲突 - 在 typescript 中,可以利用
InjectionKey<T>
定义注入 value 的类型
- provide 必须是同步调用,异步调用后续
说下 effectScope ?
答案
- effectScope 用来实现对副作用函数的批量管理,实现手动对副作用函数挂载和清除的控制。典型场景如下
- 在组件中使用
effectScope
来管理副作用函数,组件卸载时会自动清除 watch 等副作用函数,代码详见 scope.stop - 在组件外部通过
effectScope
批量管理副作用函数
- 在组件中使用
- effectScope 主要功能包括
- 采用
const scope = effectScope()
创建一个新的副作用域,通过scope.run(fn)
执行副作用函数,fn
中可以使用watch, watchEffect, computed
等函数监听响应式数据,调用scope.stop()
停止副作用函数的执行,注意effectScope
支持嵌套,停止时候默认嵌套的副作用绑定也会销毁,可以通过effectScope(false)
来忽略对嵌套副作用域的管理 getCurrentScope()
获取当前的副作用实例onScopeDispose(fn)
在副作用函数停止时执行的回调函数fn
- 采用
参考示例
延伸阅读
- effectScope 官方 RFCS 说明 effectScope 的设计初衷
- effectScope 官方 API 说明
- effectScope issue 详细说明 effectScope 的设计初衷
- effectScope code 详细说明 effectScope 的实现原理