常见问题
为什么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的选项是固定结构,打包时可能包含一些未使用的逻辑。
封装一个组件的思考步骤
- 明确组件的功能和用途。
- 要实现的功能或解决什么问题(降低组件复杂度?UI组件?业务组件?)。
- 考虑组件的使用场景和复用性。
- 确定代码写在哪个目录下。
- 全局通用组件/模块通用组件/普通子组件放在不同的地方。
- 设计组件的接口。
- props确定组件要接收的外部数据。
- slots判断组件是否需要预留插槽,以允许父组件自定义内容。
- event确定组件要触发哪些自定义事件,以便父组件监听拓展功能。
- 根据视觉稿,规划组件的结构和样式。
- 实现代码逻辑,必要的代码注释不能少。
- 调试代码,有条件的情况下编写测试用例。
- 编写组件文档。