一、声明式编程和响应式数据
1、声明式编程
- 逻辑层修改视图层元素属性值的方式有两种,一是命令式,先通过getElementById等方法获取元素对象,然后再修改对象的属性;二是声明式,先将视图层元素的属性值和逻辑层数据绑定,通过修改逻辑层数据,实现视图层元素属性值的自动更新。
- 现代前端开发框架,都采用声明式。
2、响应式数据
- 使用声明式编程的开发框架,需要通过特定的机制通知视图层更新。
- Vue通过Proxy代理机制,劫持读取或修改数据的行为,进而通知使用到数据的UI节点进行更新,被Proxy代理的数据,就称之为响应式数据,在组合式API中,Vue提供了ref和reactive两个API来创建响应式数据。
- 而Blazor中的字段或属性,不需要特殊处理,天然具有响应式特性,其内在机制,暂不清楚。.NET的另一个响应机制MVVM,则是通过事件机制,来通知UI层更新。
二、Vue定义响应式数据的方法
Vue提供reactive和ref两个API来创建响应式数据,只有响应式数据才具有通知更新功能。实际应用中,有几个点需要特别注意:
1、reactive只能用来创建对象类型(如对象、数组、Map、Set),不能创建原始类型(如string、number、boolean等)。而ref可以创建任何类型。
const a = reactive({name:'MC',age:18}) //正确
const b = reactive(18) //错误
const a = ref({name:'MC',age:18}) //正确
const b = ref(18) //正确
2、reactive创建的响应式对象,默认是深层次的,里面嵌套的数据都具有响应式。
const a = reactive({})
a.people = {name:'MC',age:18} //增加的people属性也是响应式
3、当使用ref时,值保存在ref对象的value属性上。如果是在逻辑层代码里读取或修改,需要通过访问value属性,如b.value+=2
;如果在视图层模板中读取或修改,会自动解包,不需要.value
//视图层中,自动解包,不需要.value
//逻辑层中,需要通过.value来访问
<template>
<h1>{{a.name}}</h1>
</template>
<script setup>
import {ref} from 'vue'
const a = ref({name:”MC”,age:18})
a.value.name
</script>
4、当使用ref创建对象类型时,会调用reactive来创建value属性,类似于这种感觉ref(reactive(value))
。当整体替换value值时,新值仍然是响应式的,而reactive如果整体替换新值,则会失去响应性。
- 在实际应用中,创建对象数组类型时,推荐使用ref,因为ref创建的对象,使用数组的map、filter、reduce等返回新数组的方法时,新数组仍然可以保持响应性。
- 使用模块化开发时,export出来的数据,也应该使用ref,这样在引用这个API时,解构出来的数据仍然具有响应性。
- 官方文档有一句话“为了解决reactive带来的限制,Vue 也提供了一个ref方法来允许我们创建可以使用任何值类型的响应式ref”。创建响应式数据时,除了对象类型,其它类型都请尽量使用ref,虽然.value麻烦点。
//整体替换,a失去响应性
const a = reactive({name:”MC”,age:18})
a = reactive{name:”Fun”,age:16}
//ref可以实现响应式替换,b仍具有响应性
const b = ref({name:”MC”,age:18})
b.value = {name:”Fun”,age:16}
//ref实现响应式解构
const obj1 = {foo: ref(1),bar: ref(2)}
const {foo,bar} = obj1 //响应式解构
//使用ref,即使使用filter等方法,b仍然还是响应式的
const b = ref([
{name:”MC1”,age:18},
{name:”MC2”,age:19},
{name:”MC3”,age:20}])
b.value = b.value.filter((e)=>{return e.age >18})
//上例换成reactive,b失去了响应性
const b = reactive([
{name:”MC1”,age:18},
{name:”MC2”,age:19},
{name:”MC3”,age:20}])
b = b.filter((e)=>{return e.age >18})
5、Vue的响应式原理
无论是选项式API的date()方法,还是组合式API的ref和reactive,都会将数据包装为Proxy对象。如果熟悉C#的属性,代理机制其实很简单。
//需要被代理的数据
const obj = {
name:"functionMC",
age:18
}
//当读取或修改数据时,指定代理的行为
//当读取数据时,调用get方法; 当修改数据时,调用set方法
//参数说明:target-被代理的对象、prop-读取的属性、receiver-代理对象、value-新值
const handler = {
get(target,prop,receiver){
//通过代理读取属性时,在返回值之前,它会做一个跟踪的操作,标记哪些地方用到了这个数据
//track(),追踪谁用了这个属性......
return target[prop]
},
set(target,prop,value,receiver){
target[prop] = value
//通过代理修改值之后,做一些其它操作
//trigger(),触发所有使用了该值的位置进行更新
}
};
//为数据obj,创建一个代理对象proxy
//之后对数据obj的读取和修改,都通过代理proxy来完成
const proxy = new Proxy(obj,handler)
三、Blazor中的响应式数据
Blazor中,字段和属性天然具有响应性,对象也是深层次绑定。利用属性的getter和setter方法,可以对响应式数据进行更复杂的控制。
@page "/"
<p>@Num</p>
<MButton @onclick="AddNum">增加</MButton>
<p>@employee.Name</p>
<MButton @onclick="ChangeName">更改名称</MButton>
<p>@employee.Address.City</p>
<MButton @onclick="ChangeCity">更改城市</MButton>
@foreach (var sport in sports)
{
<p>@sport</p>
}
<MButton @onclick="AddSport">增加运动</MButton>
@code{
//值类型的属性绑定
public int Num { get; set; }
//对象类型的字段绑定(深层次响应)
private Employee employee = new Employee {
Id = 1,
Name = "functionMC",
Address = new Address { Province = "广东", City = "广州" } };
//集合类型对象的字段绑定
private List<string> sports = new List<string> { "Running", "Swiming", "BasketBall" };
private void AddNum()
{
Num++;
}
private void ChangeName()
{
employee.Name = "MC";
}
private void ChangeCity()
{
employee.Address.City = "深圳";
}
private void AddSport()
{
sports.Add("FootBall");
}
}