常见问题
为什么data属性是一个函数而不是一个对象?
通过构造函数创建全局vue实例的时候,定义data属性既可以是一个对象,也可以是一个函数;组件中定义data属性,只能是一个函数,如果为组件data属性直接定义为一个对象,则会得到警告信息。
原因分析
JS通过构造函数来创建实例;当data被定义成对象时,这个对象会作为构造函数的一个属性,所有通过该构造函数创建的实例,data都是同一个内存地址的引用,其中一个实例修改data会影响到其他实例。- 当我们将组件中的
data属性写成一个函数,数据以函数的返回值形式定义,每复用一次组件,就会在内存中分配新的空间存储,不会受到其他实例对象数据的污染。 全局vue实例通常只有一个,不存在复用和数据共享的问题,所以此时data属性可以定义为一个对象。
动态给data添加一个新的属性时会发生什么?怎样解决?
vue2通过Object.defineProperty实现数据响应式,对象和数组的新增属性可能无法劫持到进而触发视图更新。vue3通过proxy实现数据响应式,直接动态添加新属性仍可以劫持到。
原因分析
vue2中采用Object.defineProperty来劫持对象的属性,初始化时递归遍历data属性,给每个属性添加getter和setter方法;当访问初始化已有属性或者设置已有属性的值时能够触发getter和setter方法;但是为对象添加新属性时,并没有经过拦截,所以无法触发响应式;如果存在深层的嵌套对象关系,需深层监听,造成性能问题。数组部分方法(
push、pop、shift、unshift、splice、sort、reverse)经过重写,用Object.defineProperty包装过,所以有响应式效果。
解决方案
Vue.set(): 通过
Vue.set(调用defineReactive => Object.defineProperty)向响应式对象中添加一个property,并确保这个新property同样是响应式的,且触发视图更新。Object.assign(): 合并原对象和混入对象的属性,再赋值给原对象。
$forceUpdated(): 强制更新,迫使实例重新渲染(不建议)。
Proxy实现数据响应式:
Proxy的监听是针对一个对象的,对这个对象的所有操作会进入监听操作。
computed VS watch
computed计算属性,是基于响应式依赖来创建一个属性,目的是根据其他响应式数据计算得出一个新的值,并且这个值会被缓存,只有当依赖的响应式数据发生变化时才会重新触发计算。
- 创建计算属性时调用
ReactiveEffect构造函数生成一个effect副作用函数实例。 - 在访问计算属性的
value属性时,触发getter函数,此时effect.run()收集依赖。 - 判断
_dirty,是否需要重新计算:- 为
true时,重新计算新的值赋值给_value,并标记_dirty为false。 - 为
false时,直接返回已缓存的_value。
- 为
- 当依赖的数据变化时,会触发这些响应式数据的
setter函数,进而触发effect副作用函数的调度器函数,将_dirty标记为true,下次访问value属性时要重新计算。
- 创建计算属性时调用
watch侦听器,用于监听一个或者多个响应式数据的变化,并在数据变化时执行响应的回调函数,目的在与数据变化时执行副作用,比如发送网络请求、获取DOM等。
- 解析监听源,标准化
source为getter函数。 - 根据
flush创建调度器(控制回调执行时机)。 - 调用
ReactiveEffect构造函数生成一个effect副作用函数实例,调度器作为副作用函数的调度器函数传入。 - 定义回调函数,对比新旧值是否变化,再更新旧值。
effect.run()初始化旧值和依赖收集。- 在依赖变化时,执行调度器函数,进而触发回调函数执行。
- 解析监听源,标准化
| 对比项 | computed | watch |
|---|---|---|
| 依赖声明 | 隐式(自动追踪) | 显式(需指定监听源) |
| 缓存 | 支持缓存,只有当依赖的数据发生变化时,才会重新计算,否则会从缓存中读取之前的计算结果,可以避免不必要的计算开销 | 不支持缓存,每当监听的数据变化时,watch都会执行回调函数 |
| 异步 | 不支持异步,需要立刻返回计算结果 | 支持异步操作,在数据变化后执行回调 |
| 返回值 | 计算属性内部函数需要返回计算结果 | 不需要返回值 |
初始化执行 | 首次访问时会执行 | 设置immediate控制 |
ref VS reactive
| 对比项 | ref | reactive |
|---|---|---|
| 接受类型 | 任意类型 | 仅对象类型 |
| 访问方式 | 通过.value访问 | 直接访问属性 |
| 模板解包 | 自动解包(无须.value) | 无须解包 |
| watch | 对于引用类型,watch默认不会开启深度监听 | 默认开启深度监听 |
| 引用替换 | 保持响应(.value = 新引用) | 完全丢失响应 |
| 深层响应 | 默认支持 | 默认支持 |
| 解构处理 | 需配合toRefs | 需配合toRefs |
| 性能优化 | shallowRef | shallowReactive |
| 使用场景 | 基本类型、跨组件传递数据、与原生DOM交互 | 处理复杂对象和数组、状态管理 |
深入Vue3响应式:手写实现reactive与ref上篇文章介绍了Vue3响应式的两个核心API,知道了两者的用法于区别 - 掘金
toRef VS toRefs
| 对比项 | toRef | toRefs |
|---|---|---|
| 作用 | 将响应式对象的单个属性转换为一个Ref引用 | 将响应式对象的所有属性批量转为Ref类型,并包装成一个普通对象 |
| 参数 | 接收三个参数: 响应式对象obj 属性名key 默认值defaultValue | 仅接收一个参数:响应式对象obj |
| 不存在的属性 | 传入不存在的key也会返回一个Ref引用 | 返回的对象只会包含源响应式对象所包含的属性 |
| 适用场景 | 指定解构某个单一属性的情况(给hooks解构props的某个属性) | 适用于需要解构reactive对象(多个属性)的场景 |
Composition Api 相比 Options Api的优势
- 逻辑聚合更清晰: 同一业务相关逻辑,Options Api分散在不同的选项中, 而Composition Api可以集中放在一个函数里,有更高的代码可读性和维护性。
- 代码复用更加灵活: Composition Api,通过自定义Hooks可以抽离通用逻辑,复用方式比mixins更加清晰,不会出现命名冲突,也能明确知道复用逻辑的来源。
- 类型推断更友好: 结合TS时,Composition Api的函数式写法能让类型推导更自然,而Options Api依赖this上下文,类型定义需要额外处理。
- 按需导入减小包体积: Composition Api支持按需导入,而Options Api的选项是固定结构,打包时可能包含一些未使用的逻辑。
setup函数与<script setup>的区别
| 对比项 | setup函数 | <script setup> |
|---|---|---|
| 写法位置 | 在组件选项中定义 | 作为script标签的属性 |
| 返回值处理 | 需返回对象和函数,模板中才能使用 | 无需返回,变量和函数直接在模板中使用 |
| 组件注册 | 需在components选项中注册局部组件 | 导入组件后自动注册,无须显式声明 |
| Props、Emits | 通过参数props和context.emit处理 | 通过defineProps和defineEmits编译宏处理 |
| 类型支持 | 需手动指定类型,类型推断较弱 | 原生支持TS,类型推断更友好 |
| 生命周期使用 | 需导入钩子函数并在setup函数中调用 | 直接导入钩子函数调用,与代码逻辑融合更自然 |
| 代码简洁度 | 相对繁琐,需手动返回和注册组件 | 更简洁,开发效率更高 |
封装一个组件的思考步骤
- 明确组件的功能和用途。
- 要实现的功能或解决什么问题(降低组件复杂度?UI组件?业务组件?)。
- 考虑组件的使用场景和复用性。
- 确定代码写在哪个目录下。
- 全局通用组件/模块通用组件/普通子组件放在不同的地方。
- 设计组件的接口。
- props确定组件要接收的外部数据。
- slots判断组件是否需要预留插槽,以允许父组件自定义内容。
- event确定组件要触发哪些自定义事件,以便父组件监听拓展功能。
- 根据视觉稿,规划组件的结构和样式。
- 实现代码逻辑,必要的代码注释不能少。
- 调试代码,有条件的情况下编写测试用例。
- 编写组件文档。