這段時間從新看了下vue的文檔,發現還有不少使用使用頻率不是那麼高,或者簡單使用過但不那麼清晰的知識點。今天咱們就來看一下其中的渲染函數render,jsx語法和插槽slot的用法。javascript
熟悉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中的渲染函數render
。vue
先來看下如何使用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
相信寫過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-for
和v-model
,須要咱們本身實現。
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函數中使用事件/按鍵修飾符比較簡單,能夠去官方文檔查看。
若是咱們所需的組件比較簡單,沒有管理任何狀態,也沒有監放任何傳遞給它的狀態,也沒有生命週期方法。實際上,它只是一個接受一些 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
得到的:
在改爲函數式組件以後,須要修改一下咱們組件的渲染函數,爲其增長context參數,而且若是有this.$slots.default
要改成context.children
,而後將this.level
要改成context.props.level
等。
上面咱們在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 的函數。
以上就是此次我要介紹的內容了,雖然比較基礎,可是基本用法都有涉及到,但願對你們以後的開發或者面試都能有所幫助。
做者簡介: 宮晨光,人和將來大數據前端工程師。