Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。
假设我们要生成一些带锚点的标题:
<h1>
<a name="hello-world" href="#hello-world">
Hello world!
a>
h1>
对于上面的 HTML,你决定这样定义组件接口:
<anchored-heading :level="1">Hello world!anchored-heading>
当开始写一个只能通过 level prop 动态生成标题 (heading) 的组件时,你可能很快想到这样实现:
<script type="text/x-template" id="anchored-heading-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>
script>
Vue.component('anchored-heading', {
template: '#anchored-heading-template',
props: {
level: {
type: Number,
required: true
}
}
})
这里用模板并不是最好的选择:不但代码冗长,而且在每一个级别的标题中重复书写了 ,在要插入锚点元素时还要再次重复。
虽然模板在大多数组件中都非常好用,但是显然在这里它就不合适了。
那么,我们来尝试使用 render 函数重写上面的例子:
Vue.component('anchored-heading', {
render(createElement) {
return createElement(
'h' + this.level, // 标签名称
this.$slots.default // 子节点数组
)
},
props: {
level: {
type: Number,
required: true
}
}
})
看起来简单多了!这样代码精简很多。但是你可能会有新的发现,就这 render 中出现了一个新的 createElement ,wtf, 这都是什么跟什么?另外 createElement 中传参都是什么?,那么下面,我们来揭晓它的神秘面纱。
createElement 其实就是增加一个元素,然后返回一个 createNodeDescription ,wtf。这又是什么? 其实它就是我们经常挂在嘴边的,虚拟节点 (virtual node)或虚拟DOM ,常简写为 VNode。
接下来你需要熟悉的是如何在 createElement 函数中使用模板中的那些功能。这里是 createElement 接受的参数:
createElement(
// {String | Object | Function}
// 一个 HTML 标签名、组件选项对象,或者
// resolve 了上述任何一种的一个 async 函数。必填项。
'div',
// {Object}
// 一个与模板中 attribute 对应的数据对象。可选。
{
// (详情见下一节)
},
// {String | Array}
// 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
// 也可以使用字符串来生成“文本虚拟节点”。可选。
[
'先写一些文字',
createElement('h1', '一则头条'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
createElement一共有三个参数,三个参数分别是:
这时候我们就可以使用 createElement 来写组件了
export default {
methods: {
getChildrenTextContent(children) {
return children.map(function (node) {
return node.children
? getChildrenTextContent(node.children)
: node.text
}).join('')
}
},
render(createElement) {
let headingId = this.getChildrenTextContent(this.$slots.default)
.toLowerCase()
.replace(/\W+/g, '-')
.replace(/(^-|-$)/g, '');
return createElement(
'h' + this.level, // h1~h6 标签
null, // 组件属性,可以不传
// 子组件集合
[
createElement('a', {
attrs: {
name: headingId,
href: '#' + headingId
}
}, this.$slots.default)
]
)
},
props: {
level: {
type: Number,
required: true
}
}
}
目前到这里为止,看着还可以,但是如果使用 createElement 来创建一个 form 表单的时候,你可能会会恐怖的发现,你想放弃了:
export default {
render(createElement) {
return createElement(
'form',
[
createElement(
'label',
[
createElement(
'input',
{
attrs: {
placeholder: '审批人'
},
on: {
input: () => {
}
}
}
)
]
),
createElement(
'label',
[
createElement(
'select',
[
createElement('option', {
attrs: {
value: 'shanghai'
},
'上海'
}),
createElement('option', {
attrs: {
value: 'beijing'
},
'北京'
})
]
)
]
),
createElement('label', null, [
createElement(
'button',
{
on: {
click: () => {
}
}
},
'查询'
)
])
]
)
}
看到上面的代码后,感觉是不是 “很香” ?那么怎么才能让它不这么香呢?
JSX 是一种 JavaScript 的语法扩展, JSX = JavaScript + XML,,就是在 JavaScript 里面写 XML ,既具备了 JavaScript 的灵活,又兼具 html 的语义化和直观性。
让我们使用JSX 来实现上面示例中的form表单:
export default {
methods: {
input_function(){
},
click_function(){
}
},
render() {
return (
<form>
<label>
<input placeholder="审批人" onInput={
this.input_function} />
</label>
<label>
<select>
<option value="shanghai">上海</option>
<option value="beijing">北京</option>
</select>
</label>
<label>
<button onClick={
this.click_function}>查询</button>
</label>
</form>
)
}
}
怎么样?是不是真香了?不过在使用 JSX 时,我们的 v-model、v-for、v-if 等等指令,该怎么办呢?
v-if 可以使用v-show来代替,如果非要实现 v-if 的话,需要使用三元表达式来代替,而 v-for 需要使用 array.map 来代替:
<ul v-if="items.length">
<li v-for="item in items">{
{ item.name }}li>
ul>
<p v-else>No items found.p>
// v-show
props: ['show', 'list'],
return (
<ul v-show={
this.show}></ul>
<p v-show={
!this.show}>No items found.</p>
)
// 三元表达式
return (
this.show
? <ul>
{
list.map(item => {
return <li>{
item}</li>
})
}
</ul>
: <p v-show={
!this.show}>No items found.</p>
)
// array.map
return (
<ul>
{
list.map(item => {
return <li>{
item}</li>
})
}
</ul>
)
你可能还会想问 v-model 呢?其实 v-model 只是 value 和 onInput 的语法糖而已
export default {
data() {
return {
name: ''
}
},
methods: {
// 监听 onInput 事件进行赋值操作
handleInput(e) {
this.name = e.target.value
}
},
render() {
// 传递 value 属性 并监听 onInput事件
return <input value={
this.name} onInput={
this.handleInput}></input>
}
}
同样, .sync 也是一个语法糖
export default {
methods: {
handleChangeVisible(value) {
this.name = value
}
},
render() {
return (
<custom-component
title="测试.sync"
name={
this.name}
on={
{
'update:name': this.handleChangeName }}
></custom-component>
)
}
}
在模板中,我们通过 v-bind:prop=“value” 或 :prop=“value” 来绑定属性:
render() {
return(
<input value={
this.value} />
)
}
在模板中,我们用 v-html 指令来更新元素的 innerHTML 内容,而在 JSX 里面,就需要用到 domPropsInnerHTML :
export default {
data() {
return {
content: '这是一段html代码'
}
},
render() {
// v-html 指令在JSX中是 domPropsInnerHTML={html}
return <div domPropsInnerHTML={
this.content}></div>
}
}
在模板中,我们常用 { {text}} 或 v-text 来输出我们的文本内容,而在 JSX 中,我们可以使用 domPropsInnerText 或者 {text} :
export default {
data() {
return {
content: '这是一段文本代码'
}
},
render() {
// v-text 指令在JSX中是 domPropsInnerText={text}
return <div domPropsInnerHTML={
this.content}></div>
// 也可以
return <div>{
this.content}</div>
}
}
在 JSX 中,如果要使用事件修饰符,有两种方法:
// .stop: 阻止事件冒泡, 可以使用 event.stopPropagation()
// .prevent: 阻止默认行为,可以受用 event.preventDefault()
// .self:只当事件是从侦听器绑定的元素本身触发时才触发回调,使用下面的条件判断进行代替
if (event.target !== event.currentTarget){
return
}
另外其他的修饰符,Vue也提供了前缀语法:
修饰符 | 前缀 |
---|---|
.passive | & |
.capture | ! |
.once | ~ |
.capture.once 或 .once.capture | ~! |
使用方法如下
render() {
return (
<div
on={
{
'!click': this.handleClick,
'~input': this.handleInput,
'&mousedown': this.handleMouseDown,
'~!mouseup': this.handleMouseUp
}}
></div>
)
}
插槽分为:默认、具名、作用域三种,可以通过 this.$slots 访问插槽的内容,每个插槽都是一个 VNode 数组,。
this.$slots上面就挂载了一个这个组件内部的所有插槽,使用 this.$slots.default 就可以将默认插槽加入到组件内部
export default {
render() {
return (
<div class="custom-component">
{
this.$slots.default}
</div>
)
}
}
有时候我们一个组件可能需要多个插槽,这个时候就需要为每一个插槽起一个名字,比如头部的 header ,底部的 footer。
render() {
return (
<div>
<div slot="header">头部</div>
<div slot="footer">底部</div>
</div>
)
}
在使用具名插槽时,如何将对应的内容插入到对应的区域呢?需要使用 this.$slots.插槽名称 :
render() {
return (
<div>
<header>{
this.$slots.header}</header>
<footer>{
this.$slots.footer}</footer>
</div>
)
}
有时候让插槽内容能够访问子组件中才有的数据是很有用的,这个时候就需要用到作用域插槽 scopedSlots 。
// 子组件
name: 'childComponent',
data() {
return {
name: '惠明'
}
},
render() {
return (
<div>
{
this.$scopedSlots.header({
props: this.name
})
}
{
this.$scopedSlots.default()
}
</div>
}
// 父组件
import childComponent from ...
render() {
const scopedSlots = {
// 这里的 header 是根据子组件中的命名来写的
// 结果 惠明
header: props => <header>{
props.name}</header>
// 如果是默认插槽的话
// 结果 default插槽
default: () => <div>default插槽</div>
};
return (
<child-component scopedSlots={
scopedSlots} />
}
要了解更多关于 JSX 如何映射到 JavaScript,请阅读使用文档。