淺析VUE里的插槽(內容分發)及無渲染組件


一、插槽

  插槽,也就是slot,是組件的一塊HTML模板,這塊模板顯示不顯示、以及怎樣顯示由父組件來決定。實際上,一個slot最核心的兩個問題這里就點出來了,是顯示不顯示怎樣顯示

  Vue中使用slot的一個重要原因,就是為了達到組件的復用,子組件的某些元素直接由調用他的父組件決定。

  由於插槽是一塊模板,所以,對於任何一個組件,從模板種類的角度來分,其實都可以分為非插槽模板插槽模板兩大類。

  非插槽模板指的是html模板,指的是‘div、span、ul、table’這些,非插槽模板的顯示與隱藏以及怎樣顯示由插件自身控制;

  插槽模板是slot,它是一個空殼子,因為它顯示與隱藏以及最后用什么樣的html模板顯示由父組件控制。但是插槽顯示的位置確由子組件自身決定,slot寫在組件template的哪塊,父組件傳過來的模板將來就顯示在哪塊

1、單個插槽 | 默認插槽 | 匿名插槽

  首先是單個插槽,單個插槽是vue的官方叫法,但是其實也可以叫它默認插槽,或者與具名插槽相對,我們可以叫它匿名插槽,因為它不用設置name屬性。

  單個插槽可以放置在組件的任意位置,但是就像它的名字一樣,一個組件中只能有一個該類插槽。相對應的,具名插槽就可以有很多個,只要名字(name屬性)不同就可以了。

  直接拿官網的例子來展示。

// 子組件 <navigation-link>
<a v-bind:href="url" class="nav-link">
  <slot></slot>
</a>

// 父組件調用
<navigation-link url="/profile">
  <!-- 添加一個 Font Awesome 圖標 -->
  <span class="fa fa-user"></span> Your Profile </navigation-link>

// 最終會渲染為
<a v-bind:href="url" class="nav-link">
  <!-- 添加一個 Font Awesome 圖標 -->
  <span class="fa fa-user"></span> Your Profile </a>

2、具名插槽

  具名插槽其實就是指定了名字的插槽,代碼會被渲染到指定的位置。具名插槽可以在一個組件中出現N次,出現在不同的位置。

  繼續上面的例子:子組件指定具名插槽和默認插槽,則父節點中slot的元素會按位置渲染。

// 子組件
<template>
  <a v-bind:href="url" class="nav-link">
    <slot name="up"></slot>
    <p>我是分割線</p>
    <slot></slot>
  </a>
</template>
<script> export default { props: { url: { type: String } }, data() {    return { specData: '我的內容來自子節點'}; } }; </script>
// 父組件
<template>
  <navigation-link url="/profile">
    <!-- 添加一個 Font Awesome 圖標 -->
    <span class="fa fa-user"></span> Your Profile <span>{{specData}}</span>

    <p slot="up">
      <span>我是up</span>
    </p>

  </navigation-link>
</template>

