本章概要
- 用普通 JavaScript 代替模板功能
- v-if 和 v-for
- v-model
- v-on
- 事件和按键修饰符
- 插槽
- JSX
- 实例:帖子列表
12.3 用普通 JavaScript 代替模板功能
原先在模板中可以使用的一些功能在 render() 函数中没有再提供,需要自己编写 JavaScript 代码来实现。
12.3.1 v-if 和 v-for
只要普通 JavaScript 能轻松完成的操作,Vue 的 render() 函数就没有提供专有的替代方案。例如,在使用 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>
在 render() 函数中可以使用 JavaScript 的 if/else 和 map 实现相同的功能。如下:
props:['items'],
render(){
if(this.items.length){
return Vue.h('ul',this.items.map((item) => {
return Vue.h('li',item.name)
}))
}else{
return Vue.h('p','No items found.')
}
}
12.3.2 v-model
在 render() 函数中没有与 v-model 指令直接对应的实现方案,不过v-model 指令在模板编译期间会被扩展为 modelValue 和 onUpdate:modelValue prop ,按照 v-model 的内在逻辑,自己实现即可,如下:
props:['modelValue'],
render(){
return Vue.h(SomeComponent,{
modelValue:this.modelValue,
'onUpdate:modelValue':value => this.$emit('update:modelValue',value)
})
}
12.3.3 v-on
必须为事件处理程序提供一个正确的 prop 名称。例如,要处理 click 事件,prop 名称应该是 onClick。代码如下:
render(){
return Vue.h('div',{
onClick:$event => console.log('clicked',$event.target)
})
}
12.3.4 事件和按键修饰符
对于 .passive、.capture 和 .once 这些事件修饰符,可以使用驼峰命名法将他们连接到事件名之后。如下:
render(){
return Vue.h('input',{
onClickCapture:this.doThisInCapturingMode,
onKeyupOnce:this.doThisOnce,
onMouseoverOnceCapture:this.doThisOnceInCapturingMode
})
}
对于其它的事件和按键修饰符,则不需要特殊的 API ,因为在处理程序中可以使用事件方法实现相同的功能,如下:
与修饰符等价的事件方法
修饰符 | 处理函数中的等价操作 |
---|---|
.stop | event.stopPropagation() |
.prevent | event.preventDefault() |
.self | if(event.target!== event.currentTarget) return |
按键:.enter、.13 | if(event.keyCode!==13) return (对于其他的按键修饰符,可将13改为其对应的按键码) |
修饰符:.ctrl、.alt、.shift、.meta | if(!event.ctrlKey) return (可将 ctrlKey 分别修改为 altKey、shiftKey、mateKey) |
下面是一个使用所有修饰符的例子:
render(){
return Vue.h('input',{
onKeyUp:event => {
// 如果触发事件的元素不是事件绑定的元素,则返回
if(event.target !== event.currentTarget) return
// 如果按下的不是 Enter 键(13)或没有同事按下 Shift 键,则返回
if(!event.shiftKey || event.keyCode !== 13) return
// 阻止事件传播
event.stopPropagation()
// 阻止该元素默认的 keyup 事件处理
event.preventDefault()
// ...
}
})
}
12.3.5 插槽
通过 this.slots 可以访问插槽的内容,插槽的内容是 VNode 数组。代码如下:
render(){
// `<div><slot></slot></div>`
return Vue.h('div',{},this.$slots.default())
}
// 访问作用域插槽
props:['message'],
render(){
// `<div><slot :text="message"></slot></div>`
return Vue.h('div',{},this.$slots.default({
text:this.message
}))
}
如果要使用 render() 函数将插槽传递给子组件,可以编写下面的代码:
render (){
// `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`
return Vue.h('div',[
Vue.h(
Vue.resolveComponent('child'),
{},
// 将 slots 作为子对象传递
// 格式为:{ name:props => VNode | Array<VNode> }
{
default:(props) => Vue.h('span',props.text)
}
)
])
}
12.4 JSX
这时候会发现,即使是简单的模板,在 render() 函数中编写也很复杂,而且模板中的 DOM 结构面目全非,可读性很差。当模板比较复杂,元素之间嵌套的层级较多时,在 render() 函数中一层层嵌套的 h() 函数也令人迷惑。
React 的 render() 函数使用 JSX 语法来简化模板的编写,使模板的编写变得和普通 DOM 模板一样简单。在 Vue 中,可以通过一个 Babel 插件让Vue 支持 JSX 语法,从而简化 render() 函数中的模板创建。
提示:
JSX 的全称是 JavaScript XML ,是一种 JavaScript 的语法扩展,用于描述用户界面。其格式比较像是模板语言,但事实上完全是在 JavaScript 内部实现的。
例如:对于下面 DOM 结构:
<anchored-heading :level="1">
<span>hello</span> world
</anchored-heading>>
不使用 JSX 语法的 render() 函数实现如下:
Vue.h(Vue.resolveComponent('anchored-heading'),
{
level:1
},
{
default:() => [Vue.h('span','hello'),'world']
}
)
使用 JSX 语法的 render() 函数实现如下:
import AnchoredHeading from './AnchoredHeading.vue'
const app = createApp({
render(){
<AnchoredHeading level={1}>
<span>hello</span> world
</AnchoredHeading>
}
})
app.mount('#demo')
12.5 实例:使用 render() 函数实现帖子列表
首先是单个帖子的组件 PostListItem,如下:
// 子组件
app.component('PostListItem', {
props: {
post: {
type: Object,
required: true
}
},
render() {
return Vue.h('li', [
Vue.h('p', [
Vue.h('span',
// 这是<span>元素的内容
'标题:' + this.post.title + ' | 发帖人:' + this.post.author + ' | 发帖时间:' + this.post.date + ' | 点赞数:' + this.post.vote
),
Vue.h('button', {
onClick: () => this.$emit('vote')
}, '赞')
]
)
]
);
}
});
一定要清楚 h() 函数的 3 个参数的作用,因为后面两个参数都是可选的,所以要主义区分哪部分是第二个参数传参,哪部分是第三个参数传参。
简单的区分方式就是看对象传参还是数组传参,如果是对象传参,就是第二个参数(设置元素的属性信息);如果是数组传参,就是第三个参数(设置子节点信息)。
完整代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="app">
<post-list></post-list>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({})
// 父组件
app.component('PostList', {
data() {
return {
posts: [
{ id: 1, title: '华为meate40怎么样', author: '张三', date: '2019-10-21 20:10:15', vote: 0 },
{ id: 2, title: '华为meate40 pro 怎么样', author: '李四', date: '2019-10-10 09:15:11', vote: 0 },
{ id: 3, title: '华为p40怎么样', author: '王五', date: '2020-11-11 15:22:03', vote: 0 }
]
}
},
methods: {
// 自定义事件vote的事件处理器方法
handleVote(id) {
this.posts.map(item => {
item.id === id ? { ...item, voite: ++item.vote } : item;
})
}
},
render() {
let postNodes = [];
// this.posts.map取代v-for指令,循环遍历posts,
// 构造子组件的虚拟节点
this.posts.map(post => {
let node = Vue.h(Vue.resolveComponent('PostListItem'), {
post: post,
onVote: () => this.handleVote(post.id)
});
postNodes.push(node);
})
return Vue.h('div', [
Vue.h('ul', [
postNodes
]
)
]
);
},
});
// 子组件
app.component('PostListItem', {
props: {
post: {
type: Object,
required: true
}
},
render() {
return Vue.h('li', [
Vue.h('p', [
Vue.h('span',
// 这是<span>元素的内容
'标题:' + this.post.title + ' | 发帖人:' + this.post.author + ' | 发帖时间:' + this.post.date + ' | 点赞数:' + this.post.vote
),
Vue.h('button', {
onClick: () => this.$emit('vote')
}, '赞')
]
)
]
);
}
});
app.mount('#app')
</script>
</body>
</html>
渲染结果如下: