Blazor和Vue对比学习(基础-4):事件和子传父

BlazorVue组件事件,都使用事件订阅者模式。相对于上一章的组件属性,需要多绕一个弯,无论Blazor还是Vue,都是入门的第一个难点。要突破这个难点,一是要熟悉事件订阅模式;二是多练几次、熟悉套路。接下面,我们开始学习以下几个知识点

  • 事件订阅模式
  • 使用事件订阅模式实现子传父
  • 子传父参数详解
  • 事件定义的校验
  • Vue可以在模板中定义和触发事件
  • Blazor中委托可以传递参数吗

 

 

一、事件订阅模式(简单的知道结构是怎样就可以了)

1、事件的两个主体:事件拥有者和事件订阅者
2、拥有者:定义事件,触发事件
3、订阅者:订阅事件(将自己的方法绑定到事件上),事件回调(事件被触发时执行绑定的方法)
4、事件的本质:持有方法的内存地址,它介于变量和方法之间。说变量,是因为它保存了方法体的内存地址(自身没有方法体),说方法是因为它可以像方法一要被触发。在C#中,事件是类的成员。

 

 

二、使用事件订阅模式实现子传父

1、Blazor和Vue是如何应用事件订阅模式,实现子传父呢?

●首先,明确事件的两个主体:

①事件拥有者:子组件,<Child></Child>
②事件订阅者:父组件,<Parent></Parent>

●接着,通过以下四个步骤实现子传父:子组件定义事件>父组件订阅事件>子组件触发事件>父组件响应事件

步骤①:子组件定义事件

//Vue(事件是childEvent1,emits是defineEmits返回的一个对象,通过它可以触发事件):
const emits = defineEmits( [‘childEvent1’] )

//Blazor(事件是ChildEvent,EventCallback<T>是Blazor内置的事件类型):
[Parameter]
public EventCallback<string> ChildEvent1 { get; set; }

 

步骤②:父组件订阅事件

//Vue
//使用v-on:指令,简写为@。
<Child @childEvent1 = “parentReceived”></Child> 

//Blazor:
//事件和属性的用法保持统一
<Child ChildEvent1 = “@ParentReceived”></Child> 

 

步骤③:子组件触发事件

//Vue:
//emits为defineEmits方法的返回值
function childEmit(){
    emits('childEvent1','I am children')
}

//Blazor:
//触发事件是异步方法
private async Task ChildEmit()
{
    await ChildEvent1.InvokeAsync("我是子组件");
}

 

步骤④:父组件响应事件

 

//Vue:
function parentReceived(msg){
    Console.log(‘收到子组件的信息为:’+msg);
}

//Blazor:
Private void ParentReceived(string msg){
    Console.WriteLine(‘收到子组件的信息为:’+msg)
}

 

 2、下面举个粟子串一下:

(1)子组件:一个数值属性(ChildCount),一个按钮;父组件:一个数值属性(ParentCount)

(2)子组件,点击按钮,递增ChidCount,同时每逢可以整除3的数时,将这个数传递给父组件,并在父组件上显示

//Vue=====================================
//【下面Vue的代码有个Bug,子组件第一次整除3时,触发事件传值,但之后每次递增,都会触发事件,暂时查不出哪里问题,请大佬们指定迷津】【找到原因:Add函数要写成回调格式】

//子组件Child代码
<template>
    <div class="Child">
        <h1>红色框是子组件</h1>
        <h3>ChildCount:{{childCount}}</h3>
        <button @click="Add">点击增加</button>
    </div>
</template>

<script setup>
import {ref} from 'vue'
const emits = defineEmits(['childEvent1'])
const childCount = ref(0)
function Add(){
    childCount.value++
    if(childCount.value % 3 === 0){
        emits('childEvent1',childCount)
    }
}
</script>

//父组件Parent代码
<template>
  <div class="Parent">
      <h1>灰色框是父组件</h1>
      <h3>ParentCount:{{parentCount}}</h3>
      <Child @childEvent1 = "parentReceived"></Child>
      
  </div>
</template>

<script setup>
import { ref } from 'vue'
import Child from './components/Child.vue'
const parentCount = ref(0)
function parentReceived(arg1){
    parentCount.value = arg1
}
</script>
//Blazor====================================
//子组件Child代码
<div class="Child">
    <h1>红色框里是子组件</h1>
    <h3>ChildCount:@ChildCount</h3>
    <button @onclick="Add">传值给父组件</button>
</div>

@code {
    private int ChildCount = 0;

    [Parameter]
    public EventCallback<int> ChildEvent1 { get; set; }

    private async Task Add()
    {
        ChildCount++;
        if (ChildCount % 3 == 0)
        {
            await ChildEvent1.InvokeAsync(ChildCount);
        }
    }
}

//父组件代码
<div class = "Parent">
    <h1>灰色框里是父组件</h1>
    <h1>ParentCount:@ParentCount</h1>
    <Child ChildEvent1="@ParentReceived"></Child>
</div>


@code {
    private int ParentCount = 0;

    private void ParentReceived(int msg)
    {
        ParentCount = msg;
    }
}

 

 

 

子传父参数详解

通过以下几个方式来对比两个框架后发现,目前BlazorEventCallback,限制还是比较多,“EventCallback<T> 旨在分配单个值,并且只能回调单个方法”,意思是只能传递一个值。下面Blazor的案例,我们使用元组Tuple来解决传递多值的问题。同时,在本章第6节,我们还将尝试结合委托,看看能不能解决Blazor传递多参数的问题,在第6节里,还将出现一个非常重要的生命周期函数StateHasChanged。

1、传递DOM事件对象(如鼠标事件对象)

//Vue===================================
//子组件,重点在DOM触发事件时,传入DOM事件对象e,所有DOM事件绑定的回调,都会自动传入这个e对象,Blazor也一样
<template>
    <div class="Child">
        <h1>红色框是子组件</h1>
        <button @click="childEmit">点击增加</button>
    </div>
</template>

<script setup>
import {ref} from 'vue'
const emits = defineEmits(['childEvent1'])
function childEmit(e){
    emits('childEvent1',e)
}
</script>


//父组件,常规的接收参数操作
<template>
  <div class="Parent">
      <h1>灰色框是父组件</h1>
      <Child @childEvent1 = "parentReceived"></Child>
      
  </div>
</template>

<script setup>
import { ref } from 'vue'
import Child from './components/Child.vue'
function parentReceived(e){
    console.log(e)
}
//Blazor====================================
//子组件,EventCallback的泛型是一个鼠标事件类型。所有DOM元素的事件绑定回调,都会自动传入e参数,和Vue一样
<div class="Child">
    <h1>红色框里是子组件</h1>
    <button @onclick="ChildEmit">传值给父组件</button>
</div>


@code {
    [Parameter]
    public EventCallback<MouseEventArgs> ChildEvent1 { get; set; }

    private async Task ChildEmit(MouseEventArgs e)
    {
        await ChildEvent1.InvokeAsync(e);
    }
}


//父组件
<div class = "Parent">
    <h1>灰色框里是父组件</h1>
    <Child ChildEvent1="@ParentReceived"></Child>
</div>

@code {
    private void ParentReceived(MouseEventArgs e)
    {
        Console.WriteLine(e);
    }
}

 

2、传递自定义参数(单个值参数、复杂参数、多个参数)

//Vue=====================================
//触发事件时,可以传递任意数量、任意类型参数
<template>
    <div class="Child">
        <h1>红色框是子组件</h1>
        <button @click="childEmit">点击</button>
    </div>
</template>

<script setup>
import {ref} from 'vue'
const emits = defineEmits(['childEvent1'])
function childEmit(){
    emits('childEvent1',1,'Hi',[1,2,3],{name:'MC',age:18})
}
</script>

<template>
  <div class="Parent">
      <h1>灰色框是父组件</h1>
      <Child @childEvent1 = "parentReceived"></Child>
      
  </div>
</template>


//父组件按顺序接受参数
<script setup>
import { ref } from 'vue'
import Child from './components/Child.vue'
function parentReceived(msg1,msg2,msg3,msg4){
    console.log(msg1)
    console.log(msg2)
    console.log(msg3[1])
    console.log(msg4.name)
}

 

//Blazor====================================
//子组件。EventCallback只能传递一个参数,但不限制类型,如果要传递多个参数,可以使用数组或元组Tuple

<div class="Child">
    <h1>红色框里是子组件</h1>
    <button @onclick="ChildEmit">传值给父组件</button>
</div>

@code {
    [Parameter]
    public EventCallback<Tuple<int,string>> ChildEvent1 { get; set; }

    private async Task ChildEmit()
    {
var tuple = new Tuple<int, string>(1, "MC");
await ChildEvent1.InvokeAsync(tuple); } } //父组件 <div class = "Parent"> <h1>灰色框里是父组件</h1> <Child ChildEvent1="@ParentReceived"></Child> </div> @code { private void ParentReceived(Tuple<int,string> tuple) { Console.WriteLine(tuple.Item1);
Console.WriteLine(tuple.Item2);
}
}

 

 

3、同时传递DOM事件参数和自定义参数

//Vue=====================================
//子组件,触发DOM里面,以回调方式传入DOM的事件参数,Vue中必须以这种方式获取e后,才可以实现自定义参数和e一起传递,这和只传e不一样,和Blazor也不一样

<template>
    <div class="Child">
        <h1>红色框是子组件</h1>
        <button @click="(e)=>childEmit(e)">点击</button>
    </div>
</template>

<script setup>
import {ref} from 'vue'
const emits = defineEmits(['childEvent1'])
function childEmit(e){
    emits('childEvent1',1,'Hi',e)
}
</script>

//父组件按顺序接收参数,没有变化
//Blazor====================================
//子组件。因为EventCallback只能传递一个参数,所以可以考虑把DOM的事件参数,也包装到类里。和只传e一样,回调能自动传入e,而Vue不可以。

<div class="Child">
    <h1>红色框里是子组件</h1>
    <button @onclick="ChildEmit">传值给父组件</button>
</div>

@code {
    [Parameter]
    public EventCallback<Tuple<int,string,MouseEventArgs>> ChildEvent1 { get; set; }

    private async Task ChildEmit(MouseEventArgs e)
    {
var tuple = new Tuple<int, string, MouseEventArgs>(1, "MC", e);
await ChildEvent1.InvokeAsync(tuple); } } //父组件,略

 

  

、事件定义的校验

1、事件定义时,可以对事件的参数和返回值做约束。本来事件的使用,就比较绕弯烧脑,所以在还没有熟练使用事件前,可以暂且绕过这一环。

2、在Vue中,defineEmits有两种写法,一是数组写法,如defineEmits[事件1, 事件1];二是对象写法,在对象写法中,可以定义校验。对象写法如果在JS环境下,会比较麻烦;在TS中,表达反而简明很多。同时,和props一样,JS只支持运行时校验,而TS支持编译校验。如果需要使用校验,建议直上TS

3、Blazor是强类型,天生自带类型约束,但仅可以约束参数,无法约束返回值。以下案例,仅列举Vue的事件校验

//Vue=====================================
//JS中:比较麻烦
const emits = defineEmits({
  event1:null//不做校验
  event2: (arg1,arg2) => { //校验参数
      if (arg1 && arg2) {
        return true
      } else {
        console.warn('请确定传递参数')
        return false
      }
    }
})

 
//TS中:语义明确,表达简明
const emits = defineEmits<{
   (e: 'event1'): void
   (e: 'event1', arg1: string, arg2:string): void
}>()

 

 

 

五、Vue中在模板中定义和触发事件的方法

Vue中,可以在模板中使用$emit,一步完成定义事件和触发事件两个操作。但这种操作的语义不明确,而且将逻辑混在模板里,不推荐。

//子组件:

//定义事件doSomething,同时触发。触发事件的时候,传递两个参数
<button @click="$emit('doSomething','arg1','arg2')"></button>

//定义事件doSomething,同时触发。触发事件的时候,传递两个参数和鼠标事件参数
<button @click="(e)=>$emit('doSomething','arg1','arg2',e)">/button>

 

//父组件没有变化,传来几个参数,父组件的回调函数就定义多少个参数:

<Child @doSomething="receiveMsg"></Child>

<script setup>
//接收两个参数
function receiveMsg(msg1,msg2) {console.log(`收到子组件的信息,${msg1}-${msg2}`)}
//接收两个信息和鼠标事件参数
function receiveMsg(msg1,msg2,e) {console.log(`收到子组件的信息,${msg1}-${msg2}-${e}`)}

 

 

六、Blazor中,委托可以完成子传父的任务吗?

1、子组件中,只触发委托

//子组件中只触发委托,父组件渲染失败
//子组件。和事件不一样,委托可空,所以加?号
<div class="Child">
    <h1>红色框里是子组件</h1>
    <button @onclick="ChildEmit">传值给父组件</button>
</div>

@code {
    [Parameter]
    public Action<string,int>? ChildAction1{ get; set; }

    private void ChildEmit()
    {
        ChildAction1?.Invoke("MC", 18);
    }
}


//父组件。代码里实际上接收到值了,但模板里没有显示。此时,新增一个按钮“<button @onclick = "@(()=>StateHasChanged())"></button>”,即可显示
<div class = "Parent">
    <h1>灰色框里是父组件</h1>
    <h1>@actionMsg1</h1> //但模板里无显示值
    <h1>@actionMsg2</h1> //但模板里无显示值
    <Child ChildAction1="@ParentReceived2"></Child>
</div>

@code {
    private string actionMsg1 = "";
    private int actionMsg2 = 0;
    //其实回调接收到参数值
    private void ParentReceived2(string actionMsg1,int actionMsg2)
    {
        this.actionMsg1 = actionMsg1;
        this.actionMsg2 = actionMsg2;
    }
}

 

2、子组件中,事件和委托一起触发

//子组件中,同时触发委托和事件,且先触发委托,后触发事件,成功传值

//子组件
<div class="Child">
    <h1>红色框里是子组件</h1>
    <button @onclick="ChildEmit">传值给父组件</button>
</div>

@code {
    [Parameter]
    public EventCallback<string> ChildEvent1 { get; set; } //事件

    [Parameter]
    public Action<string,int>? ChildAction1{ get; set; } //委托

    private async Task ChildEmit()
    {
        ChildAction1?.Invoke("MC", 18); //先触发委托
        await ChildEvent1.InvokeAsync("成功了"); //后触发事件【正常情况下,触发事件最后先判断一下是否为空(ChildEvent1.HasDelegate),因为EventCallback不可为空】
    }
}


//父组件
<div class = "Parent">
    <h1>灰色框里是父组件</h1>
    <h1>@eventMsg1</h1>
    <h1>@actionMsg1</h1>
    <h1>@actionMsg2</h1>
    <Child ChildEvent1="@ParentReceived1" ChildAction1="@ParentReceived2"></Child>
</div>


@code {
    private string eventMsg1 = "";
    private string actionMsg1 = "";
    private int actionMsg2 = 0;

    private void ParentReceived1(string eventMsg1)
    {
        this.eventMsg1 = eventMsg1;
    }

    private void ParentReceived2(string actionMsg1,int actionMsg2)
    {
        this.actionMsg1 = actionMsg1;
        this.actionMsg2 = actionMsg2;
    }
}

 

3、原因?

(1)委托和EventCallback<T>,在逻辑层,都可以实现组件间数据传递

(2)委托和EventCallback<T>,最主要区别,当EventCallback触发时,会调用父组件的StateHasChanged(生命周期函数),重新渲染父组件和子组件,而委托不会。所以使用委托的时候,逻辑层数据是传递过去了,但模板没有响应。

 

posted @ 2022-05-09 23:34  functionMC  阅读(884)  评论(2编辑  收藏  举报