渲染函数
渲染函数是组成完整的Vue响应式渲染系统的另一半。
Vue会将template模板编译成渲染函数,然后通过调用渲染函数生成虚拟DOM(VNode),最后将虚拟DOM渲染成真实DOM。
Vue会监听状态的变化,当状态发生变化时,会重新调用渲染函数生成新的虚拟DOM,然后通过对比新旧虚拟DOM的差异,最终只更新需要更新的真实DOM节点,从而实现高效的DOM更新。
虚拟DOM
所谓的虚拟DOM,其实本质是一个JavaScript对象,用来描述真实DOM的结构。 例如:
const VNode = {
tag: 'div',
props: {
class: 'container'
},
children: [
{
tag: 'h1',
props: {
class: 'title'
},
children: 'Hello World'
}
]
}
该虚拟DOM描述了一个div元素,其中包含一个h1元素,h1元素中包含文本内容"Hello World"。 相当于:
<div class="container">
<h1 class="title">Hello World</h1>
</div>
为什么要用虚拟DOM?
// 真实DOM
document.createElement('div')
// [Object HTMLDivElement]
// 虚拟DOM
vm.$createElement('div')
// { tag: 'div', data: { attrs: {}, ... }, children: [] }
我们知道一个DOM元素有很多属性,比如class、style、id、data-*等,如果直接操作DOM,那么就需要逐个设置这些属性,非常繁琐,且浪费资源。而虚拟DOM则将这些属性封装成一个JavaScript对象,只需要操作JavaScript对象,就可以实现DOM的更新,大大简化了操作。
注意:常见的误解是虚拟DOM让框架变快了,其实虚拟DOM只是让框架变得更简单,更高效,让开发者更方便地操作DOM,从而提高开发效率。 其实虚拟DOM只是解决了原始DOM局限性的一个方法,它提供了以声明方式去构成你想要的DOM结构。 虚拟DOM的另一个好处,它把渲染逻辑从真实DOM抽离出来,我们先计算差异,然后将这些更新应用到DOM上,这样就可以避免频繁操作DOM,提高性能。
响应性和渲染函数
我们结合前面学习的响应性和渲染函数,来理解组件更新的过程。
- template模版被编译成渲染函数
- 数据被转化成响应式数据,它的getter/setter方法被重写
- 每个组件都会有一个Watcher用于收集依赖、清理依赖、通知修改等;当数据发生变化时,setter方法被触发,通知依赖该数据的组件进行更新
- 组件的渲染函数被重新调用,生成新的虚拟DOM
- Vue将新旧虚拟DOM进行对比,找出差异
- Vue将差异应用到真实DOM上,完成组件的更新
渲染函数的使用
export default {
render(h) {
return h('div', {}, [])
}
}
hyperscript函数(h函数)是Vue渲染函数的核心,它接收三个参数:
- 第一个参数是标签名,可以是字符串或组件对象
- 第二个参数是数据对象,可选的,里面可以嵌套
props
、attrs
、style
、class
、DOM属性、事件等 - 第三个参数是子节点,可以是字符串、数组(可包含更多的h函数或字符串)
例如:
h('div', 'some text')
h('div', { class: 'foo' }, 'some text')
h('div', { ... }, [
'some text',
h('span', 'bar')
])
import MyComponent from './MyComponent.vue'
h(MyComponent, {
props: { ... }
})
练习1
使用render函数,动态渲染标签。 Example组件如下:
<div>
<h1>0</h1>
<h2>1</h2>
<h3>2</h3>
</div>
编写:
<script src="./node_modules/vue/dist/vue.min.js"></script>
<div id="app">
<Example :tags="['h1', 'h2', 'h3']"></Example>
</div>
<script>
Vue.component('Example', {
// TODO:补充完整
})
const vm = new Vue({
el: '#app'
})
</script>
答案:
<script src="./node_modules/vue/dist/vue.min.js"></script>
<div id="app">
<Example :tags="['h1', 'h2', 'h3']"></Example>
</div>
<script>
Vue.component('Example', {
props: ['tags'],
render(h) {
return h('div', this.tags.map((tag, index) => h(tag, index)))
}
})
const vm = new Vue({
el: '#app'
})
</script>
练习2
动态渲染不同的组件
<script src="./node_modules/vue/dist/vue.min.js"></script>
<div id="app">
<Example :ok="ok"></Example>
<div>
<script>
const Foo = {
// TODO:编写完整
}
const Bar = {
// TODO:编写完整
}
Vue.component('Example', {
// TODO:编写完整
})
const vm = new Vue({
el: '#app'
})
</script>
答案:
<script src="./node_modules/vue/dist/vue.min.js"></script>
<div id="app">
<Example :ok="ok"></Example>
<button @click="ok = !ok">toggle</button>
<div>
<script>
const Foo = {
render(h) {
return h('div', 'foo')
}
}
const Bar = {
render(h) {
return h('div', 'bar')
}
}
Vue.component('Example', {
props: ['ok'],
render(h) {
return this.ok ? h(Foo) : h(Bar)
}
})
const vm = new Vue({
el: '#app',
data: {
ok: true
}
})
</script>
接下来,我们进入下一个话题:插件编写