reactive
const obj = reactive({ count: 0 })
相当于Vue 2.x中的Vue.observable()
API,返回一个普通对象的响应式代理,响应式转换是深层的,会影响对象内部嵌套的属性,基于ES2015的Proxy实现,返回的代理对象不等于原始对象,要避免使用原始对象
经过试验,Vue3中可以通过修改数组下标来响应式的更改数组成员的值了
reactive
将自动解构所有深层次的refs
,同时维持ref
的响应性
const count = ref(1)
const obj = reactive({ count })
// ref 会被解构
console.log(obj.count === count.value) // true
// 它会更新 `obj.value`
count.value++
console.log(count.value) // 2
console.log(obj.count) // 2
ref
ref
的引入是为了以变量形式传递响应式的值而不再依赖访问this
:
const count = ref(0)
接受一个参数,返回一个响应式可改变的ref
对象,ref
对象拥有一个指向内部值的单一属性.value
ref
主要目的是保证基本类型值的响应式,如果传入的参数不是基本类型,会调用reative
方法进行深层响应式转换
ref
使用时:
ref
的返回值setup
中返回应用在模板中时,会自动解构,不需要书写.value
ref
作为reactive
对象的属性被修改或访问时,也会自动解构,不需要书写.value
Array
、Map
等原声集合类中范围ref
时,不会自动解构,需要使用.value
获取值reactive
VS ref
使用ref
和reactive
的区别可以通过如何撰写编撰的JavaScript逻辑比较
// 风格 1: 将变量分离
let x = 0
let y = 0
function updatePosition(e) {
x = e.pageX
y = e.pageY
}
// --- 与下面的相比较 ---
// 风格 2: 单个对象
const pos = {
x: 0,
y: 0,
}
function updatePosition(e) {
pos.x = e.pageX
pos.y = e.pageY
}
使用ref
就是将将风格(1)转换为使用ref
,让基础类型值也具有响应性,使用reactive
和风格(2)一致
只使用reactive
的问题是,使用组合函数的时候必须始终保持对这个组合函数返回对象的引用以保持响应性,这个对象不能够被解构或者展开
// 组合函数:
function useMousePosition() {
const pos = reactive({
x: 0,
y: 0,
})
// ...
return pos
}
// 消费者组件
export default {
setup() {
// 这里会丢失响应性!
const { x, y } = useMousePosition()
return {
x,
y,
}
// 这里会丢失响应性!
return {
...useMousePosition(),
}
// 这是保持响应性的唯一办法!
// 你必须返回 `pos` 本身,并按 `pos.x` 和 `pos.y` 的方式在模板中引用 x 和 y。
return {
pos: useMousePosition(),
}
},
}
解决方法是使用toRefs
将响应式对象的每个对象都转换为响应的ref
:
function useMousePosition() {
const pos = reactive({
x: 0,
y: 0,
})
// ...
return toRefs(pos)
}
// x & y 现在是 ref 形式了!
const { x, y } = useMousePosition()
目前阶段可以从下面两种风格二选其一:
(1)如果在普通的JavaScript中声明基础变量类型与对象变量时一样区别使用ref
和reacitve
,也就是说如果声明响应式的基础类型使用ref
,如果声明响应式对象变量使用reactive
(2)全部使用reactive
,然后在组合函数返回对象时使用toRefs
目前(2020.08.01)官方对ref
和reactive
的最佳实践还没有建议,自己选择更适合自己的风格使用,我会选择风格1使用。
readonly
如果我们希望一个响应式对象在某些情况下被改变,例如我们提供了一个Provide的响应式对象,不希望它在被注入时被改变,这时就可以基于原始对象创建一个只读的Proxy对象
const original = reactive({ count: 0 });
const copy = readonly(original);
// 通过 original 修改 count,将会触发依赖 copy 的侦听器
original.count++;
// 通过 copy 修改 count,将导致失败并出现警告
copy.count++; // 警告: "Set operation on key 'count' failed: target is readonly."
readonly
传入一个对象(普通或者响应式对象)或ref
,返回原始对象的深层的制度代理,任何被访问的嵌套的Property也是只读的
computed
计算属性用来创建依赖于其他状态的状态,有两种方法,一种是接受Getter函数,并为Getter返回值返回一个不可变的响应式ref对象
const count = ref(1);
const plusOne = computed(() => count.value + 1);
console.log(plusOne.value); // 2
plusOne.value++; // error
也可以传入一个拥有get
和set
函数的对象来创建一个可写的ref对象
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
watchEffect
与React的
useEffect
非常类似
watchEffect
会根据响应式状态自动应用和重新应用副作用,它立即执行传入的一个函数,同时响应式追踪其依赖,并在依赖改变时重新运行改函数;
const count = ref(0);
watchEffect(() => console.log(count.value));
// -> logs 0
setTimeout(() => {
count.value++;
// -> logs 01
}, 100)
当watchEffect
在setup
中或生命周期钩子中被调用时,会被链接到组件的生命周期,在组件卸载时自动停止
watchEffect
的返回值是一个函数,可以显示调用它来手动停止侦听
const stop = watchEffect(() => {
/* ... */
})
// later
stop()
副作用函数会执行一些异步的副作用,需要在失效时清除(即完成之前状态已经被改变了),所以侦听器被传入的函数中,可以接受onInvalidate
函数作为入参,用来注册清理失效时的回调,在下面的情况中发生时,onInvalidate
中传入的回调函数会被触发:
watchEffect
再次执行)setup()
或生命周期中函数中调用watchEffect
的话,就是组件卸载时)watchEffect((onInvalidate) => {
const token = performAsyncOperation(id.value)
onInvalidate(() => {
// id 改变时 或 停止侦听时
// 取消之前的异步操作
token.cancel()
})
})
watchEffect
会在组件初始运行时同步打印出来,在监听状态变化后,会在组件更新后执行副作用。
Vue会缓存副作用函数,并且异步的刷新它们,可以避免在同一个Tick中多个状态改变导致不必要的重复调用。在核心实现中,组件的update
函数也是一个被侦听的副作用,当一个用户定义的副作用函数进入队列时,默认情况下,自定义的副作用函数会在所有组件的update
前执行
{
{ count }}
在这个例子中:
count
会在初始运行是同步打印出来count
时,在组件更新前执行副作用如果需要在组件更新后重新运行侦听器副作用(常见的就是在侦听器中获取更新后的DOM,在Vue 2.x中需要使用$nextTick
实现),可以传递带有flush
选项的options
:
post
,在组件更新后执行sync
,同步运行,低效,很少需要pre
,默认,在组件更新前执行这些选项也会更改副作用的首次运行实际
// 在组件更新后触发,这样你就可以访问更新的 DOM。
// 注意:这也将推迟副作用的初始运行,直到组件的首次渲染完成。
watchEffect(
() => {
/* ... */
},
{
flush: 'post'
}
)
在第二个参数中传入onTrack
和onTrigger
来调试,建议在传入的回调中进行debugger
:
watchEffect(
() => {
/* 副作用的内容 */
},
{
onTrigger(e) {
debugger;
},
}
)
onTrack
在依赖被追踪时被调用,onTrigger
在依赖变更导致副作用被触发时调用,这两个回调都接受到一个包含所有依赖项信息的调试器事件
仅在开发模式下生效
watch
watch
需要侦听特定的数据源,并在回调函数中执行副作用。默认情况下,它是惰性的,即只有当被侦听的源发生变化时才会执行回调
// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
// 直接侦听一个 ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
// 侦听多个数据源
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
使用侦听器俩比较数组和对象的值,要求使用副本监听,来避免更新前的值与更新后的值指向同一份引用而无法体现更新前后值的区别:
const numbers = reactive([1, 2, 3, 4])
watch(
() => [...numbers],
(numbers, prevNumbers) => {
console.log(numbers, prevNumbers);
})
numbers.push(5) // logs: [1,2,3,4,5] [1,2,3,4]
尝试检查深度嵌套对象或者数组中某个成员的Property变化时,仍然需要设置deep
为true
const state = reactive({
id: 1,
attributes: {
name: "",
},
});
watch(
() => state,
(state, prevState) => {
console.log(
"deep ",
state.attributes.name,
prevState.attributes.name
);
},
{ deep: true }
);
state.attributes.name = "Alex"; // 日志: "deep " "Alex" "Alex"
上面的状态是有问题的,因为state
和prevState
都是更新后的值,因为返回的都是该对象的当前值的上一个状态值的引用,为了完全侦听深度嵌套的对象和数组,需要对值进行深拷贝(上面使用结构来创建副本的原理相同),可以使用loadsh.cloneDeep等工具来实现
import _ from 'lodash';
const state = reactive({
id: 1,
attributes: {
name: "",
},
});
watch(
() => _.cloneDeep(state),
(state, prevState) => {
console.log(
state.attributes.name,
prevState.attributes.name
);
}
);
state.attributes.name = "Alex"; // 日志: "Alex" ""
watch
vs watchEffect
二者共享的行为包括:
watch
中onInvalidate
是第三个参数,watchEffect
是唯一参数)二者的不同点:
watch
懒执行(不会立即执行),watchEffect
会在初始化时立即执行watch
将依赖提取为第一个参数,更明确哪些状态的改变会重新运行副作用,而watchEffect
自动收集所有依赖watch
可以访问侦听状态变化前后的值unref
用来快速返回ref
的值,如果参数是ref
,返回它的value
,否则返回参数本身。它是val = isRef(val) ? ref.value : ref
的语法糖
toRef
为reactive
对象的属性创建一个ref
,这个ref
可以被传递并且保持对其源Property的响应式连接
const state = reactive({
foo: 1,
bar: 2,
})
const fooRef = toRef(state, 'foo')
fooRef.value++
console.log(state.foo) // 2
state.foo++
console.log(fooRef.value) // 3
当需要将一个Prop中的属性作为ref
传给组合逻辑函数时,可以使用toRef
export default {
setup(props) {
useSomeFeature(toRef(props, 'foo'))
},
}
即使源Property不存在,toRef
也会返回一个可用的ref,这使得它在可选Prop时特别有用,可选Prop并不会被toRefs
处理
toRefs
把一个响应式对象转换为普通对象,该普通对象的每个属性都是一个ref
,与原来的响应式对象一一对应
const state = reactive({
foo: 1,
bar: 2,
})
const stateAsRefs = toRefs(state)
/*
stateAsRefs 的类型如下:
{
foo: Ref,
bar: Ref
}
*/
// ref 对象 与 原属性的引用是 "链接" 上的
state.foo++
console.log(stateAsRefs.foo) // 2
stateAsRefs.foo.value++
console.log(state.foo) // 3
当从一个组合逻辑中返回响应式对象时,用toRefs
是很有效的,它可以让消费组件可以解构或者扩展返回的对象,而不失去响应性
function useFeatureX() {
const state = reactive({
foo: 1,
bar: 2,
})
// 对 state 的逻辑操作
// 返回时将属性都转为 ref
return toRefs(state)
}
export default {
setup() {
// 可以解构,不会丢失响应性
const { foo, bar } = useFeatureX()
return {
foo,
bar,
}
},
}
isRef
判断值是否是ref
对象
isProxy
判断一个对象是否是由reactive
或者readonly
创建的代理
isReactive
判断一个对象是否是由reactive
创建的代理。
如果这个代理是由readonly
创建的,但是又被reactive
创建的另一个代理包裹了一层,那么同样也会返回true
isReadonly
判断一个对象是否是由readonly
创建的代理。
customRef
用来自定义ref
,可以显示依赖追踪和触发响应,接受一个函数,函数的两个参数是用于追踪的track
和触发响应式的trigger
,返回一个带有get
和set
属性的对象
可以使用自定义ref
来实现带防抖功能的v-model
function useDebouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger()
}, delay)
},
}
})
}
export default {
setup() {
return {
text: useDebouncedRef('hello'),
}
},
}
个人还不知道有什么好的应用场景,实际上上面的例子不通过
custromRef
来实现,可能灵活度还更大
toRaw
返回reactive
或者readonly
方法转换为响应式代理的普通对象。用于临时读取数据而无需承担代理访问/跟踪的开销,也可用于写入数据而避免触发更改,访问不会被代理、跟踪,写入时不会触发更改。
不建议一致持有原始对象的引用。
markRaw
显示标记一个对象永远不会转换为响应式代理,返回这个对象本身
const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false
// 如果被 markRaw 标记了,即使在响应式对象中作属性,也依然不是响应式的
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false
它和下方的shallowXXX
API的作用都是用来让用户有选择地退出默认的深度响应式/只读转换模式,并将原始的、未被代理的对象嵌入视图中,主要用来提升性能:
这种标识只停留在根级别,markRaw
对象的属性如果被reactive
处理,仍然会返回一个响应式对象,并且导致原始值与Proxy值不同
const foo = markRaw({
nested: {},
})
const bar = reactive({
// 尽管 `foo` 己经被标记为 raw 了, 但 foo.nested 并没有
nested: foo.nested,
})
console.log(foo.nested === bar.nested) // false
shallowReactive
只为某个对象的私有(第一层)属性创建浅层次的响应式代理,不会对深层属性做深层次、递归地响应式代理
onst state = shallowReactive({
foo: 1,
nested: {
bar: 2,
},
})
// 变更 state 的自有属性是响应式的
state.foo++
// ...但不会深层代理
isReactive(state.nested) // false
state.nested.bar++ // 非响应式
shallowReadonly
与shallowReactive
类似,只为对象的私有(第一层)属性创建浅层的只读响应代理
const state = shallowReadonly({
foo: 1,
nested: {
bar: 2
}
})
// 改变 state 本身的 property 将失败
state.foo++
// ...但适用于嵌套对象
isReadonly(state.nested) // false
state.nested.bar++ // 适用
shallowRef
创建一个ref
,将会追踪它的.value
更改操作,但是不会对变更后的.value
做响应式代理转换
const foo = shallowRef({})
// 更改对操作会触发响应
foo.value = {}
// 但上面新赋的这个对象并不会变为响应式对象
isReactive(foo.value) // false
注意,如果每次都为foo.value
重新赋值,那么仍然会触发响应式改动。上面说的“不会变为响应式对象”指的是更改value
的某个属性不会触发响应式改动