vue2 中如何使用 render 函数编写组件
- render 基础语法
- createElement
- 返回值:VNode
- 参数
- 处理样式和类
- 组件 props
- HTML 特性和 DOM 属性
- 处理事件
- 插槽
- 指令
- v-model 指令
- 其他属性
- 使用 render 封装一个输入框
- 其他问题
- 参考
vue 提供了声明式编写 UI 的方式,即 vue 提供了对 DOM 进行描述的方式。
有两种描述 DOM 的方式:模板和 render 函数。模板在编译阶段会被转成 render 函数,这一过程叫编译模板。
模板可读性好,但是有时候模板并不灵活,大型的模板可读性也不好。
render 函数可读性不高,但是灵活,使用 render 函数封装组件,使用得当,可提高组件的扩展性和易用性。jsx 可解决 render 函数读写性不高的问题。
在 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 的属性丢失。 两者表现不同,有点奇怪,但是不需要太关注这个区别。
今天再来复习 render 函数,重点关注这些容易踩坑的地方:
-
学习 render 函数的使用,重点:样式、事件、插槽、指令、props、v-model、函数组件的处理。
-
学习使用 jsx 封装组件。
-
在 render 函数中使用表单组件,因为表单组件涉及到
v-model
容易踩坑。
为何要了解 render ?
在 render 函数中,可用 jsx 代替模板语法,可充分发挥 js 的能力,使得组件扩展性更好、封装更加优雅。
官网说得比较明白了,但是例子过于简单,只能体现优雅,没有体现扩展性,稍后封装一个能体现扩展性
的组件。
render 基础语法
render 函数签名:
render(createElement: CreateElement, hack: RenderContext<Props>): VNode;
- 返回值 – VNode (虚拟节点) — 一个用于描述 vue 组件结构的 JS 对象
说明
①. 返回值往往是一个单节点,划线的 context 是 vue 全局对象,有 $children 属性,是一个 VNode 数组,元素是一个 VNode,这样的层层嵌套,正好对应的组件的嵌套。 $el 是组件挂载点,_uid 是组件 id。调试时
可能会用到。②. 也可以返回 VNode 数组。比如返回
this.$scopedSlots.default()
这是一个作用域插槽,可能是数组。
- 参数
第一个参数 createElement
,是一个函数,DOM 中有 createElement 用于创建 DOM 节点。vue 中的 createElement
是用于创建 VNode 的。
h 是 createElement 的别名,代表
Hyperscript
(生成 HTML 的脚本),Hyperscript itself stands for “script that generates HTML structures”。
写成
h
,更加方便输入,更加语义化。
第二个参数 hack
是渲染上下文,组件的props
、listeners
、slots
都在这个参数里。
说明
第二个参数在函数组件中才有,非函数组件为 undefined。因为函数组件中不存在组件实例 this,要提供这个参数获取 props 等内容。
第二个参数通常写成context
,更加语义化。
render 写成这样:
render(h,context){
return <span>render函数</span>
}
render 使用 es6 声明方法的方式且返回 jsx ,vue 会自动注入
const h = this.$createElement
,仅限于 render 函数,其他函数要使用,就要手动通过参数传入。
render(){
return <span>render函数</span>
}
这是 es5
的方式:
render:function(){ // 显示传递 h: function(h) 才行
return <span>render函数</span>
}
render
不能返回文本,想要显示文本内容,必须套一个标签, react 的 render 可以返回文本。
返回 jsx,vue 内部会调用
createElement
编译成 VNode。
如何返回纯文本?
返回文本的 hack 写法
return this._v('someText')
,不推荐这么写,会导致他人难以理解。
createElement
返回值:VNode
VNode 是一个描述组件的普通 js 对象。
参数
createElement(
// html 标签 、自定义标签,比如 el-input
// template // NOTE 用于传递插槽
// 一个组件选项对象
// resolve 了上述任何一种的一个 async 函数 // TODO 如何使用
'div', // NOTE 必需的
// 模板使用到的数据对象
{},
// string 或者 子VNode
[]
)
第一个参数不能省略,所以不能返回纯文本。 和 react 的 render 不同。
注意
第一个参数可以是 template,往往和第二个参数的slot
属性一起使用,指定插槽名称,传递插槽时可以用到。
resolve 的用法我没有搜索到例子,欢迎大佬告诉我。
重点关注第二个参数
处理样式和类
{
// :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 在组件内部使用 $attrs 获取 attrs
// NOTE 会和 class 属性合并吗?
// NOTE 和 class 属性的优先级,谁高?
// 这里的 class 不会添加到 标签上
attrs: {
id: 'divId',
class: 'className'
},
// DOM 属性
domProps:{
textContent: 'div 文本',// 优先级高于 v-text
innerHTML: 'BAR' // 优先级高于 v-html
}
}
注意
①. attrs 特性中的 class 不会被添加到标签上。
②. 注意区分 HTML 特性和 DOM 属性的区别。
处理事件
{
// v-bind:event
on: {
customEventName: value => {
// 监听组件的自定义事件 即 emit 触发事件
}
},
// 监听组件上的原生事件,只能用在组件上
nativeOn: {
click: target => {
// 监听原生事件 即非 emit 触发的事件
}
}
}
注意
nativeOn
只能用于自定义组件。
插槽
{
scopedSlots: {
// 默认插槽
default: props => h('span',props.text),
otherSlot: props => h('div',props.customProp)
},
slot: 'slotName'// 一般和第一个参数 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 第一个参数为 template 第二个参数里的 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 中获取作用域插槽抛出的数据
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 创建的 VNode
return h('span', {}, text)
},
right: props => {
console.log(props)
const { age } = props
// 返回 jsx
return <span>按钮右边 {age} 岁</span>
},
},
},
children
)
},
}
总结
①. 普通命名插槽,使用h('template',{slot:'slotName'},children)
编写,然后放渲染组件的第三个参数
里。
②. 作用域插槽在第二个参数的 scopedSlots
对象里,该对象的每个属性名是组件的插槽名
,值是一个函数,参数为插槽绑定的数据。
使用 render 函数重写编写 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>
)
},
}
函数式组件:
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 的函数。
②. 插槽绑定的数据通过插槽函数传递,基本数据使用{}
包裹,方便在模板中解构。
③. 返回 jsx 能让 div 结构更加清晰。
④. 注意检查是否存在插槽,以启用后备内容。
指令
{
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>
<!-- 只能带一个参数 -->
<p v-title:argu>这是带参数的指令</p>
<!-- 动态参数 -->
<p v-title:[dynamicArgu()]>这是带动态参数的指令</p>
<p v-title:argu.foo.bar>这是带参数和修饰符的指令</p>
<p v-title:job.foo="data">这是带参数、修饰符和普通表达式的指令</p>
<p v-title:job.foo="expresFun">这是带参数、修饰符和函数表达式的指令</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() || '暂无数据'
el.title = typeof titleValue === 'string' ? titleValue : title
},
dynamicArgu() {
return Math.random() > 0.5 ? 'argu1' : 'argu0'
},
expresFun(data) {
return data.age + '岁'
},
},
}
</script>
指令对象 bindings
总结
不建议在 render 函数中编写指令,难以理解,指令需要在模板使用才能发挥其设计的目的。render 中可直接控制 DOM。
v-model 指令
使用 render 定义组件,如何提供 v-model
?
说明:
prop:–value +使用 on
监听组件的事件,在处理函数中触发input
自定义事件。
在 render 函数中使用 v-model 指令的处理有三种方案:
① . 在数据对象中使用 model
属性:
{
model: {
value: this.value,// value 是 data 里的属性
callback: value => {
// 可以再赋值之前做其他逻辑
// 验证数据
// 触发事件
this.value = value
}
}
}
②. 传递 value + 监听 input 事件
{
props: {
// value 是 data 中的属性
value: this.value
},
on: {
input: value => {
// 可做其他事情
// 触发事件
this.value = value
}
}
}
③. 在 jsx 中使用 vModel
属性
// input 是 data 中的属性
<MyInput vModel={this.input} />
三种方案的优缺点:
model
属性更加好,当表单项还有其他事件时,还可以在 on
中监听它们,比如 element 的下拉,有change
、clear
等事件。
props value
+ input
, 很符合 v-model 的语法糖。
jsx+ vModel
属性,简洁,常用。
其他属性
{
key: 'v for 中的 key',
ref:'模板变量',
refInFor: true, // 循环中的 ref 是一个数组
}
使用 render 封装一个输入框
MyInput.jsx
import './my-input.css'
export default {
name: 'MyInput',
props: {
// 需要实现 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,
},
// 监听 input 的 input 事件
on: {
input: ({ target }) => {
this.$emit('input', target.value)
},
},
})
},
}
说明
还可以使用computed
: domProp 的 value 接收一个计算属性,为该计算属性提供 setter 和 getter ,在 input 事件处理函数中设置计算属性的值,在 setter 中触发 自定义的 input 事件。这种方法不如上面的明白,代码量也多了。
在模板中使用该组件
<MyInput v-model="myInput" />
在 render 函数中使用
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 中触发 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,监听 UseInput 的input
事件,在此触发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
属性留位置给从外传入的处理函数。
方案 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>
)
},
}
** 注意**:这种方案不能实现双向绑定
其他问题
- 如何限制继承的属性,inheritAttrs 设置为 false,无法显示。
在模板定义的组件中,inheritAttrs
属性设置为 false, 除style
、class
以为的属性不会添加到根组件,实现手动控制。
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?