<script> import navigationLink from './child.vue'; export default { created(){ }, data() { return { specData: '我必須由父節點來傳遞,我的內容來自父節點};
 }, components: { navigationLink }, } </script>
// 最終渲染為: <a href="/profile" class="nav-link">
  <p>
    <span>我是up</span>
  </p>
  <p>我是分割線</p>
  <span class="fa fa-user"></span> Your Profile <span>我必須由父節點來傳遞,我的內容來自父節點</span>
</a>

  可見,多了一個name,其實我們就可以定制顯示的位置,和默認的匿名插槽混用也不會有影響,這個在需要渲染多個插槽時十分有效。

3、Vue的編譯作用域

  在提到作用域插槽之前,必須確保已經了解Vue的編譯作用域。

  父組件模板的所有東西都會在父級作用域內編譯;子組件模板的所有東西都會在子級作用域內編譯。

  其實仔細看上面的例子,父子組件都賦值了specData,但是渲染的是父組件的內容便了解了,其實寫錯了也沒事,Vue會給你及時提示的。

4、作用域插槽 | 帶數據的插槽

  最后,就是我們的作用域插槽,這個稍微難理解一點,官方叫它作用域插槽,實際上,對比前面兩種插槽,我們可以叫它帶數據的插槽。

  作用域插槽就是子組件通過給slot上綁定參數,達到給父組件傳遞進來的模板賦值的效果

  我們前面說了,插槽最后顯示不顯示是看父組件有沒有在child下面寫模板,像下面那樣。

<child> html模板 </child>

  寫了,插槽就總得在瀏覽器上顯示點東西,東西就是html該有的模樣,沒寫,插槽就是空殼子,啥都沒有。

  我們再來對比,作用域插槽和單個插槽和具名插槽的區別,因為單個插槽和具名插槽不綁定數據,所以父組件是提供的模板要既包括樣式又包括內容和數據,而作用域插槽,父組件只需要提供一套樣式(在確實用作用域插槽綁定的數據的前提下),數據就是使用子組件傳遞過來的數據。

// 父組件 <template>
  <div class="father">
    <h3>這里是父組件</h3>
    <!--第一次使用:直接顯示數據-->
    <child>
      <template slot-scope="user"> {{user.data}} </template>

    </child>

    <!--第二次使用:不使用其提供的數據, 作用域插槽退變成匿名插槽-->
    <child> 我就是模板 </child>
  </div>
</template> // 子組件 <template>
  <div class="child">
    <h3>這里是子組件</h3> // 作用域插槽 <slot :data="data"></slot>
  </div>
</template> export default { data: function(){ return { data: ['zhangsan','lisi','wanwu','zhaoliu','tianqi','xiaoba'] } } }

  其中slot-scope=user其實是一個對象,里面包含的屬性是子組件中傳給<slot>的prop,在上面的例子中就是user=={data: ['zhangsan','lisi','wanwu','zhaoliu','tianqi','xiaoba']},所以可以通過user.data獲取到子組件的data。

二、Vue組件本質

1、VUE組件的本質:Vue 及其所有組件都只不過是 JavaScript。

  Vue 提供了很多種方法用於定義組件的標記:

(1)單文件組件讓我們可以像普通的 HTML 文件一樣定義組件及其標記。

(2)template 屬性讓我們可以使用 JavaScript 的模板字面量來定義組件的標記。

(3)el 屬性讓 Vue 通過查詢 DOM 來獲取用作模板的標記。

  觀察下面這個單文件組件,相信很多人都寫過類似的代碼。

<template>
  <div class="mood"> {{ todayIsSunny ? 'Makes me happy' : 'Eh! Doesn't bother me' }}
  </div>
</template>
<script> export default { data: () => ({ todayIsSunny: true }) } </script>

  Vue 試圖簡化樣式和其他資源的管理,只是 Vue 並沒有直接做這些事情,而是把這些工作留給了構建過程,比如 webpack。

  webpack 在處理 .vue 文件時,會對其執行一個轉換。在轉換過程中,CSS 被從組件中提取出來,並放到單獨的文件中,剩余部分則被轉換為 JavaScript。例如:

export default { template: ` <div class="mood"> {{ todayIsSunny ? 'Makes me happy' : 'Eh! Doesn't bother me' }}
    </div>`, data: () => ({ todayIsSunny: true }) }

2、模板編譯器和渲染函數

  當模板編輯器遇到上面這段代碼時,它將 template 屬性提取出來,並將它的內容編譯為 JavaScript,然后將一個渲染函數添加到組件對象中。這個渲染函數將返回已轉換為 JavaScript 的 template 屬性的內容。

  對JSX熟悉的人對這個代碼就十分眼熟了。Vue提供了方法可以自己寫渲染函數,有關渲染函數的更多信息,請參閱官方文檔 https://vuejs.org/v2/guide/render-function.html。

render(h) { return h( 'div', { class: 'mood' }, this.todayIsSunny ? 'Makes me happy' : 'Eh! Doesn't bother me'  ) }
  現在當組件對象被傳給 Vue 時,組件的渲染函數會經過一些優化並變成 VNode(虛擬節點),然后 VNode 被傳給 snabbdom(Vue 內部用於管理虛擬 DOM 的庫)。Vue 通過 VNode 的方式來渲染組件。

三、無渲染組件

  無渲染組件是指不渲染任何內容的組件。

  我們可以這樣理解無渲染組件:為一個組件創建通用功能抽象,然后通過擴展這個組件來創建更好更健壯的組件,或者說,遵循 S.O.L.I.D 原則。

1、根據 S.O.L.I.D 的單一責任原則:一個類應該只有一個用途。

  我們將這個概念移植到 Vue 開發中,讓每個組件只提供一個用途。

  例如我們實現一個評論組件,當另一個需求過來,修改了樣式,修改了交互時,就不得不去修改組件代碼來實現這個需求。而我們之前做的和接口的通信方式,和客戶端的通信方式,可能又需要重新copy一份。

  這打破了 S.O.L.I.D 的開放封閉原則,這個原則規定:類或組件應該為擴展而開放,為修改而封閉。

  也就是說,你應該擴展它,而不是直接修改組件的源代碼。

  Vue 遵循了 S.O.L.I.D. 原則,讓組件擁有 prop、event、slot 和 scoped slot,這些東西讓組件的交互和擴展變得輕而易舉。我們可以構建具備所有特性的組件,而無需修改任何樣式或標記。這對於可重用性和高效代碼來說非常重要。

2、在設計時需要仔細思考的是

(1)表現和行為分離

  由於無渲染組件只處理狀態和行為,所以它們不會對設計或布局強加任何的決策。這意味着,如果你能夠找到一種方法,將所有的行為從UI組件中移出,將將其轉換成一個無組件的組件,那么你就可以重用無渲染組件來實現任何布局效果的標簽輸入控件。

(2)簡單的一個toogle組件

  這個組件完成一個簡單的功能,傳遞給子組件一個狀態on (也可以不傳,例子中就沒傳),通過調用子組件暴露出的方法,達到利用子組件的狀態,影響父組件的渲染的目的。這樣,父組件的結構可以隨便改,樣式隨便讓產品設計去折騰,我們的基本功能是不變的,達到一定層度的代碼復用。

  父組件:注意slot-scope不能直接用在子組件標簽<toogle>

<template>
  <toogle>
    <div slot-scope="{ on, setOn, setOff }" class="container">
      <button @click="click(setOn)" class="button">Blue pill</button>
      <button @click="click(setOff)" class="button isRed">Red pill</button>
      <div v-if="buttonPressed" class="message">
        <span v-if="on">It's all a dream, go back to sleep.</span>
        <span v-else>I don't know how far the rabbit hole goes, I'm not a rabbit, neither do I measure holes.</span>
      </div>
    </div>
  </toogle>
</template>
<script> import toogle from "./toogle"; export default { data() { return { buttonPressed: false }; }, components: { toogle }, methods: { click(fn) { this.buttonPressed = true; fn && fn(); } } }; </script>
// 子組件
<template>
  <div>
    <slot :on="currentState" :setOn="setOn" :setOff="setOff" :toogle="toggle"></slot>
  </div>
</template>
<script> export default { props: { on: { type: Boolean, default: false } }, data() { return { currentState: this.on }; }, methods: { setOn() { this.currentState = true; }, setOff() { this.currentState = false; }, toggle() { this.currentState = !this.currentState; } } }; </script>

  如果要用render函數,子組件可以省去template部分,在script部分添加

render: function(createElement) { return createElement("div", [ this.$scopedSlots.default({ on: this.currentState, setOn: this.setOn, setOff: this.setOff, toggle: this.toggle }) ]); },

參考鏈接:https://www.jianshu.com/p/50dcf932a159


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM