vue中的渲染函數/jsx和插槽slot

1. 前言

這段時間從新看了下vue的文檔,發現還有不少使用使用頻率不是那麼高,或者簡單使用過但不那麼清晰的知識點。今天咱們就來看一下其中的渲染函數render,jsx語法和插槽slot的用法。javascript

2. 模板語法的弊端

熟悉vue單文件組件寫法的同窗們都知道,vue文件的html部分是由<template></template>組成,這種方法使用起來比較簡單,配合vue指令能夠實現大多數狀況下的需求。不過仍是存在模板語法不方便的時候,好比須要開發一個組件,這個組件要根據父組件傳過來的值來選擇渲染的html標籤,來看一個示例:html

// hLabel.vue
<template>
  <h1 v-if="level === 1">
    <slot></slot>
  </h1>
  <h2 v-else-if="level === 2">
    <slot></slot>
  </h2>
  <h3 v-else-if="level === 3">
    <slot></slot>
  </h3>
  <h4 v-else-if="level === 4">
    <slot></slot>
  </h4>
  <h5 v-else-if="level === 5">
    <slot></slot>
  </h5>
  <h6 v-else-if="level === 6">
    <slot></slot>
  </h6>
</template>
<script>
export default{
  props: {
    level:{
      type: Number
    }
  }
}
</script>
複製代碼

上面這個組件雖然可以實現根據level值來渲染對應的<h1>,<h2>...標籤,可是冗餘代碼也不少,並且在每一個級別的標題標籤中都有一個<slot>標籤。前端

爲了解決這個問題,咱們須要用到vue中的渲染函數rendervue

3. 渲染函數render

先來看下如何使用render函數來實現上面要求的組件:java

// hLabel.vue
<script>
export default {
  props: {
    level: {
      type: Number,
    }
  },
  render: function(createElement){
    return createElement(
      'h' + this.level, // 標籤名稱,根據父組件傳入的level值肯定
      this.$slots.default // 子節點數組
    )
  }
}
</script>
複製代碼

上面的代碼十分精簡,經過render函數就能夠渲染一個標籤模板。同時若是此時須要向組件中傳遞原來<slot>接收的內容,這時候要使用$slots.default,關於slot的用法咱們後面會專門說起。react

vue給render函數提供了一個參數createElement,這個參數也是一個函數方法,接受必定的參數,返回的是虛擬DOM(Virtual Dom) VNode,並且在vue中咱們通常約定能夠把createElement簡寫爲h。下面來看下createElement的用法:面試

render: function(createElement){
  // @returns {VNode}
  createElement(
    // {String | Object | Function}
    // 一個 HTML 標籤名、組件選項對象,或者
    // resolve 了上述任何一種的一個 async 函數。必填項。
    'div',

    // {Object}
    // 一個與模板中屬性對應的數據對象。可選。
    {
      // 主要是html模板標籤中的屬性值的寫法,下面單獨介紹
    },

    // {String | Array}
    // 子級虛擬節點 (VNodes),由 `createElement()` 構建而成,
    // 也可使用字符串來生成「文本虛擬節點」。可選。
    [
      '先寫一些文字', // 若是是字符串,表示是生成標籤中的內容
      createElement('h1', '一則頭條'), // createElement生成的新VNode
      createElement(MyComponent, {
        props: {
          someProp: 'foobar'
        }
      })
    ]
  )
}
複製代碼

來單獨看下createElement函數中,模板中屬性的寫法:vue-cli

{
  // 與 `v-bind:class` 的 API 相同,
  // 接受一個字符串、對象或字符串和對象組成的數組
  'class': {
    foo: true,
    bar: false
  },
  // 與 `v-bind:style` 的 API 相同,
  // 接受一個字符串、對象,或對象組成的數組
  style: {
    color: 'red',
    fontSize: '14px'
  },
  // 普通的 HTML 特性
  attrs: {
    id: 'foo'
  },
  // 組件 prop,這個屬性是當createElement渲染的是一個組件時使用
  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
}
複製代碼

經過上面的示例,咱們能夠看到正如 v-bind:class 和 v-bind:style 在模板語法中會被特別對待同樣,它們在 VNode 數據對象中也有對應的頂層字段。該對象也容許你綁定普通的 HTML 特性,也容許綁定如 innerHTML 這樣的 DOM 屬性 (這會覆蓋 v-html 指令)。express

相信說到這裏你們確定會以爲render的使用方法太麻煩了,若是須要寫一個稍微複雜點的html模版,那個人render函數要寫到死了,因此天然就引出了jsx的使用。api

4. jsx語法

相信寫過react的人對這種語法確定不會陌生。經過babel插件的支持,在vue的render函數中也能夠直接使用jsx語法。若是你使用的是vue-cli 3.x建立的項目,那麼不須要任何配置,直接就把jsx用起來吧。

// hLabel.vue
<script>
export default{
  props: {
    level: {
      type: Number,
    }
  },
  methods: {
    clickHandler(){

    },
    nativeClickHandler(){

    }
  },
  render:function(h) { // createElement約定可簡寫爲h
    let tag = `h${this.level}`
    return (
      <tag
        key="key"
        ref="ref"
        id='title'
        class={{'foo':true,, 'bar':false}}
        style={{margin: '10px', color:'red'}}
        onClick={this.clickHandler}
        nativeOnClick={this.nativeClickHandler} // 監聽組件內的原生事件
      >{this.$slots.default}
      </tag>
    )
  }
}
</script>
複製代碼

上面的例子給出了jsx語法和在標籤上添加屬性的一個簡單示例。不過若是咱們使用了render函數以後vue中自帶的一些指令就不在生效了,包括v-if,v-forv-model,須要咱們本身實現。

5. render函數中vue指令的實現

v-if和v-for:

<ul v-if="items.length">
  <li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>
複製代碼
// 在渲染函數中須要使用 if/else 和 map 來重寫
props: ['items'],
render: function (h) {
  if (this.items.length) {
    return h('ul', this.items.map(function (item) {
      return h('li', item.name)
    }))
  } else {
    return h('p', 'No items found.')
  }
}
複製代碼

v-model:

props: ['value'],
render: function (createElement) {
  var self = this
  return createElement('input', {
    domProps: {
      value: self.value
    },
    on: {
      input: function (event) {
        self.$emit('input', event.target.value)
      }
    }
  })
}
複製代碼

其實上面的代碼就是vue中v-model指令雙向綁定的原理,只是v-model對不一樣的綁定元素作了兼容處理。同時v-model也是能夠綁定在組件上的,具體用法能夠點擊這裏查看

同時在vue中綁定事件時,事件和按鍵修飾符也不能使用了,由於這些事件修飾符都是vue替咱們作了處理的語法糖。關於如何在render函數中使用事件/按鍵修飾符比較簡單,能夠去官方文檔查看。

6. 函數式組件

若是咱們所需的組件比較簡單,沒有管理任何狀態,也沒有監放任何傳遞給它的狀態,也沒有生命週期方法。實際上,它只是一個接受一些 prop 的函數。在這樣的場景下,咱們能夠將組件標記爲functional,這意味它無狀態 (沒有響應式數據),也沒有實例 (沒有 this 上下文)。

一個函數式組件就像這樣:

<script>
export default{
  functional: true, // 添加屬性functional: true,表示該組件爲函數式組件
  // Props 是可選的
  props: {
    // ...
  },
  // 爲了彌補缺乏的實例
  // 提供第二個參數做爲上下文
  render: function (createElement, context) {
    // ...
  },
}
</script>
複製代碼

在2.5.0及以上版本的單文件組件,那麼基於模板的函數式組件能夠這樣聲明:

<template functional>
</template>
複製代碼

由於函數式組件是無狀態的,也沒有this上下文,沒有data等屬性,因此若是所須要的數據都是由render函數的第二個參數context得到的:

  • props:提供全部 prop 的對象
  • data:傳遞給組件的整個數據對象,做爲 createElement 的第二個參數傳入組件
  • children: VNode 子節點的數組
  • parent:對父組件的引用
  • slots: 一個函數,返回了包含全部插槽的對象
  • scopedSlots: (2.6.0+) 一個暴露傳入的做用域插槽的對象。也以函數形式暴露普通插槽。
  • listeners: (2.3.0+) 一個包含了全部父組件爲當前組件註冊的事件監聽器的對象。這是 data.on 的一個別名。
  • injections: (2.3.0+) 若是使用了 inject 選項,則該對象包含了應當被注入的屬性。

在改爲函數式組件以後,須要修改一下咱們組件的渲染函數,爲其增長context參數,而且若是有this.$slots.default 要改成context.children,而後將this.level 要改成context.props.level等。

7. 插槽, slots 和scopedSlots

上面咱們在render函數中,反覆看到插槽slot的使用。因此此次也來順便看下slot究竟是什麼的東西。

插槽內容:

Vue 實現了一套內容分發的 API,將 <slot> 元素做爲承載分發內容的出口,便可以將在組件內填寫的內容渲染在子組件的slot之間。

<!-- 父組件 -->
<navigation-link url="/profile">
  Your Profile
</navigation-link>
<!-- navigation-link組件 -->
<a v-bind:href="url" class="nav-link" >
  <slot></slot>
</a>
複製代碼

當組件渲染的時候,<slot></slot> 將會被替換爲「Your Profile」。

插槽內能夠包含任何模板代碼,包括 HTML或者其餘組件:

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

<!-- 插槽內爲組件 -->
<navigation-link url="/profile">
  <!-- 添加一個圖標的組件 -->
  <font-awesome-icon name="user"></font-awesome-icon>
  Your Profile
</navigation-link>
複製代碼

編譯做用域:

編譯做用域是指在引用組件內部寫的內容和子組件內部的內容,所能獲取的都只能是其當前做用域下的值。有一個原則是**父級模板裏的全部內容都是在父級做用域中編譯的;子模板裏的全部內容都是在子做用域中編譯的。**結合代碼來看:

<!-- 若是想在插槽中使用數據user -->
<!-- user必須是navigation-link組件坐在的做用域能夠訪問到的值 -->
<navigation-link url="/profile">
  Logged in as {{ user.name }}
</navigation-link>
複製代碼

下面是個訪問不到錯誤例子:

<!-- 這裏是訪問不到url的 -->
<!-- 由於當前的url值"/profile"是在navigation-link組件內部定義的 -->
<!-- 在navigation-link組件所在的做用域,是訪問不到url的 -->
<navigation-link url="/profile">
  Clicking here will send you to: {{ url }}
</navigation-link>
複製代碼

後備內容和具名插槽:

直接看後備內容的示例代碼:

<!-- 父組件 -->
<submit-button>
  Save
</submit-button>

<!-- submit-button組件 -->
<button type="submit">
  <slot>Submit</slot>
</button>
複製代碼

後備內容就是說我在slot之間也寫入內容做爲後備內容,當若是在父組件內使用submit-button且之間有內容時,會優先顯示這個值。若是submit-button之間沒有內容時,則會顯示slot之間的後備內容。

<!-- 父組件 -->
<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>
<!-- base-layout 組件 -->
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>
複製代碼

上面是具名插槽的用法示例。若是須要指定多個插槽的渲染內容,能夠給slot添加name屬性,同時在向插槽提供內容的時候,可使用template包裹住內容,並且在template之上寫入v-slot指定,而且以參數的形式在v-slot上提供要渲染的插槽的名稱。這樣template的內容就能夠渲染到指定name的slot以內。template上的v-slot: name能夠簡寫爲#name。若是template沒有指定名稱的話,默認name爲default。

做用域插槽:

上面咱們說到,插槽是有做用域的,父級模板裏的內容只能訪問到父級模板的做用域,子級組件內的內容只能在子級的做用域內渲染。假如我想在父級模板內使用子級組件內的值如何實現呢,這個時候就須要用到做用於插槽,:

<!-- 錯誤示範 -->
<!-- 父組件想要使用current-user組件內的值user -->
<current-user>
  {{ user.firstName }}
</current-user>

<!-- current-user組件 -->
<span>
  <slot>{{ user.lastName }}</slot>
</span>

<!-- 正確示範 -->
<current-user>
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>
</current-user>
<span>
  <slot v-bind:user="user">
    {{ user.lastName }}
  </slot>
</span>
複製代碼

$slots:

vue中用來訪問被插槽分發的內容的api,至關於模板中的<slot></slot>。每一個具名插槽有其相應的屬性 (例如:v-slot:foo 中的內容將會在 vm.$slots.foo 中被找到)。default 屬性包括了全部沒有被包含在具名插槽中的節點,或 v-slot:default 的內容。

$scopedSlots:

用來訪問做用域插槽,至關能夠給<slot>提供值的插槽做用域。對於包括默認 slot 在內的每個插槽,該對象都包含一個返回相應 VNode 的函數。

8. 總結

以上就是此次我要介紹的內容了,雖然比較基礎,可是基本用法都有涉及到,但願對你們以後的開發或者面試都能有所幫助。

9. 參考文章

vue官方文檔:渲染函數&jsx

vue官方文檔:插槽

vue官方api

在vue中使用jsx語法

vue jsx 不徹底指北


做者簡介: 宮晨光,人和將來大數據前端工程師。

相關文章
相關標籤/搜索