Vue-3 props,$emit,slot,render,JSX和createElement

Vue-3 props,$emit,slot,render,JSX和createElement

Props 和 $emit

使用 Vue 開發項目時,我們將項目中的內容按照模塊劃分,但是有時候模塊和模塊之間會存在數據交互。在真正的項目開發中,父子、兄弟組件之間需要互相傳值。最傳統的傳值方式就是 props 和 $emit。

一、Props

Prop 是你可以在組件上註冊的一些自定義特性。當一個值傳遞給一個 prop 特性的時候,它就變成了那個組件實例的一個屬性。爲了給博文組件傳遞一個標題,我們可以用一個 props 選項將其包含在該組件可接受的 prop 列表中。

1. props傳值的示例

<!-- 父組件 -->
<template>
    <div id="app">
        <!-- 父組件將Home組件調用了三次,每次調用時傳入不同的參數 -->
        <Home msg="hello world!" />
        <Home msg="tom" />
        <Home msg="lion king" />
    </div>
</template>

<script>
import Home from './components/Home'

export default {
    name: 'App',
    components: { Home },
    data() {
        return { }
    },
}
</script>
<!-- 子組件 -->
<template>
    <div class="home">
        <!-- props 中接受到的數據,直接渲染就好 -->
        <h3>{{ msg }}</h3>
    </div>
</template>

<script>
    export default {
        name: "Home",
        // 通過props接受外界在調用當前組建時,傳入的參數
        props: ['msg']
    }
</script>

也可以向子組件傳不同的參數:

<!-- ... -->
<Home msg="hello world!" />
<Home name="tom" />
<Home ani="lion king" />
<!-- ... -->
<script>
    export default {
        name: "Home",
        // props接受多個參數
        props: ['msg','name','ani']
    }
</script>

2. props類型驗證
如果我們希望每個 prop 都有指定的值類型,並且以對象形式列出 prop,這些屬性的名稱和值分別是 prop 各自的名稱和類型。

// ...
// 使用props接收數據並規定數據的類型
props: {
    title: String,
    likes: Number,
    isPublished: Boolean,
    commentIds: Array,
    author: Object,
    callback: Function,
    contactsPromise: Promise // or any other constructor
}
// ...

爲了讓傳值變得更有靈活性,vue 提供了以下 props 的驗證方式:

props: {
    // 基礎的類型檢查 (`null` 和 `undefined` 會通過任何類型驗證)
    propA: Number,
    // 多個可能的類型
    propB: [String, Number],
    // 必填的字符串
    propC: {
      type: String,
      required: true
    },
    // 帶有默認值的數字
    propD: {
      type: Number,
      default: 100
    },
    // 帶有默認值的對象
    propE: {
      type: Object,
      // 對象或數組默認值必須從一個工廠函數獲取
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定義驗證函數
    propF: {
      validator: function (value) {
        // 這個值必須匹配下列字符串中的一個
        return ['success', 'warning', 'danger'].indexOf(value) !== -1
      }
    }
  }

如果有一個需求沒有被滿足,則 Vue 會在瀏覽器控制檯中警告你。這在開發一個會被別人用到的組件時尤其有幫助。

注:

  • 所有的 prop 都使得其父子 prop 之間形成了一個單向下行綁定:父級 prop 的更新會向下流動到子組件中,但是反過來則不行。這樣會防止從子組件意外改變父級組件的狀態,從而導致你的應用的數據流向難以理解。
  • 注意那些 prop 會在一個組件實例創建之前進行驗證,所以實例的屬性 (如 data、computed 等) 在 default 或 validator 函數中是不可用的。
  • 因爲組件不能總是預見其他組件在調用當前組件時要傳入的參數,所以 vue 也允許組件在被調用時傳入一些非 props 特性。這些特性會自動添加到組件的根元素上。

二、$emit

一個子組件被一個父組件調用,當子組件觸發某個函數時,需要父組件響應,就要使用到 $emit

1. $emit的簡單示例

<!-- 子組件 -->
<template>
  <div class="child">
    <button @click="ev">子組件中的按鈕</button>
  </div>
</template>
<script>
  export default {
      name: 'Child',
      data() {
          return {
              msg: 'I am a data from child component'
          }
      },
      methods: {
          ev() {
              // 當前組件的按鈕被點擊時,向外拋出一個事件,誰調用當前組件,誰就在當前組件的按鈕被點擊時響應這個事件
              this.$emit('clickBtn');
          }
      }
  }
</script>
<!-- 父組件 -->
<template>
  <div id="app">
    <!-- 父組件在調用子組件時綁定子組件中事件的響應程序 -->
    <Child v-on:clickBtn="evInFather" />
  </div>
</template>

<script>
  import Child from './components/Child'

  export default {
    name: 'App',
    components: {
        Child
    },
    data() {
      return {
        flag: true
      }
    },
    methods: {
       //當Child子組件中的 clickBtn 事件被emit時,evInFather 也會觸發    
       evInFather() {
          console.log('father up');
       }
    }
  }
</script>

我們也可以在拋出事件時,同時向外界拋出一些數據。

// Child的methods
// ...
methods: {
    ev() {
        // 向外拋出數據
        this.$emit('clickBtn', this.msg);
    }
}
// ...
// 父組件中的methods
// ...
methods: {
    evInFather(data) {
        console.log(data);
    }
}
// ...

如果組件在拋出事件時拋出的數據不止一個,可以以參數列表的形式繼續向 $emit 傳值,然後在父組件中再以參數列表的形式調用。但是在數據很多的情況下會讓代碼顯得和冗長,所以我們建議將所有要拋出的數據整合在一個對象中,向 $emit 函數的第二個參數傳入一個對象。

slot 插槽

插槽是組件被調用的第二種方式,更好的提高了組件的複用性。

子組件在調用父組件並向其傳值時,更偏向於數據層的傳遞,但是有時候我們希望子組件在結構渲染層也能更好的響應其某個父組件,可以通過父組件的數據控制讓子組件中的內容渲染。

如我們需要一個操作結果提示框,彈框中的標題,圖片和按鈕欄全部根據操作結果渲染具體結果。使用 props 或者 $emit 也能實現這樣的效果,但是如果多種渲染結果的差異很大,那代碼冗餘就又上去了,所以 Vue 提供了一種更簡潔的方式,就是插槽。

插槽相當於將組件封裝成一個大的框架,至於具體顯示什麼,怎麼顯示,可以在調用時傳數據,通過數據控制,也可以在調用時傳入內容,顯示傳入的內容。

1. 插槽的小示例

<!-- Alert.vue -->
<template>
    <div class="alert">
        <p>我是組件中本來就有的內容</p>
        <!-- 在組件中用slot預留即將被分發的內容,組件被調用時,分發的內容會被渲染在slot所在的位置 -->
        <slot></slot>
    </div>
</template>

<script>
    export default {
        name: "Alert"
    }
</script>
<template>
  <div id="app">   
     <Alert>
        <!-- 調用alert組件並向其中分發內容 -->
        <p>
            hello,我是father調用時分發給子組件的內容
        </p>
     </Alert>
  </div>
</template>
<script>
    // ...
</script>

注:
如果 Alert 組件中沒有包含一個 <slot> 元素,則該組件被調用時起始標籤和結束標籤之間的任何內容都會被拋棄。

2. 爲組件設置插槽的後備內容

如果一個組件中預留了插槽,但是在組件被調用時並沒有爲其分發內容,可能會導致結構、邏輯出現一些無法預知的錯誤,所以有時候爲插槽設置一個默認內容是很有必要的。

<!-- Alert.vue -->
<div class="alert">
    <slot>
        <!-- 當前組件調用時,如果不分發內容,那就展示下面p標籤中的內容 -->
        <p>default message</p>
    </slot>
</div>

3. 具名插槽

要想封裝一個具有高複用性的組件,一個插槽可能還不夠,我們可以在組件結構的不同位置爲其預留多個插槽,爲每個插槽命名,在調用組建時,向不同的插槽分發對應的內容。這就是具名插槽。

<!-- Alert.vue -->
<div class="alert">
    <div class="title">
        <!-- 具名插槽 title -->
        <slot name="title">溫馨提示</slot>
    </div>
    <div class="content">
        <!-- 插槽 con -->
        <slot name="con">您確定要做這樣的操作嗎</slot>
    </div>
    <div class="btn">
        <!-- 插槽 btn -->
        <slot name="btn">
            <button>確定</button>
        </slot>
    </div>
</div>

調用 Alert 組件:

<Alert>
    <!-- 向不同的具名插槽分發對應的內容 -->
    <template v-slot:title>
        提示
    </template>
    <template v-slot:con>
        <p>這個操作很危險,是否要進行?</p>
    </template>
    <template v-slot:btn>
        <button>確定</button>
        <button>取消</button>
    </template>
</Alert>

具名插槽的簡寫:

<template #btn></template>

注:

  • 沒有設置 name 屬性的 <slot> ,默認 name 值是 default。
  • v-slot 屬性只能添加在一個 <template> 上(除了獨佔默認插槽情況)。

4. 作用域插槽

有時我們需要在調用組件並向組件的插槽分發內容時,訪問組件內部的數據。可以將組件中的數據綁定成 插槽 prop,然後就可以在組件被調用時,同時訪問插槽 prop 上的數據。

<!-- Alert.vue -->
<template>
    <div class="Alert">
        <!-- 這個插槽中的數據是有關 user 的,父組件在調用時可能想要訪問 user 中的其他數據,所以可以將user綁定成插槽 prop -->
        <slot name="userMsg" v-bind:user="user">{{ user.name }}</slot>
    </div>
</template>

<script>
    export default {
        name: "Alert",
        data() {
          return {
            user: {
                name: 'tom',
                age: 18
            }
          }
        }
    }
</script>

調用插槽並使用插槽 prop 的值:

<Alert>
    <!-- 通過v-slot指令接受插槽 prop -->
    <template v-slot:userMsg="um">
        <!-- 調用插槽 prop 的數據 -->
        <i>{{ um.user.name }}</i>
        今年
        <mark>{{ um.user.age }}</mark>
        歲啦
    </template>
</Alert>

可以爲插槽設置多個插槽 prop:

<slot name="userMsg" 
    v-bind:user="user"
    v-bing:num="100000"
>
    {{ user.name }}
</slot>

接收:

<template v-slot:userMsg="um">
    <i>{{ um.user.name }}</i>
    今年
    <mark>{{ um.user.age }}</mark>
    歲啦
    <strong>{{ um.num }}</strong>
</template>

插槽 prop 也可以被解構使用:

<template v-slot:userMsg="{user}"></template>

注:
如果插槽 prop 很多,建議將所有數據整合成一個對象,傳遞一個插槽 prop 即可。

Render 函數,JSX 語法和 CreateElement 函數

render 函數 跟 template 一樣都是創建 html 模板的,但是有些場景中用 template 實現起來代碼冗長繁瑣而且有大量重複,這時候就可以用 render 函數。

如果在組件中的使用 render 函數渲染,那就可以不使用 <template> 標籤。組件文件中只需要 <script> 標籤和 <style> 標籤。

在瞭解 render 函數之前,我們要先明確一個 Vnode 的概念。

1. Vnode(虛擬節點)

在使用 Vue 開發項目時,渲染在瀏覽器上的結構是 Vue 通過底層機制的各種條件、循環、計算等操作之後最終渲染在瀏覽器上的,我們把瀏覽器上渲染的最終結果看做真實的 DOM 樹。

但是 Vue 對於數據的響應很高效,面對這樣高效的數據響應,也要同樣高效的更新頁面中的節點,但是 DOM 結構非常龐大且複雜,要完成所有 DOM 的更新特別困難。

比如我們渲染了一個商品管理列表,當某個商品的某個值發生變化時,頁面的列表渲染也要更新,如果要使用原生JS去渲染,我們可能需要重新渲染整個表格,或者某一行。如果要精確地定位到某個單元格,對於代碼的要求很高。

好在使用 Vue 時,我們不用手動的使用JS去更新DOM樹,Vue 提供了一棵虛擬 DOM 樹,它通過這個虛擬 DOM樹 來追蹤自己要如何改變真實 DOM。

我們在 vue 文件中寫的 DOM 節點和 render 函數中的 DOM 都是虛擬 DOM,瀏覽器渲染時,會對所有的虛擬 DOM 進行計算,最終渲染在瀏覽器上。

我們在創建虛擬 DOM 時,包含了虛擬 DOM 的所有信息,如子元素,類名,樣式,位置等等。

Vue 實例提供了 render 函數來渲染虛擬 DOM,render 函數的參數(也是一個函數)來創建虛擬DOM。

2. render 函數示例

<script>
    export default {
        name: "Home",
        data() {
          return {
          }
        },
        render(ce) {
            return ce(
                'div'
            )
        }
    }
</script>

<Home> 組件可以被正常調用,調用時拿到是 render 函數渲染出來的這個 <div>

render 函數:

  • 參數是 ce 函數(很多地方會將其寫成createElement)。
  • 返回值是一個 VNode (即將要渲染的節點)。

3. render函數的參數-createElement函數

render 函數的參數就是 createElement 函數,它創建 VNode ,作爲 render 的返回值。

createElement 函數接受三個參數:

  • 參數一:
    String | Object | Function
    一個 HTML 標籤名、組件選項對象,或者 resolve 了上述任何一種的一個 async 函數。必填項。
  • 參數二:
    Object
    一個包含這個標籤(或模板)所有相關屬性的對象。可選。
  • 參數三:
    String | Array
    createElement 創建的子節點或列表。

下面是 createElement 第二個參數和第三個參數的詳細介紹及示例:

// 參數二
{
    // 與 `v-bind:class` 的 API 相同,
    // 接受一個字符串、對象或字符串和對象組成的數組
    'class': {
        foo: true,
        bar: false
    },
    // 與 `v-bind:style` 的 API 相同,
    // 接受一個字符串、對象,或對象組成的數組
    style: {
        color: 'red',
        fontSize: '14px'
    },
    // 普通的 HTML 特性
    attrs: {
        id: 'foo'
    },
    // 組件 prop
    props: {
        myProp: 'bar'
    },
    // DOM 屬性
    domProps: {
        innerHTML: 'baz'
    },
    // 事件監聽器在 `on` 屬性內,
    // 但不再支持如 `v-on:keyup.enter` 這樣的修飾器。
    // 需要在處理函數中手動檢查 keyCode。
    on: {
        click: this.clickHandler
    },
    // 僅用於組件,用於監聽原生事件,而不是組件內部使用
    // `vm.$emit` 觸發的事件。
    nativeOn: {
        click: this.nativeClickHandler
    },
    // 自定義指令。注意,你無法對 `binding` 中的 `oldValue`
    // 賦值,因爲 Vue 已經自動爲你進行了同步。
    directives: [
        {
        name: 'my-custom-directive',
        value: '2',
        expression: '1 + 1',
        arg: 'foo',
        modifiers: {
            bar: true
        }
        }
    ],
    // 作用域插槽的格式爲
    // { name: props => VNode | Array<VNode> }
    scopedSlots: {
        default: props => createElement('span', props.text)
    },
    // 如果組件是其它組件的子組件,需爲插槽指定名稱
    slot: 'name-of-slot',
    // 其它特殊頂層屬性
    key: 'myKey',
    ref: 'myRef',
    // 如果你在渲染函數中給多個元素都應用了相同的 ref 名,
    // 那麼 `$refs.myRef` 會變成一個數組。
    refInFor: true
}
// 參數三:
// 如果參數三是一個字符串,會被渲染成元素的innerText
render(ce) {
    return ce(
        'div',
        {},
        '<p>hello world</p>'
    )
}
// 如果參數三是一個數組,那麼裏面就是元素的子內容列表,每個內容都是由createElement 創建的 VNode
render(ce) {
    return ce(
        'div',
        {},
        [ce('p'),ce('h4')]
    )
}
// 但是如果在參數三的數組中,羅列了字符串,那麼這些字符串會被拼接在一起作爲元素的innerText
render(ce) {
    return ce(
        'div',
        {},
        ['hello','world']
    )
}

4. JSX語法

如果一個模板中的結構較爲簡單,我們使用 createElement 函數就可以了,但是一旦結構稍稍複雜一點點,代碼就會變得特別冗長。

如果想繼續使用 render函數,就要使用到 JSX 語法。因爲JSX 語法允許在JS代碼中書寫HTML結構

render(ce) {
    return (
        <div class="home">
            這裏面可以任意寫結構
        </div>
    );
}

JSX 語法在 React 中的使用可能會更廣泛一點。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章