vue的核心是声明式渲染:通过扩展于标准HTML的模版语法,可以根据JavaScript的状态来描述HTML应该是什么样子的.当状态改变,HTML会自动更新.
响应式基础(核心)
能在改变时触发更新的状态被称作响应式
ref
- ref 可以接受任何类型,但是会返回一个包裹的对象,并用.value属性暴露对应的值
- 想要模板中使用ref进行声明变量,需要在script setup 中将其返回出来,不需要带有value属性
- ref 解决了vue2中SFC组件中data声明数据数据的变更失真的情况(Vue2中有个$set 方法,vue3中不存在,原因就是这样)
- 深层次的响应
reactive
- 只适用于对象(包括数组和内置类型,Map和Set同样)
- 创建的对应都是JavaScript Proxy,行为和普通对象一样
- 对于对象而言,该属性声明的变量也是深层次的响应性
- ref 声明对象的时候,也是调用的该Api(因此,ref可以声明一切可以声明的东西)
- 通过ref或者reactive声明的对象才具有响应性,原始对象是没有响应性的。
- 未代理的对象会生成一个代理对象,已经代理的对象,使用该方式则会返回原代理对象
- 局限性:
- 有限的值类型(只能用于对象类型,例如:数组,对象,Set,Map等数据类型)
- 不能替换整个对象(Vue 的响应是通过对象属性进行追踪的,因此整个响应性对象必须是同一个,如果替换成其他对象,则会使原对象的响应性失效)
- 解构不太好 (结构之后的变量,会失去响应性,ref的结构也是没有响应性(不清楚为什么官方文档要推荐使用ref,ref声明的变量,只能结构value。结构后,只是做了一层代理))
computed
- 官方文档解释,computed 期望接受的是一个getter函数(),这个获取的值是同ref声明变量的值一样,使用时需要带有.value。模板表达式中无需解包(即,无需使用.value进行表示)
- 可以接收一个带有get和set函数的对象来创建一个可写的 ref 对象。
- 与方法的区别: 缓存(只有对应响应式变更的时候,才会触发computed),时机(方法总是在重渲染后才执行)
- 注意,getter函数里面不应该有其他副作用(不要改变其他状态、在 getter 中做异步请求或者更改 DOM!)
// TS 类型
// 只读
function computed<T>(
getter: (oldValue: T | undefined) => T,
// 查看下方的 "计算属性调试" 链接
debuggerOptions?: DebuggerOptions
): Readonly<Ref<Readonly<T>>>
// 可写的
function computed<T>(
options: {
get: (oldValue: T | undefined) => T
set: (value: T) => void
},
debuggerOptions?: DebuggerOptions
): Ref<T>
// 例子
// 只可读
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
// 可读可写
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
readonly
- 只读,可接受一个对象(响应式或者原始对象)或一个ref,返回一个只读代理
- 属于深层次的,如果想要浅层次的同理使用shallowReadonly。
// 例子
const original = reactive({ count: 0 })
const copy = readonly(original)
watchEffect(() => {
// 用来做响应性追踪
console.log(copy.count)
})
// 更改源属性会触发其依赖的侦听器
original.count++
// 更改该只读副本将会失败,并会得到一个警告
copy.count++ // war
watchEffect
- 立即执行函数,同时响应式地追踪其依赖,并在依赖更改时重新执行
- 两个参数:
- 第一个参数就是要运行的副作用函数。这个副作用函数的参数也是一个函数,用来注册清理回调。
- 第二参数是一个可选的选项,可以用来调整副作用的刷新时机或调试副作用的依赖。
// TS 类型
function watchEffect(
effect: (onCleanup: OnCleanup) => void,
options?: WatchEffectOptions
): StopHandle
type OnCleanup = (cleanupFn: () => void) => void
interface WatchEffectOptions {
flush?: 'pre' | 'post' | 'sync' // 默认:'pre'
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
}
type StopHandle = () => void
// 例子
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> 输出 0
count.value++
// 清除回调
watchEffect(async (onCleanup) => {
const { response, cancel } = doAsyncWork(id.value)
// `cancel` 会在 `id` 更改时调用
// 以便取消之前
// 未完成的请求
onCleanup(cancel)
data.value = await response
})
// 停止监听器
const stop = watchEffect(() => {})
// 当不再需要此侦听器时:
stop()
watchPostEffect,watchSyncEffect
属于watchEffect的别名,分别对应post时机,和Sync时机,可以查看上面watchEffect。
不过post这个是将会使侦听器延迟到组件渲染之后再执行。
Sync这个在某些特殊情况下 (例如要使缓存失效),可能有必要在响应式依赖发生改变时立即触发侦听器。
注意: sync方法应谨慎使用,因为如果有多个属性同时更新,这将导致一些性能和数据一致性的问题。
watch
普通的监听函数,与Vue2中类似。
官方回答:
- 侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。
- 默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。
- watchEffect的比对:
- 懒执行副作用;
- 更加明确是应该由哪个状态触发侦听器重新执行;
- 可以访问所侦听状态的前一个值和当前值。
- 接受三个参数:
- 侦听器的源 (源的来源有这些:一个函数,返回一个值。一个ref。一个响应式对象。或者是上面组成的数组)
- 是在发生变化时要调用的回调函数。该函数接受3个参数,新值,旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用。当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值。
- 可选的参数是一个对象。
- immediate 立即执行
- deep 深层次改变
- flush 调整回调函数的刷新时机。
- onTrack / onTrigger 调试侦听器的依赖。
- once 回调函数只会运行一次。
注意
setup
- 有两种形式,第一种:script setup,第二种: setup(){}
- 第一种,声明变量和方法类似原生,无需return对应的变量
- 第二种,需要return声明的变量
响应式工具
isRef()
检查某个值是否为 ref。
unref()
如果参数是 ref,则返回内部值,否则返回参数本身。
toRef()
也可以基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。
toValue(3.3+)
将值、refs 或 getters 规范化为值。这与 unref() 类似,不同的是此函数也会规范化 getter 函数。如果参数是一个 getter,它将会被调用并且返回它的返回值。
这可以在组合式函数中使用,用来规范化一个可以是值、ref 或 getter 的参数。
import type { MaybeRefOrGetter } from 'vue'
function useFeature(id: MaybeRefOrGetter<number>) {
watch(() => toValue(id), id => {
// 处理 id 变更
})
}
// 这个组合式函数支持以下的任意形式:
useFeature(1)
useFeature(ref(1))
useFeature(() => 1)
toRefs()
将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。
// 这个例子标明了响应性的深入
const state = reactive({
foo: 1,
bar: 2
})
const stateAsRefs = toRefs(state)
/*
stateAsRefs 的类型:{
foo: Ref<number>,
bar: Ref<number>
}
*/
// 这个 ref 和源属性已经“链接上了”
state.foo++
console.log(stateAsRefs.foo.value) // 2
stateAsRefs.foo.value++
// 进行解构的时候,不失去响应性
function useFeatureX() {
const state = reactive({
foo: 1,
bar: 2
})
// ...基于状态的操作逻辑
// 在返回时都转为 ref
return toRefs(state)
}
// 可以解构而不会失去响应性
const { foo, bar } = useFeatureX()
isProxy()
检查一个对象是否是由 reactive()、readonly()、shallowReactive() 或 shallowReadonly() 创建的代理。
isReactive()
检查一个对象是否是由 reactive() 或 shallowReactive() 创建的代理。
isReadonly()
检查传入的值是否为只读对象。只读对象的属性可以更改,但他们不能通过传入的对象直接赋值。主要监测通过 readonly() 和 shallowReadonly() 创建的代理都是只读的
响应式进阶
shallowRef
ref 属于深层次响应,也就是说使用ref声明的变量在使用的时候,如果接收的值是复杂类型,当对应的属性有变动时,都会触发对应的响应式(watchEffect 进行监听)。
而shallowRef 属于浅层次响应,但是对应的内容还是会进行变更。(如果下图不存在的话:可自行打印一下ref和shallowRef声明变量时的结构。变量套用多层对象形式。)
因此,对于大型数据、以及外部库管理其内部状态的情况,请使用shallowRef进行声明,这样会减少响应性的追踪。
import {ref, shallowRef, watchEffect} from 'vue'
const list = ref([1,2,3])
const shallowList = shallowRef([1,2,3])
list.value[0] = 4
// list => [4,2,3]
// 响应性不会变动
shallowList.value[0] = 4
// shallowList => [4,2,3] 界面上会进行变化
// 响应性会进行变动
shallowList.value = []
// 这里可以使用立即执行函数可以看到响应性是否进行变动
watchEffect(() => {
console.log(shallowList.value)
})
triggerRef
这个是配合上面的shallowRef进行使用的,它主要的作用就是强制触发依赖于一个浅层 ref 的副作用,这通常在对浅引用的内部值进行深度变更后使用。 副作用:一般来说指的是响应性。
const shallow = shallowRef({
greet: 'Hello, world'
})
// 触发该副作用第一次应该会打印 "Hello, world"
watchEffect(() => {
console.log(shallow.value.greet)
})
// 这次变更不应触发副作用,因为这个 ref 是浅层的,但是值是进行变化的,打印可以看出来的
shallow.value.greet = 'Hello, universe'
// 打印 "Hello, universe"
triggerRef(shallow)
customRef
创建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式。
customRef() 预期接收一个工厂函数作为参数,这个工厂函数接受 track 和 trigger 两个函数作为参数,并返回一个带有 get 和 set 方法的对象。
一般来说,track() 应该在 get() 方法中调用,而 trigger() 应该在 set() 中调用。然而事实上,你对何时调用、是否应该调用他们有完全的控制权。
shallowReactive()
浅层的reactive()
shallowReadonly()
浅层的readonly()
toRaw()
根据一个 Vue 创建的代理返回其原始对象。
toRaw() 可以返回由 reactive()、readonly()、shallowReactive() 或者 shallowReadonly() 创建的代理对应的原始对象。
这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。
// 类型
function toRaw<T>(proxy: T): T
// 例子
const foo = {}
const reactiveFoo = reactive(foo)
console.log(toRaw(reactiveFoo) === foo) // true
markRaw()
将一个对象标记为不可被转为代理。返回该对象本身。
// 类型
function markRaw<T extends object>(value: T): T
// 例子
const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false
// 也适用于嵌套在其他响应性对象
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false
effectScope()
创建一个 effect 作用域,可以捕获其中所创建的响应式副作用 (即计算属性和侦听器),这样捕获到的副作用可以一起处理。
// 类型
function effectScope(detached?: boolean): EffectScope
interface EffectScope {
run<T>(fn: () => T): T | undefined // 如果作用域不活跃就为 undefined
stop(): void
}
// 例子
const scope = effectScope()
scope.run(() => {
const doubled = computed(() => counter.value * 2)
watch(doubled, () => console.log(doubled.value))
watchEffect(() => console.log('Count: ', doubled.value))
})
// 处理掉当前作用域内的所有 effect
scope.stop()
getCurrentScope()
如果有的话,返回当前活跃的 effect 作用域。
// 类型
function getCurrentScope(): EffectScope | undefined
onScopeDispose()
在当前活跃的 effect 作用域上注册一个处理回调函数。当相关的 effect 作用域停止时会调用这个回调函数。
这个方法可以作为可复用的组合式函数中 onUnmounted 的替代品,它并不与组件耦合,因为每一个 Vue 组件的 setup() 函数也是在一个 effect 作用域中调用的。
// 类型
function onScopeDispose(fn: () => void): void