Virtual DOM 就是用 JavaScript 对象来描述真实的 DOM 节点。
例如,有这样一段 HTML:
<div id="div">
<button @click="click">click</button>
</div>
用 Virtual DOM 来表示可以是这样的(为什么说可以是这样,因为这完全取决于你的设计):
const vDom = {
tag: 'div',
id: 'div',
children: [{
tag: 'button',
onClick: this.click
}]
}
- 可跨平台,Virtual DOM 使组件的渲染逻辑和真实 DOM 彻底解耦,因此你可以很方便的在不同环境使用它,例如,当你开发的不是面向浏览器的,而是 IOS 或 Android 或小程序,你可以利用编写自己的
render
函数,把 Virtual DOM 渲染成自己想要的东西,而不仅仅是 DOM。 - 可程序式修改,Virtual DOM 提供了一种可以通过编程的方式修改、检查、克隆 DOM 结构的能力,你可以在把 DOM 返回给渲染引擎之前,先利用基本的 JavaScript 来处理好。
- 提升性能,当页面中有大量的 DOM 节点操作时,如果涉及到了浏览器的回流和重绘,性能是十分糟糕的,就像第二条说的,在 DOM 返回给渲染引擎之前,我们可以先用 JavaScript 处理 Virtual DOM,最终才返回真实 DOM,极大减少回流和重绘次数。
首先我们知道,当你在编写 Vue 组件或页面时,一般会提供一个 template
选项来写 HTML 内容。根据Vue 3 总览这一部分内容的介绍,Vue 会先走编译阶段,把 template
编译成 render
函数,所以说最终的 DOM 一定是从 render
函数输出的。因此 render
函数可以用来代替 template
,它返回的内容就是 VNode
。直接使用 render
反而可以省去 complier
过程。
Vue 中提供的 render
函数是非常有用的,因为有些情况用 template
来表达业务逻辑会一定程度受到限制,这种情况你需要一种比较灵活的编程方式来表达底层的逻辑。
例如,当你有一个需求是大量的文本输入框,这中需求你要写的标签并不多,但是却揉了大量的交互逻辑,你需要在模板上添加大量的逻辑代码(比如控制关联标签的显示),然而,你的 JavaScript 代码中也有大量的逻辑代码。
render
函数的存在可以让你在一个地方写业务逻辑,这时你就不用太多的去考虑标签的问题了。
有一段 template
如下:
template: '<div id="foo" @click="onClick">hello</div>'
Vue 3 中用 render
函数实现如下:
import { h } from 'vue'
render() {
return h('div', {
id: 'foo',
onClick: this.onClick
}, 'hello')
}
由于这是纯粹的 JavaScript,所以如果你需要实现 template
中类似 v-if
、v-for
这样的功能,直接通过三元表达式做到。
import { h } from 'vue'
render() {
let nodeToReturn
// v-if="ok"
if(this.ok) {
nodeToReturn = h('div', {
id: 'foo',
onClick: this.onClick
}, 'ok')
} else {
// v-for="item in list"
const children = this.list.map(item => {
return h('p', {
key: item.id
}, item.text)
})
nodeToReturn = h('div', {}, children)
}
return nodeToReturn
}
这就是 render
基本使用用法,就是 JavaScript 代码而已。
一般来说我们用 tempate
可以满足大多数场景了,但是你一定了解过 slot
这个东西,如果只使用 tempate
你将无法操作 slot
中的内容,如果你需要程序式地修改传进来的 slot
内容,你就必须用到 render
函数了(这也是大多数使用 render
函数的场景)。
下面我们用一个例子来说明。
比如我们要实现这样一个组件:实现层级缩进效果,即类似 HTML 中嵌套的 UL 标签,看起来就像这样:
level 1
level 1-1
level 1-2
level 1-2-1
level 1-2-2
我们的模板是这样写的,实际上 Stack
组件就是给每一个 slot
都增加一个左边距:
<Stack size="10">
<div>level 1</div>
<Stack size="10">
<div>level 1-1</div>
<div>level 1-2</div>
<Stack size="10">
<div>level 1-2-1</div>
<div>level 1-2-2</div>
</Stack>
</Stack>
</Stack>
现在我们只用 template
是无法实现这种效果的,众所周知,template
只能把默认的 slot
渲染出来,它不能程序式处理 slot
的值。
我们先用 template
来实现这个组件,stack.html:
const Stack = {
props: {
size: [String, Number]
},
template: `
<div class="stack">
<slot></slot>
</div>
`
}
这样由于不能处理 slot
内容,那么它的表现效果如下,并没有层级缩进:
level 1
level 1-1
level 1-2
level 1-2-1
level 1-2-2
我们现在尝试用 render
函数实现 Stack
组件:
const { h } = Vue
const Stack = {
props: {
size: [String, Number]
},
render() {
const slot = this.$slots.default
? this.$slots.default()
: []
return h('div', { class: 'stack' },
// 这里给每一项 slot 增加一个缩进 class
slot.map(child => {
return h('div', { class: `ml${this.$props.size}` }, [ child ])
}))
}
}
render
函数中我们可以通过 this.$slots
拿到插槽内容,通过 JavaScript 把它处理成任何我们想要的东西,这里我们给每一项 slot
添加了一个 margin-left: 10px
缩进,看下效果:
level 1
level 1-1
level 1-2
level 1-2-1
level 1-2-2
完美,我们实现了一个用 template
几乎实现不了的功能。
原则:
- 一般来说开发一些公共组件时才会用到
render
- 当你发现用 JavaScript 才能更好的表达你的逻辑时,那么就用
render
函数 - 日常开发的功能性组件使用
template
,这样更高效,且template
更容易被complier
优化
你可能会想,为什么不直接编译成 VNode
,而要在中间加一层 render
呢?
这是因为 VNode
本身包含的信息比较多,手写太麻烦,也许你写着写着不自觉就封装成了一个 helper
函数,h
函数就是这样的,它把公用、灵活、复杂的逻辑封装成函数,并交给运行时,使用这样的函数将大大降低你的编写成本。
知道了为什么要有 render
后,才需要去设计实现它,其实主要是实现 h
函数。