Blazor和Vue的组件事件,都使用事件订阅者模式。相对于上一章的组件属性,需要多绕一个弯,无论Blazor还是Vue,都是入门的第一个难点。要突破这个难点,一是要熟悉事件订阅模式<其实不难>;二是多练几次、熟悉套路。接下面,我们开始学习以下几个知识点
一、事件订阅模式(简单的知道整个结构是怎样就可以了)
1、事件的两个主体:事件拥有者和事件订阅者
2、拥有者做的事情:定义事件,触发事件
3、订阅者做的事情:订阅事件(将自己的方法绑定到事件上),事件回调(事件被触发时执行绑定的方法)
4、事件的本质:持有 (任何类的)方法体的内存地址 的某某某,它介于变量和方法之间。说变量,是因为它只是保存了法体的内存地址(自身没有方法体),说方法是因为它可以像方法一要被触发。【注:C#里,有人将委托说成变量,是不对的】
二、使用事件订阅模式实现子传父
1、Blazor和Vue是如何应用事件订阅模式,实现子传父的?
●首先,明确事件的两个主体:
①事件拥有者:子组件,<Child></Child>
②事件订阅者:父组件,<Parent></Parent>
●其次,通过四个步骤实现子传父:子组件定义事件>父组件订阅事件>子组件触发事件>父组件响应事件
步骤①:子组件定义事件
//Vue:const emits = defineEmits( [‘childEvent1’] )//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的数时,将这个数传递给父组件,并在父组件的ParentCount上显示
//Vue=====================================//下面的代码有个Bug,子组件第一次整除3时,触发事件传值,但之后每次递增,都会触发事件,暂时查不出哪里问题,请大佬们指定迷津//子组件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; }}
三、子传父参数详解
通过以下几个方式来对比两个框架后发现,目前Blazor的EventCallback,限制还是很多,未来EventCallback应该像Action和Func一样,具备多重载。“EventCallback<T> 旨在分配单个值,并且只能回调单个方法”,目前只能传递单个值。本章第6节,我们尝试结合委托,看看能不能解决Blazor传递多参数的问题。
1、传递“事件触发DOM”的事件参数(如鼠标事件参数)
//Vue===================================//子组件,重点在DOM触发事件时,传入DOM事件参数e<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的泛型是一个鼠标事件类型<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, MouseEventArgs>(1, "MC", e); await ChildEvent1.InvokeAsync(tuple); }}//父组件<div class = "Parent"> <h1>灰色框里是父组件</h1> <Child ChildEvent1="@ParentReceived"></Child></div>@code { private void ParentReceived(Tuple<int,string,MouseEventArgs> tuple) { Console.WriteLine(tuple.Item1);
Console.WriteLine(tuple.Item2);
}
}
3、同时传递DOM事件参数和自定义参数
//Vue=====================================//子组件,触发DOM里面,以回调方式传入DOM的事件参数<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的事件参数,也包装到类里<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,一步完成定义事件和触发事件两个操作。但这种操作的语义不明确,而且将逻辑混在模板里,不推荐。
//子组件://触发事件的时候,传递两个参数<button @click="$emit('doSomething','arg1','arg2')"></button>//触发事件的时候,传递两个参数和鼠标事件参数<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("成功了"); //后触发事件 }}//父组件<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(生命周期函数),重新渲染父组件和子组件,而委托不会。所以使用委托的时候,代码层数据是传递过去了,但模板没有响应。
后记:这章有一定难度,且做了一些比较深入的尝试,建议多看几次。这章熟练了,就可以愉快的搞双向绑定了。