Blazor和Vue對比學習(基礎1.4):事件和子傳父

functionMC發表於2022-05-09

BlazorVue元件事件,都使用事件訂閱者模式。相對於上一章的元件屬性,需要多繞一個彎,無論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;
    }
}

 

 

 

子傳父引數詳解

通過以下幾個方式來對比兩個框架後發現,目前BlazorEventCallback,限制還是很多,未來EventCallback應該像ActionFunc一樣,具備多過載。“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(生命週期函式),重新渲染父元件和子元件,而委託不會。所以使用委託的時候,程式碼層資料是傳遞過去了,但模板沒有響應。

 

後記:這章有一定難度,且做了一些比較深入的嘗試,建議多看幾次。這章熟練了,就可以愉快的搞雙向繫結了。

相關文章