虚拟DOM和render()函数
虚拟DOM
vue在DOM之上增加一个抽象层来解决渲染效率的问题,这就是虚拟DOM。
虚拟DOM使用普通的JavaScript对象描述DOM元素,每一个虚拟节点都是一个VNode实例。vue在更新真实DOM前,会比较更新前后虚拟DOM结构中的有差异的部分,然后采用异步更新队列的方式将差异的部分更新到真实DOM中,从而减少了最终要在真实DOM上执行的操作次数,提高了页面渲染的效率。
render函数
vue推荐在大多数情况下使用模板构建HTML。然而在一些场景中可能需要JavaScript的编程能力,这时可以使用render函数,它比模板更接近编译器。
下面是一个问答页面,用户单击某个问题链接,跳转到对应的回答部分,也可以单击返回顶部链接,回到页面顶部。这是通过a标签锚点实现的。
下面是带有锚点的标题的基础代码:
<h1>
<a name="hello-world" href="#hello-world">hello world</a>
</h1>
如果采用组件实现上述代码,考虑到标题元素可以变化,我们将标题的级别(1~6)定义成组件的prop,这样在调用组件时就可以通过该prop动态设置标题元素的级别。组件的使用形式如下:
<anchored-heading :level="1">Hello world</anchored-heading>
接下来是组件的实现代码:
const app = vue.createApp({})
app.component(‘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>
`
props:{
level:{
type:Number,
required:true
}
}
})
虽然模板在大多数组件中都好用,但在本例中不太合适,模板代码冗长,且slot元素在每一级标题元素中都重复书写了。当添加锚元素时,我们还必须在每个v-if/v-else-if分支中再次复制slot元素。下面改用render函数重写上面的示例:
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="app">
<anchored-heading :level="3">
<a name="hello-world" href="#hello-world">
Hello world!
</a>
</anchored-heading>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({})
app.component(‘anchored-heading‘, {
render() {
const { h } = Vue
return h(
‘h‘ + this.level, // tag name
{}, // props/attributes
this.$slots.default() // array of children
)
},
props: {
level: {
type: Number,
required: true
}
}
})
app.mount(‘#app‘)
</script>
</body>
</html>
$slots用于以编程方式访问由插槽分发的内容。每个命名的插槽都有其相应的属性(例如v-slot:foo的内容将在this.$slots.foo()中找到)。this.$slots.default()属性包含了所有未包含在命名插槽的节点或v-slot:default的内容。
render函数最重要的是h()函数,它返回的并不是一个真正的DOM元素,而是一个纯JavaScript对象,其中包含向vue描述应该在页面上渲染的节点类型的信息,包括任何子节点的描述,也就是虚拟节点(VNode)。
h()函数的作用是创建VNode,可以带三个参数,第一个参数是必须的,形式为{String|Object|Function},即该参数可以是字符串(HTML标签名)、对象(组件或一个异步组件)、函数对象(解析前两者的async函数);第二个参数是可选的,形式为{object},表示一个与模板中元素属性对应的数据对象;第三个参数也是可选的,用于生成子虚拟节点,形式为{String|Array|Object},即该参数可以是字符串(文本虚拟节点)、数组(子虚拟节点的数组)、对象(带插槽的对象)。
下面是h函数可以接收的各种参数的形式:
h(
//第一个参数,必填项
‘div‘,
//第二个参数,可选
{},
//第三个参数,可选
[
‘zzd‘,
h(‘h1‘,‘一级标题‘),
h(Component,{
someprop:‘zzd‘
})
])
简单来说,h函数的第一个参数是要创建的元素节点的名字或组件;第二个参数是元素的属性集合(包括普通属性、prop、事件属性等),以对象形式给出;第三个参数是子节点的信息,以数组形式给出,如果该元素只有文本子节点,则直接以字符串形式给出即可,如果还有子元素,则继续调用h函数。
下面继续完善anchored-heading组件,将标题元素的子元素a也放到render函数中构建:
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="app">
<anchored-heading :level="3">
Hello world!
</anchored-heading>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({})
function getChildrenTextContent(children) {
return children
.map(node => {
return typeof node.children === ‘string‘
? node.children
: Array.isArray(node.children)
? getChildrenTextContent(node.children)
: ‘‘
})
.join(‘‘)
}
app.component(‘anchored-heading‘, {
render() {
// 从子节点的文本内容创建kebab-case 风格的 ID
const headingId = getChildrenTextContent(this.$slots.default())
.toLowerCase()
.replace(/\W+/g, ‘-‘) // 将非单词字符替换为短划线
.replace(/(^-|-$)/g, ‘‘) // 删除前导和尾随的短划线
return Vue.h(‘h‘ + this.level, [
Vue.h(
‘a‘,
{
name: headingId,
href: ‘#‘ + headingId
},
this.$slots.default()
)
])
},
props: {
level: {
type: Number,
required: true
}
}
})
app.mount(‘#app‘)
</script>
</body>
</html>
如果真的要用很多重复的元素或组件可以使用工厂函数:
render(){
return Vue.h(‘div‘,
Array.apply(null,{ length:20 }).map(() => {
return Vue.h(‘p‘,‘hi‘)
})
)
}
用普通JavaScript代替模板功能
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‘)
}
}
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)
})
}
v-on
render() {
return Vue.h(‘div‘,{
onClick:$event => console.log(‘clicked‘,$event.target)
})
}
事件和按键修饰符
对于.passive、.capture和.once这些事件修饰符,可以使用驼峰命名法将它们连接到事件名之后:
render() {
return Vue.h(‘input‘,{
onClickCapture:this.doThisInCapturingMode,
onKeyupOnce:this.doThisOnce,
onMouseoverOnceCapture:this.doThisOnceInCapturingMode,
})
}
插槽
通过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函数实现帖子列表
<!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: ‘《Servlet/JSP深入详解》怎么样‘, author: ‘张三‘, date: ‘2019-10-21 20:10:15‘, vote: 0},
{id: 2, title: ‘《VC++深入详解》观后感‘, author: ‘李四‘, date: ‘2019-10-10 09:15:11‘, vote: 0},
{id: 3, title: ‘《Java无难事》怎么样‘, 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>