vue2 中使用 render 函數(shù)編寫組件的方式
vue2 中如何使用 render 函數(shù)編寫組件
vue 提供了聲明式編寫 UI 的方式,即 vue 提供了對 DOM 進行描述的方式。
有兩種描述 DOM 的方式:模板和 render 函數(shù)。模板在編譯階段會被轉成 render 函數(shù),這一過程叫編譯模板。
模板可讀性好,但是有時候模板并不靈活,大型的模板可讀性也不好。
render 函數(shù)可讀性不高,但是靈活,使用 render 函數(shù)封裝組件,使用得當,可提高組件的擴展性和易用性。jsx 可解決 render 函數(shù)讀寫性不高的問題。
在 vue 的項目入口文件中,下面的代碼新建一個 vue 應用的根組件,并默認命名為 Root,并將其掛載在 HTML 模板 #app div 上,它的模板在哪?
new Vue({
render: h => h(App),
}).$mount('#app')這是一個沒有模板的組件。
KaTeX parse error: Expected 'EOF', got '#' at position 8: mount('#?app'),選擇器對應的 do…mount(elector)`掛載元素,替換 selector 后,dom 的屬性丟失。 兩者表現(xiàn)不同,有點奇怪,但是不需要太關注這個區(qū)別。
今天再來復習 render 函數(shù),重點關注這些容易踩坑的地方:
- 學習 render 函數(shù)的使用,重點:樣式、事件、插槽、指令、props、v-model、函數(shù)組件的處理。
- 學習使用 jsx 封裝組件。
- 在 render 函數(shù)中使用表單組件,因為表單組件涉及到
v-model容易踩坑。
為何要了解 render ?
在 render 函數(shù)中,可用 jsx 代替模板語法,可充分發(fā)揮 js 的能力,使得組件擴展性更好、封裝更加優(yōu)雅。
官網(wǎng)說得比較明白了,但是例子過于簡單,只能體現(xiàn)優(yōu)雅,沒有體現(xiàn)擴展性,稍后封裝一個能體現(xiàn)擴展性的組件。
render 基礎語法
render 函數(shù)簽名:
render(createElement: CreateElement, hack: RenderContext<Props>): VNode;
返回值 – VNode (虛擬節(jié)點) — 一個用于描述 vue 組件結構的 JS 對象

說明
①. 返回值往往是一個單節(jié)點,劃線的 context 是 vue 全局對象,有 $children 屬性,是一個 VNode 數(shù)組,元素是一個 VNode,這樣的層層嵌套,正好對應的組件的嵌套。 $el 是組件掛載點,_uid 是組件 id。調(diào)試時可能會用到。②. 也可以返回 VNode 數(shù)組。比如返回
this.$scopedSlots.default()這是一個作用域插槽,可能是數(shù)組。
參數(shù)
第一個參數(shù) createElement,是一個函數(shù),DOM 中有 createElement 用于創(chuàng)建 DOM 節(jié)點。vue 中的 createElement 是用于創(chuàng)建 VNode 的。
h 是 createElement 的別名,代表
Hyperscript(生成 HTML 的腳本),Hyperscript itself stands for “script that generates HTML structures”。
寫成
h,更加方便輸入,更加語義化。
第二個參數(shù) hack 是渲染上下文,組件的props、listeners、slots 都在這個參數(shù)里。

說明
第二個參數(shù)在函數(shù)組件中才有,非函數(shù)組件為 undefined。因為函數(shù)組件中不存在組件實例 this,要提供這個參數(shù)獲取 props 等內(nèi)容。
第二個參數(shù)通常寫成context,更加語義化。
render 寫成這樣:
render(h,context){
return <span>render函數(shù)</span>
}render 使用 es6 聲明方法的方式且返回 jsx ,vue 會自動注入
const h = this.$createElement,僅限于 render 函數(shù),其他函數(shù)要使用,就要手動通過參數(shù)傳入。
render(){
return <span>render函數(shù)</span>
}這是 es5 的方式:
render:function(){ // 顯示傳遞 h: function(h) 才行
return <span>render函數(shù)</span>
}
render不能返回文本,想要顯示文本內(nèi)容,必須套一個標簽, react 的 render 可以返回文本。
返回 jsx,vue 內(nèi)部會調(diào)用
createElement編譯成 VNode。
如何返回純文本?
返回文本的 hack 寫法
return this._v('someText'),不推薦這么寫,會導致他人難以理解。
createElement
返回值:VNode
VNode 是一個描述組件的普通 js 對象。
參數(shù)
createElement(
// html 標簽 、自定義標簽,比如 el-input
// template // NOTE 用于傳遞插槽
// 一個組件選項對象
// resolve 了上述任何一種的一個 async 函數(shù) // TODO 如何使用
'div', // NOTE 必需的
// 模板使用到的數(shù)據(jù)對象
{},
// string 或者 子VNode
[]
)第一個參數(shù)不能省略,所以不能返回純文本。 和 react 的 render 不同。
注意
第一個參數(shù)可以是 template,往往和第二個參數(shù)的slot屬性一起使用,指定插槽名稱,傳遞插槽時可以用到。
resolve 的用法我沒有搜索到例子,歡迎大佬告訴我。
重點關注第二個參數(shù)
處理樣式和類
{
// :class = "{foo:true,bar:false}"
class:{
foo: true,
bar: false
},
// :style="{color:'red','font-size':'14px'}"
style:{
color:'red',
fontSize:'14px'
}
}組件 props
{
props: {
myCustomProp: '組件屬性值'
}
}HTML 特性和 DOM 屬性
{
// HTML 特性
// NOTE 在組件內(nèi)部使用 $attrs 獲取 attrs
// NOTE 會和 class 屬性合并嗎?
// NOTE 和 class 屬性的優(yōu)先級,誰高?
// 這里的 class 不會添加到 標簽上
attrs: {
id: 'divId',
class: 'className'
},
// DOM 屬性
domProps:{
textContent: 'div 文本',// 優(yōu)先級高于 v-text
innerHTML: 'BAR' // 優(yōu)先級高于 v-html
}
}注意
①. attrs 特性中的 class 不會被添加到標簽上。
②. 注意區(qū)分 HTML 特性和 DOM 屬性的區(qū)別。
處理事件
{
// v-bind:event
on: {
customEventName: value => {
// 監(jiān)聽組件的自定義事件 即 emit 觸發(fā)事件
}
},
// 監(jiān)聽組件上的原生事件,只能用在組件上
nativeOn: {
click: target => {
// 監(jiān)聽原生事件 即非 emit 觸發(fā)的事件
}
}
}注意
nativeOn只能用于自定義組件。
插槽
{
scopedSlots: {
// 默認插槽
default: props => h('span',props.text),
otherSlot: props => h('div',props.customProp)
},
slot: 'slotName'// 一般和第一個參數(shù) template 一起使用
}使用模板定義一個按鈕:
<template>
<div>
<slot name="left"></slot>
<button>
<slot v-bind:person="person">
<span>按鈕</span>
</slot>
</button>
<slot name="right" v-bind:age="person.age"></slot>
</div>
</template>
<script>
export default {
name: 'MyButton',
data() {
return {
person: {
name: 'jack',
age: 23,
},
}
},
}
</script>在模板種使用該組件:
<MyButton>
<template #right="{age}">
<span>按鈕右邊 {{ age }} 歲</span>
</template>
<template v-slot="{ person }">這是按鈕,{{ person }}</template>
<template #left>
<span>按鈕左邊</span>
</template>
</MyButton>在 render 中使用該組件
import MyButton from './MyButton.vue'
export default {
name: 'UseButton',
render(h) {
//NOTE h 第一個參數(shù)為 template 第二個參數(shù)里的 slot 屬性指定插槽名稱
const slotLeft = h('template', { slot: 'left' }, '按鈕左邊')
const slotRight = h('template', { slot: 'right' }, '按鈕右邊')
const slotDefault = h('template', { slot: 'default' }, '默認插槽')
const children = [slotLeft, slotDefault, slotRight]
return h(MyButton, {}, children)
},
}在 render 中獲取作用域插槽拋出的數(shù)據(jù)
import MyButton from './MyButton.vue'
export default {
name: 'UseButton',
render(h) {
const slotLeft = h('template', { slot: 'left' }, '按鈕左邊')
const children = [slotLeft]
return h(
MyButton,
{
scopedSlots: {
default: props => {
console.log(props)
const { person } = props
const text = `作用域插槽,${JSON.stringify(person)}`
// 返回 h 創(chuàng)建的 VNode
return h('span', {}, text)
},
right: props => {
console.log(props)
const { age } = props
// 返回 jsx
return <span>按鈕右邊 {age} 歲</span>
},
},
},
children
)
},
}總結
①. 普通命名插槽,使用h('template',{slot:'slotName'},children) 編寫,然后放渲染組件的第三個參數(shù)里。
②. 作用域插槽在第二個參數(shù)的 scopedSlots 對象里,該對象的每個屬性名是組件的插槽名,值是一個函數(shù),參數(shù)為插槽綁定的數(shù)據(jù)。
使用 render 函數(shù)重寫編寫
MyButton
export default {
name: 'MyButton',
data() {
return {
person: {
name: 'jack',
age: 23,
},
}
},
render(h) {
// NOTE default 關鍵字 不重命名 無法解構
const { left, right, default: _defaultSlot } = this.$scopedSlots
// NOTE 傳遞一個對象,在模板中使用解構取出屬性
const defaultSlot = _defaultSlot({ person: this.person })
const leftSlot = left()
const rightSlot = right(this.person)
const button = h('button', {}, [defaultSlot])
return h('div', {}, [leftSlot, button, rightSlot])
},
}返回 jsx
export default {
name: 'MyButton',
data() {
return {
person: {
name: 'jack',
age: 23,
},
}
},
render(h) {
const { left, right, default: _defaultSlot } = this.$scopedSlots
// NOTE 檢查插槽是否存在
const defaultSlot = (_defaultSlot && _defaultSlot({ person: this.person })) || <span>按鈕</span>
const leftSlot = (left && left()) || ''
const rightSlot = right(this.person)
const button = h('button', {}, [defaultSlot])
// 返回 jsx 使得 dom 結構更加清晰
return (
<div>
{leftSlot}
{defaultSlot}
{rightSlot}
</div>
)
},
}函數(shù)式組件:
export default {
name: 'MyButton',
functional: true,
props: {
person: {
type: Object,
default: () => ({ name: 'jack', age: 23 }),
},
},
// NO DATA in functional component
// data() {
// return {
// person: {
// name: 'jack',
// age: 23,
// },
// }
// },
render(h, { props, scopedSlots }) {
const { left, right, default: _defaultSlot } = scopedSlots
const defaultSlot = (_defaultSlot && _defaultSlot({ person: props.person })) || <span>按鈕</span>
const leftSlot = (left && left()) || ''
const rightSlot = right(props.person)
const button = h('button', {}, [defaultSlot])
return (
<div>
{leftSlot}
{button}
{rightSlot}
</div>
)
},
}總結
①. 普通插槽、命名插槽、作用域插槽都通過this.$scopedSlots獲取,它們都是返回 VNode 的函數(shù)。
②. 插槽綁定的數(shù)據(jù)通過插槽函數(shù)傳遞,基本數(shù)據(jù)使用{}包裹,方便在模板中解構。
③. 返回 jsx 能讓 div 結構更加清晰。
④. 注意檢查是否存在插槽,以啟用后備內(nèi)容。
指令
{
directives: [{ name: 'directive-name', value: '2', expression: '1+1', arg: 'foo', modifiers: { foo: true } }]
}在模板中定義指令
<template>
<!-- title 是名字, 指令的 value 由表達式計算出來 -->
<!-- v-title:argument.modifier1.modifier2="expression" -->
<div>
在模板中編寫指令
<p v-title>這是簡單指令</p>
<!-- 只能帶一個參數(shù) -->
<p v-title:argu>這是帶參數(shù)的指令</p>
<!-- 動態(tài)參數(shù) -->
<p v-title:[dynamicArgu()]>這是帶動態(tài)參數(shù)的指令</p>
<p v-title:argu.foo.bar>這是帶參數(shù)和修飾符的指令</p>
<p v-title:job.foo="data">這是帶參數(shù)、修飾符和普通表達式的指令</p>
<p v-title:job.foo="expresFun">這是帶參數(shù)、修飾符和函數(shù)表達式的指令</p>
</div>
</template>
<script>
export default {
name: 'Title',
directives: {
title: {
inserted(el, bindings, vnode) {
const { context: that } = vnode
const { value = false } = bindings
if (typeof value === 'function') {
that.setTile(el, value(that.data))
} else {
that.setTile(el, value)
}
},
componentUpdated(el, bindings, vnode) {
const { context: that } = vnode
const { value = false } = bindings
if (typeof value === 'function') {
that.setTile(el, value(that.data))
} else {
that.setTile(el, value)
}
},
},
},
data() {
return {
data: { age: 23, job: 'web dev' },
}
},
methods: {
setTile(el, titleValue) {
const textContent = el.textContent
const title = textContent.trim() || '暫無數(shù)據(jù)'
el.title = typeof titleValue === 'string' ? titleValue : title
},
dynamicArgu() {
return Math.random() > 0.5 ? 'argu1' : 'argu0'
},
expresFun(data) {
return data.age + '歲'
},
},
}
</script>指令對象 bindings

總結
不建議在 render 函數(shù)中編寫指令,難以理解,指令需要在模板使用才能發(fā)揮其設計的目的。render 中可直接控制 DOM。
v-model 指令
使用 render 定義組件,如何提供 v-model?
說明:
prop:–value +使用 on監(jiān)聽組件的事件,在處理函數(shù)中觸發(fā)input自定義事件。
在 render 函數(shù)中使用 v-model 指令的處理有三種方案:
① . 在數(shù)據(jù)對象中使用 model 屬性:
{
model: {
value: this.value,// value 是 data 里的屬性
callback: value => {
// 可以再賦值之前做其他邏輯
// 驗證數(shù)據(jù)
// 觸發(fā)事件
this.value = value
}
}
}②. 傳遞 value + 監(jiān)聽 input 事件
{
props: {
// value 是 data 中的屬性
value: this.value
},
on: {
input: value => {
// 可做其他事情
// 觸發(fā)事件
this.value = value
}
}
}③. 在 jsx 中使用 vModel 屬性
// input 是 data 中的屬性
<MyInput vModel={this.input} />三種方案的優(yōu)缺點:
model 屬性更加好,當表單項還有其他事件時,還可以在 on 中監(jiān)聽它們,比如 element 的下拉,有change、clear 等事件。
props value + input, 很符合 v-model 的語法糖。
jsx+ vModel 屬性,簡潔,常用。
其他屬性
{
key: 'v for 中的 key',
ref:'模板變量',
refInFor: true, // 循環(huán)中的 ref 是一個數(shù)組
}使用 render 封裝一個輸入框
MyInput.jsx
import './my-input.css'
export default {
name: 'MyInput',
props: {
// 需要實現(xiàn) v-model 指令
value: {
type: [String, Number],
default: '',
},
},
render(h) {
return h('input', {
class: {
'my-input': true,
},
style: {
backgroundColor: '#ccc',
},
attrs: {
id: 'my-input',
class: 'a-my-input',
'data-key': 'key',
},
domProps: {
value: this.value,
},
// 監(jiān)聽 input 的 input 事件
on: {
input: ({ target }) => {
this.$emit('input', target.value)
},
},
})
},
}說明
還可以使用computed: domProp 的 value 接收一個計算屬性,為該計算屬性提供 setter 和 getter ,在 input 事件處理函數(shù)中設置計算屬性的值,在 setter 中觸發(fā) 自定義的 input 事件。這種方法不如上面的明白,代碼量也多了。
在模板中使用該組件
<MyInput v-model="myInput" />
在 render 函數(shù)中使用
export default {
name: 'UseInput',
data() {
return {
input: '',
}
},
render(h) {
return h('div', {}, [
h(MyInput, {
model: {
value: this.input,
callback: value => {
// 可在此做其他事件
this.input = value
},
},
}),
h('h3', {}, this.input),
])
},
}希望 UseInput,支持 v-model,即在二次封裝 MyInput。
方案 1:添加 value props 在 model 中觸發(fā) input,刪除 data 中的 input。
import MyInput from './my-input.jsx'
export default {
name: 'UseInput',
props: {
value: { type: [String, Number], default: '' },
},
render(h) {
return h('div', {}, [
h(MyInput, {
model: {
value: this.value,
callback: value => {
// 可在此做其他事件
this.$emit('input', value)
},
},
}),
h('h3', {}, this.value),
])
},
}方案 2: 添加 value props,將其通過 props 傳入 UseInput,監(jiān)聽 UseInput 的input事件,在此觸發(fā)input事件。
import MyInput from './my-input.jsx'
export default {
name: 'UseInput',
props: {
value: { type: [String, Number], default: '' },
},
render(h) {
return h('div', {}, [
h(MyInput, {
props: {
value: this.value,
},
on: {
input: value => {
this.$emit('input', value)
},
},
}),
h('h3', {}, this.value),
])
},
}說明: 對于具有多種事件的表單項,比如 element 的下拉框,第一種方案更加好,
on屬性留位置給從外傳入的處理函數(shù)。
方案 3: jsx + vModel + prop value
import MyInput from './my-input.jsx'
export default {
name: 'UseInput',
props: {
value: { type: [String, Number], default: '' },
},
data() {
return {
input: this.value,
}
},
render(h) {
return (
<div>
<MyInput vModel={this.input} />
{/* <h2>{this.input}</h2> */}
</div>
)
},
}** 注意**:這種方案不能實現(xiàn)雙向綁定
其他問題
如何限制繼承的屬性,inheritAttrs 設置為 false,無法顯示。
在模板定義的組件中,inheritAttrs 屬性設置為 false, 除style、class 以為的屬性不會添加到根組件,實現(xiàn)手動控制。
render 定義的組件中,也是一樣的。
參考
What does the ‘h’ stand for in Vue’s render method?
A Practical Use Case for Vue Render Functions: Building a Design System Typography Grid
How to use v-model ( for custom input component) in render function?
到此這篇關于vue2 中如何使用 render 函數(shù)編寫組件的文章就介紹到這了,更多相關vue2使用 render 函數(shù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
VUE使用vue?create命令創(chuàng)建vue2.0項目的全過程
vue-cli是創(chuàng)建Vue項目的一個腳手架工具,vue-cli提供了vue create等命令,下面這篇文章主要給大家介紹了關于VUE使用vue?create命令創(chuàng)建vue2.0項目的相關資料,文中通過圖文介紹的非常詳細,需要的朋友可以參考下2022-07-07

