Render函数
渲染DOM原理
在前面的内容我们学习到了Vue的响应式,但是只有响应式是不够的,我们要将内容渲染出来。Vue的templates就是通过Render(渲染)函数渲染出来的。
第一阶段
(生成虚拟DOM)
在Vue中,如果你把模版直接传入Vue实例,那Vue会执行完整的编译,把传入的template编译为浏览器可运行的DOM,就像你直接在DOM中编写模板一样。
如果你使用vue-cli构建项目,会用到webpack和vue-loader。它会在构建时预编译模板为可以直接解析的DOM代码(h()函数),即纯JavaScript代码。还有另一种编译模式,就是将编译器也打包进去,压缩之后会比第一种体积大一些。
上面两种情况其实都是使用Render函数生成
虚拟DOM
。
第二阶段
(生成真实DOM)
Vue会基于第一阶段的虚拟DOM转换成真实DOM。
虚拟DOM更新
第一、二阶段相当于完成了初始化,如果之后DOM需要更新呢?回顾之前讲的
autorun
函数(用于订阅发布),我们可以将虚拟DOM的代码放在里面,当数据改变时,
Render
函数会生成新的虚拟DOM,即触发我们的发布,新的虚拟DOM和旧的虚拟DOM进行比较,得出最少需要更新的节点并生成真实DOM完成一次更新。
虚拟DOM
简单来说,就是将真实的DOM转化成JS代码,既然都是JS代码,虚拟DOM产生变化的时候也只需要修改JS的一些内容,最终再转换成真实DOM。
你可能会好奇,我本来直接操作DOM就可以,为什么要在中间加一系列操作呢?
这是由于操作真实DOM中会存在一些问题,当DOM节点非常多的时候:会有资源消耗问题和执行效率问题。
使用JavaScript操作真实DOM会非常消耗资源,因为要修改真实DOM操作的内容很多,但是如果使用虚拟DOM,你无论是如何增删修改节点,都只是在操作JS,这样会很节省资源。同样,这样只操作JS,用JS计算差异,也会比真实DOM比较差异快很多。(当然:如果你能将DOM操作到炉火纯青,保证每次对DOM的操作都是较为节约快捷的方式,那还是要比虚拟DOM快的,毕竟我们多了一步将它转换成了JS,又变回真实DOM的过程)
<div>Hello World</div>
调用Render函数转化为虚拟DOM
import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, "Hello World"))
}
Vue整体机制
现在我们来整合一下之前的所有知识
每个组件都有一个渲染函数,它包装在我们第一节实现的
autorun
函数中,当执行渲染的时候,getters收集它的依赖。同时,每个组件都有一个watcher观察者去监听,一旦数据更新或依赖的渲染属性发生变化,我们就执行收集的依赖(即渲染函数),这样每个组件即可实现自己的自动循环渲染。
Render Function API
export default{
render(h){
return h('div',{},[...])
}
}
上面是渲染函数的使用,你需要一个参数h,h是一种简写表示超脚本(HyperScript),这是一种虚拟DOM渲染函数的编写风格。就像超文本叫HTML一样,他没有什么特殊的含义,只是方便书写的表现形式而已。
接下来我们分析一下
h()
h()
接收三个函数,第一个是元素类型;第二个是参数对象();第三个是子节点
h('div','some text')
h('div',{class:'foo'},'some text')
h('div',{...},[
'some text',
h('span','bar')
])
最后一个的渲染效果就像
<div ...>some text <span> bar </span></div>
h()
同样也接受一个组件定义,这将会创建一个组件实例
import MyComponent from '...'
h(MyComponent,{
props:{...}
})
练习Render函数动态渲染标签
前面说了这么多,让我们来做一个demo真正体验一下
传入tags数组
<example :tags="['h1', 'h2', 'h3']"></example>
渲染出标签并显示元素索引值
<div>
<h1>0</h1>
<h2>1</h2>
<h3>2</h3>
</div>
如果经常使用Vue,你的第一反应可能是使用一个构造器,动态渲染
<component is="h1"></component>
但是这个组件设计的初衷是用来在组件间动态切换,而不是渲染真实的标签;如果Vue没有将这个api提供给我们,我们的开发就会受限。这也是模板的一个缺点,不如jsx灵活。开发者总不能等待vue官方提供解决办法,所以在这里我们使用
渲染函数
。
<script src="../node_modules/vue/dist/vue.js"></script>
<div id="app">
<example :tags="['h1', 'h2', 'h3']"></example>
</div>
<script>
Vue.component('example', {
props: ['tags'],
render(h) {
return h('div', {
attrs: { id: 'hello' }
}, this.tags.map((tag, i) => h(tag, i)))
},
})
new Vue({ el: '#app' })
</script>
我们通过example创建的模板就是这样
<div id="hello">
<h1>0</h1>
<h2>1</h2>
<h3>2</h3>
</div>
函数组件和状态组件
上面我们使用的是Vue的状态组件,而
函数组件
不包含state和props,你可以理解它就是一个函数。
const foo = {
functional: true,
render: h => h('div','foo')
}
函数组件特点:
- 组件不支持实例化。
- 优化更优,因为在Vue中它的渲染函数比父级组件更早被调用,但是他并不会占用很多资源,因为它没有保存数据和属性,所以它常用于优化一个有很多节点的组件。
- 容易扩展,如果你的组件只是用来接收 prop然后显示数据,或者一个没有状态的按钮,建议使用函数组件。
-
函数组件没有this,获取prop可以通过render函数的第二参数得到
render(h, context)
我们改写一下上面的demo
Vue.component('example', {
functional: true,
props: {
tags: {
type: Array,
validator (arr) { return !!arr.length }
}
},
render: (h, context) => {
const tags = context.props.tags
return h('div', context.data, tags.map((tag, index) => h(tag, index)))
}
})
new Vue({ el: '#app' })
练习Render函数动态渲染组件
渲染函数除了可以渲染普通标签外,还可以渲染组件,下面代码有
Foo
和
Bar
组件,点击
toggle
按钮的时候,切换两组件的显示状态。
<script src="../node_modules/vue/dist/vue.js"></script>
<div id="app">
<example :ok="ok"></example>
<button @click="ok = !ok"></button>
</div>
<script>
const Foo = {
functional: true,
render: h => h('div', 'foo')
}
const Bar = {
functional: true,
render: h => h('div', 'bar')
}
Vue.component('example', {
functional: true,
props: {
ok: Boolean
},
render: (h, context) => h(context.props.ok ? Foo : Bar)
})
new Vue({
el: '#app',
data: {
ok: true
}
})
</script>