Vue 推薦在絕大多數情況下使用模板來創建你的 HTML。然而在一些場景中,你真的需要 JavaScript 的完全編程的能力。這時你可以用渲染函數,它比模板更接近編譯器。
瞭解render函數的用法,可以先查看官方文檔 渲染函數 & JSX。
1、首先引用下官方的示例:
這裏用模板並不是最好的選擇:不但代碼冗長,而且在每一個級別的標題中重複書寫了<slot></slot>
,在要插入錨點元素時還要再次重複。
<template>
<div>
<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>
</div>
</template>
<script>
export default {
name: "App",
props: ['level']
};
</script>
雖然模板在大多數組件中都非常好用,但是顯然在這裏它就不合適了。那麼,我們來嘗試使用 render
函數重寫上面的例子:
<script>
export default {
name: "App",
render: function (createElement) {
return createElement(
'h' + this.level, // 標籤名稱
this.$slots.default // 子節點數組
)
},
props: {
level: {
type: Number,
required: true
}
}
};
</script>
這樣看起來代碼就精簡多了,但是需要非常熟悉 Vue 的實例屬性。在這個例子中,你需要知道,向組件中傳遞不帶 v-slot
指令的子節點時,這些子節點被存儲在組件實例中的 $slots.default
中,否則具名插槽可通過 $slots.插槽名
稱來指定。
可以看到,render
函數接收一個參數createElement
,然後Vue 通過建立一個虛擬 DOM
(VNode
)來追蹤自己要如何改變真實 DOM
。
createElement
函數中使用模板中的那些功能,它接受的參數如下:
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一個 HTML 標籤名、組件選項對象,或者
// resolve 了上述任何一種的一個 async 函數。必填項。
"div",
// {Object}
// 一個與模板中屬性對應的數據對象。可選。
{
// 與 `v-bind:class` 的 API 相同,
// 接受一個字符串、對象或字符串和對象組成的數組
class: {
foo: true,
bar: false,
},
// 與 `v-bind:style` 的 API 相同,
// 接受一個字符串、對象,或對象組成的數組
style: {
color: "red",
fontSize: "14px",
},
// 普通的 HTML attribute
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,
},
// {String | Array}
// 子級虛擬節點 (VNodes),由 `createElement()` 構建而成,
// 也可以使用字符串來生成“文本虛擬節點”。可選。
[
"先寫一些文字",
createElement("h1", "一則頭條"),
createElement(MyComponent, {
props: {
someProp: "foobar",
},
}),
]
);
2、父子template
組件通過render
方法實現:
首先初始單文本組件如下,來模擬一個簡單的TODO頁面:
// 父組件:Todo.vue
<template>
<div class="todo">
<input type="text" v-model="content" placeholder="接下來的計劃..." />
<button @click="commit">提交</button>
<todo-list :todoList="todoList">待辦事項:</todo-list>
</div>
</template>
<script>
import TodoList from "./TodoList.vue";
export default {
name: "Todo",
data() {
return {
content: "",
todoList: []
};
},
methods: {
commit() {
let id = this.todoList.length + 1;
this.todoList.push({ id: id, title: this.content });
this.content = '';
},
},
components: {
TodoList
},
};
</script>
<style lang="less" scoped>
.todo {
width: 500px;
margin: 50px auto;
input {
padding: 5px 10px;
}
button {
margin-left: 20px;
padding: 5px 10px;
}
}
</style>
// 子組件:TodoList.vue
<template>
<div class="todo-list">
<slot></slot>
<ul>
<li v-for="item in todoList" :key="item.id">{{ item.title }}</li>
</ul>
</div>
</template>
<script>
export default {
name: "TodoList",
props: ["todoList"]
};
</script>
<style lang="less" scoped>
.todo-list {
margin-top: 20px;
padding-left: 0;
li {
margin: 5px 0;
list-style: none;
}
}
</style>
通過以上是現實瞭如下頁面:
接下來嘗試使用render
函數實現以上功能頁面:
- 發現如果
template
和render
函數同時存在時,Vue還是會優先使用template
中的內容。
父組件Todo.vue:
<!--<template>
<div class="todo">
<input type="text" v-model="content" placeholder="接下來的計劃..." />
<button @click="commit">提交</button>
<todo-list :todoList="todoList">{{ listTitle }}</todo-list>
</div>
</template>-->
<script>
import TodoList from "./TodoList.vue";
export default {
name: "Todo",
data() {
return {
content: "",
todoList: [],
listTitle: "待辦事項:",
};
},
methods: {
commit() {
let id = this.todoList.length + 1;
this.todoList.push({ id: id, title: this.content });
this.content = "";
},
},
render(createElement) {
var self = this; // 定義self保存this,使之始終指向vue示例
return createElement(
"div",
{
// 接受一個字符串、對象或字符串和對象組成的數組
class: {
todo: true, // 通過對象定義class是否啓用
}
},
[
createElement("input", {
// DOM 屬性
domProps: {
type: "text",
placeholder: "接下來的計劃...",
value: self.content, // 將content屬性值賦值給輸入框
},
// 普通的 HTML attribute
// attrs: {
// type: "text",
// placeholder: "接下來的計劃...",
// value: self.content, //
// },
on: {
change: function(event) {
self.content = event.target.value; // 輸入框值賦給content屬性
},
},
}),
createElement(
"button",
{
// 給按鈕添加click事件,觸發commit方法
on: {
click: this.commit,
}
},
"提交"
),
createElement(
// 子組件選項對象
"todo-list",
{
// props傳值給子組件
props: {
todoList: this.todoList,
}
},
this.listTitle
),
]
);
},
components: {
TodoList,
},
};
</script>
<style lang="less" scoped>
.todo {
width: 500px;
margin: 50px auto;
input {
padding: 5px 10px;
}
button {
margin-left: 20px;
padding: 5px 10px;
}
}
</style>
子組件TodoList.vue:
<!--<template>
<div class="todo-list">
<slot></slot>
<ul>
<li v-for="item in todoList" :key="item.id">{{ item.id }}. {{ item.title }}</li>
</ul>
</div>
</template>-->
<script>
export default {
name: "TodoList",
props: ["todoList"],
render(createElement) {
return createElement(
"div",
{
class: {
'todo-list': true,
}
},
[
this.$slots.default,
createElement(
"ul",
this.todoList.map(function(item) {
return createElement(
"li",
{
key: item.id,
},
`${item.id}. ${item.title}`
);
})
),
]
);
},
};
</script>
<style lang="less" scoped>
.todo-list {
margin-top: 20px;
padding-left: 0;
li {
margin: 5px 0;
list-style: none;
}
}
</style>
3、小結
-
createElement
createElement
,是 Vue 虛擬DOM
的概念,創建出來的並不是html
節點,而是VNode
的一個類,類似DOM
結構的一個結構,並存在內存中,它會和真正的DOM
進行對比,若發現需要更新的DOM
,纔會去轉換這部分DOM
內容,並填到真正的DOM
中,從而提高性能; -
nativeOn 與 on 的區別
對於nativeOn
,官方的解釋是:僅對於組件,用於監聽原生事件,而不是組件內部使用vm.$emit
觸發的事件。
解釋比較抽象,個人理解:
父組件要在子組件上使用click
事件,就像使用正常的html
標籤那樣使用click
,我們知道在Vue中,普通html
標籤中這樣寫click
事件是沒問題:<h @click="doSomething()"></h>
但假如我們有一個組件叫comA,直接使用
click
是不行的(除非子組件裏面做了處理),加上了.native
就可以生效:<comA @click="doSomething()"></comA> // 無效 <comA @click.native="doSomething()"></comA> // 有效
所以,僅用於組件這句話意思應該是:
createElement()
裏面使用nativeOn
創建的不可以是原生html
元素而是組件,比如:createElement("p", { nativeOn: { click: function() {} } })
這個時候
nativeOn
就沒有意義,而下面寫法就會有意義:createElement("組件名稱", { nativeOn: { click: function() {} } })
在該組件
根節點
上發生了點擊事件會觸發nativeOn
裏面的click
事件。