本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-09-24
reactive
是 Vue3 中提供的实现响应式数据的方法,在 Vue2.0 中响应式数据是通过 defineProperty
来实现的,而在 Vue3.0 中响应式数据是通过 ES6 的 Proxy
来实现的。
需要注意的是:
reactive
参数必须是对象,会被包装成一个 Proxy
对象。
给 reactive
传递了值类型,不会被包装后才能一个 Proxy
对象,仍是一个值类型,也就不会是响应式数据。
如果给 reactive
传递是一个如果不是json
或者array
,而是一个其他对象,那么直接修改这个对象界面是不会更新的,而要重新赋值才行,如下例中的 time
demo 如下:
<template>
<div>
<p>{{ state }}</p>
<button @click="handleClick">click</button>
<p>{{ obj.time }}</p>
<button @click="handleTime">加一天</button>
</div>
</template>
<script>
import { reactive } from 'vue';
export default {
name: 'App',
setup() {
let state = reactive(123); // 传值的情况
function handleClick() {
state = 'hello';
console.log(state); // hello 值被改变了,但是界面不会更新
}
let obj = reactive({
time: new Date()
})
function handleTime() {
obj.time.setDate(obj.time.getDate() + 1); // 日期加一天
console.log(obj.time); // 值确实被加了一天,但是界面不会更新
// 需要这样才行
const newTime = new Date(obj.time);
obj.time = newTime;
}
return { state, handleClick, obj, handleTime };
}
}
</script>
为了解决 reactive
只能接受一个对象的问题,Vue3 提供 ref
来处理值类型的响应式。 ref
的本质就是:ref(3) -> reactive({value: 3})
在Vue模板中使用,可以直接使用 const a = ref(3)
处理过后的变量 {{ a }}
。但是在 js 业务代码中,必须 a.value
来访问。
需要注意的是,在 template 里使用的是ref
类型的数据,那么 Vue 会自动渲染为 .value
的值,但是如果是 reactive
类型的数据,是不会有这一步操作的。内部通过当前数据的 a.__v_ref
这个属性值来判断是否是 ref
,同时也暴露了isRef
和 isReactive
两个方法来供使用者判断。
示例:
<template>
<div>
<!-- 显示 3 -->
<p>{{ age1 }}</p>
<!-- 显示 { value : 3 } -->
<p>{{ age2 }}</p>
</div>
</template>
<script>
import { reactive, ref, isRef } from 'vue';
export default {
name: 'App',
setup() {
let age1 = ref(3);
let age2 = reactive({ value: 3 })
console.log(isRef(age1)); // true
console.log(isRef(age2)); // false
return { age1, age2 };
}
}
</script>
ref
也能接受一个对象,同时也可以像 reactive
进行递归监听,但是需要注意的是即使传入ref
的是一个对象,更改的时候还是要使用 .value
。
export default {
setup() {
let state = ref({
a: {
b: {
c: 2
}
}
})
function handle() {
state.value.a.b.c = 1; // 要这样更改
}
}
}
递归监听是比较消耗性能的,可以使用 shallowReactive
和 shallowRef
进行非递归监听,只监听第一层。
<template>
<div>
<button @click="handleState">state is: {{ state.a.b.c }}</button>
</div>
</template>
<script>
import { shallowReactive, shallowRef } from 'vue';
export default {
name: 'App',
setup() {
const state = shallowReactive({
a: {
b: {
c: 1
}
}
})
function handleState() {
// 界面是不会发生变化的 因为 shallowReactive 没有监听到 state.a.b.c 只监听到了 state.a
state.a.b.c = 2;
// 这样赋值是可以的,界面也会被更新为3
// state.a = {
// b: {
// c: 3
// }
// }
}
return { state, handleState };
}
}
</script>
需要注意的是 shallowRef
进行非递归监听一个对象的时候,监听的是 .value
。因为 shallowRef(a)
近似于 shallowReactive({ value: a })
,所以 .value
才是第一层。
const state = shallowRef({
a: {
b: {
c: 1
}
}
})
function handleState() {
// state.a = 1; // 这样是不行的
// 这样赋值是可以的,界面也会被更新为3
state.value = {
a: {
b: {
c: 3
}
}
}
}
针对 ref
还有一个方法就是 triggerRef
,用来触发非递归监听属性的更新,但是没有提供 triggerRecative
。
import { triggerRef, shallowRef } from 'vue';
export default {
setup() {
const state = shallowRef({
a: {
b: {
c: 1
}
}
})
function handleState() {
state.value.a.b.c = 2; // 这样不会更新
triggerRef(state); // 主动更新界面
}
}
}
通过 toRaw
方法可以拿到响应式数据的原始对象。
import { reactive, toRaw } from 'vue';
export default {
setup() {
let obj = { name: 'jack' }
let state = reactive(obj)
console.log(obj === state); // false
let obj2 = toRaw(state);
console.log(obj === obj2); // true
}
}
如果想通过 toRaw
拿到 ref 类型的原始数据(创建时传入的那个数据),那么就必须明确的告诉toRaw
方法,要获取的是 .value
的值,因为经过 Vue 处理之后,.value
中保存的才是当初创建时传入的那个原始数据。
export default {
setup() {
let obj = { name: 'jack' }
let state = ref(obj)
let obj2 = toRaw(state.value);
console.log(obj === obj2); // true
}
}
markRaw
方法用来标记一个对象永远都不要被追踪。
import { reactive, markRaw } from 'vue';
export default {
setup() {
let obj = { name: 'jack' }
obj = markRaw(obj)
let state = reactive(obj)
function change() {
state.name = 'andy' // 不会更新视图,因为被markRaw标记的对象,不会被追踪成为响应式数据
}
return {state, change }
}
}
toRef
将某一个对象中的属性变成响应式的数据
export default {
setup() {
// 使用 `ref` 将某一个对象中的属性变成响应式的数据,我们修改响应式的数据是不会影响到原始数据的。
let obj = { name: 'jack' }
let state = ref(obj.name)
state.value = 'andy';
console.log(obj); // { name: 'jack' }
// 当使用 `toRef` 将某一个对象中的属性变成响应式的数据,当我们修改响应式的数据时会影响到原始数据
let obj2 = { name: 'jack' }
let state2 = toRef(obj2, 'name'); // 要这么使用
state2.value = 'andy';
console.log(obj2); // { name: 'andy' }
}
}
使用toRefs
可以将某一个对象中的多个属性变成响应式对象。
export default {
setup() {
let obj = { name: 'jack', age: 18 }
let state = toRefs(obj) // 要这么使用
// 需要先访问.name 才访问 .value
state.name.value = 'andy';
state.age.value = 20;
}
}
customRef
返回一个 ref 对象,可以显示地控制依赖追踪和触发响应。
import { customRef } from 'vue';
function myRef(value) {
// customRef 用来自定义一个 ref
// 接收一个回调函数,这个函数需要返回一个有 get 和 set 的对象,用来定制响应式方法的处理逻辑
// 这个回调有两个参数, track 用来告诉Vue这个数据是需要追踪变化的(要与视图双向绑定),trigger 用来告诉Vue触发界面更新
return customRef((track, trigger) => {
return {
get() {
track();
console.log('get', value);
return value;
},
set(newVal) {
console.log('set', newVal);
value = newVal;
trigger();
}
}
})
}
export default {
setup() {
let name = 'jack'
let state = myRef(name)
function change() {
state.value = 'andy';
}
return { state, change };
}
}
setup
函数只能是同步的,不能是异步的,那么在 setup
中就不能使用 async/await
,那么一些通过网络请求获取数据的逻辑如果放在 setup
中就得使用 .then
的方式来书写。 在某些场景下,可以将获取数据的方法放在customRef
中,即获取到数据之后就进行响应式的追踪以及之后的触发。
<template>
<div>
<ul>
<li v-for="item in state" :key="item.name">
{{ item.name }}
</li>
</ul>
<button @click="change">click</button>
</div>
</template>
<script lang="ts">
import {customRef } from 'vue'
function myRef(dataPath) {
return customRef((track, trigger) => {
let val = [];
fetch(dataPath)
.then((res) => res.json())
.then((data) => {
console.log('获取到数据了');
val = data; // get return的是什么,响应数据就是什么
trigger();
})
.catch(console.log)
return {
get() {
// 千万不能再get中获发送网络请求获取数据
// 会变成死循环,渲染页面就要触发get发送网络请求,获得数据就更新页面,就重新渲染页面就又得发生网络请求。
track();
return val;
},
set(newVal) {
val = newVal;
trigger();
}
}
})
}
export default {
setup: () => {
let state = myRef('./data.json')
console.log(state.value); // get return出来的 []
function change() {
state.value = [
...state.value,
{name: 'zs'}
]
}
return { state, change };
}
}
</script>
在 Vue2 中,可以使用 this.$refs
访问到使用了 ref
属性的DOM元素,而在 Vue3 中,访问方式变得不一样了。
<template>
<div>
<!-- 与Vue2写法一样 -->
<p ref="myDom">this is p</p>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
export default {
setup() {
let myDom = ref(null);
// 当页面挂载完成之后,会将这个Dom赋值给myDom.value
onMounted(() => { // 在Vue3中,使用钩子函数,要这样注册
console.log('myDom', myDom.value); // DOM
})
console.log(myDom.value); // null
return { myDom } // 要在这里暴露出去
}
}
</script>
readonly
方法用于创建一个只读的数据(template中可以使用),并且是递归只读。
shallowReadonly
创建一个只读数据,但是不是递归只读,则只是让第一层为只读。
isReadonly
判断是否是只读的。
<template>
<div>
<p>{{ obj.name }}</p>
<p>{{ obj2.attr.name }}</p>
<button @click="change">click</button>
</div>
</template>
<script>
import { readonly, shallowReadonly, isReadonly } from 'vue';
export default {
setup() {
const obj = readonly({ name: 'jack'})
const obj2 = shallowReadonly({ name: 'andy', attr: { age: 18}})
function change() {
obj.name = 'andy'; // 修改无法生效,界面不会更新,会报警告
obj2.attr.age = 20; // shallowReadonly 修改生效,但是界面也不会更新
}
console.log(isReadonly(obj)); // true
console.log(isReadonly(obj2)); // true
return { obj, obj2 } // 暴露出去template可以使用
}
}
</script>
const 与 readonly 的区别:
可以使用 const 声明一个 readonly,这样就既不能改变引用,也不能改变属性了。
import { readonly } from 'vue';
export default {
setup() {
const obj = readonly({ name: 'jack'}) // 不能改变引用,也不能改变属性
let obj2 = readonly({ name: 'jack'}) // 能改变引用,但不能改变属性
}
}
function shallowReactive(obj) {
return new Proxy(obj, {
get(obj, key) {
console.log('get');
return obj[key]
},
set(obj, key, val) {
console.log('set');
obj[key] = val;
return ture;
}
})
}
function shallowRef(val) {
return shallowReactive({value: val});
}
function reactive(obj) {
if (typeof obj === 'object') {
if (Array.isArray(obj)) {
obj.forEach((item, index) => {
if (typeof item === 'object') {
obj[index] = reactive(item)
}
})
} else {
for (const key in obj) {
const item = obj[key];
if (typeof item === 'object') {
obj[key] = reactive(item)
}
}
}
return new Proxy(obj, {
get(obj, key) {
console.log('get');
return obj[key]
},
set(obj, key, val) {
console.log('set');
obj[key] = val;
return ture;
}
})
} else {
console.warn(`${obj} not an object`);
}
}
function ref(val) {
return reactive({value: val});
}
function shallowReadonly(obj) {
return new Proxy(obj, {
get(obj, key) {
console.log('get');
return obj[key]
},
set(obj, key, val) {
// 禁止修改
console.warn(`${obj} is readonly`);
}
})
}
// isReactive isRef isReadonly 则是在对应的 Reactive Ref Readonly 中 修改 get
new Proxy(obj, {
get(obj, key) {
console.log('get');
obj[key].__v_isRef = true
return obj[key]
},
})
function isRef(val) {
return val.__v_isRef; // 访问val的时候就会触发get注册__v_isRef
}
isRef({__v_isRef: true}); // Vue3暴露isRef对于这个对象也返回true