Blazor和Vue的元件事件,都使用事件訂閱者模式。相對於上一章的元件屬性,需要多繞一個彎,無論Blazor還是Vue,都是入門的第一個難點。要突破這個難點,一是要熟悉事件訂閱模式<其實不難>;二是多練幾次、熟悉套路。接下面,我們開始學習以下幾個知識點
- 事件訂閱模式
- 使用事件訂閱模式實現子傳父
- 子傳父引數詳解
- 事件定義的校驗
- Vue中使用模板,定義和觸發事件的方法
- Blazor中委託可以傳遞引數嗎
一、事件訂閱模式(簡單的知道整個結構是怎樣就可以了)
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(生命週期函式),重新渲染父元件和子元件,而委託不會。所以使用委託的時候,程式碼層資料是傳遞過去了,但模板沒有響應。
後記:這章有一定難度,且做了一些比較深入的嘗試,建議多看幾次。這章熟練了,就可以愉快的搞雙向繫結了。