我正在参加「掘金·启航计划」
在介绍渲染函数的使用前,我们需要先了解一些官网提供的基础概念
DOM 树
在深入渲染函数之前,了解一些浏览器的工作原理是很重要的。以下面这段 HTML 为例:
<div>
<h1>My title</h1>
Some text content
<!-- TODO: Add tagline -->
</div>
当浏览器读到这些代码时,它会建立一个 ”DOM 节点“ 树 来保持追踪所有内容,如同你会画一张家谱树来追踪家庭成员的发展一样。
上述 HTML 对应的 DOM 节点树如下图所示
每个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。一个节点就是页面的一个部分。就像家谱树一样,每个节点都可以有孩子节点 (也就是说每个部分可以包含其它的一些部分)。
高效地更新所有这些节点会是比较困难的,不过所幸你不必手动完成这个工作。你只需要告诉 Vue 你希望页面上的 HTML 是什么,这可以是在一个模板里:
<h1>{{ blogTitle }}</h1>
或者一个渲染函数里:
render() {
return h('h1', {}, this.blogTitle)
}
render: function (h) {
return h('h1', this.blogTitle)
}
在这两种情况下,Vue 都会自动保持页面的更新,即便 blogTitle 发生了改变。
Vue的虚拟DOM转换逻辑
虚拟dom简单来说就是一个普通的JavaScript对象,包含tag,props,children三个属性。
<div id="app">
<p className="text">lxc</p>
</div>
Vue将上边的HTML代码转为虚拟DOM如下:
{
tag:"div",
props:{
id:"app"
},
children:[
{
tag:"p",
props:{
className:"text"
},
children:[
"lxc"
]
}
]
}
虚拟 DOM 树
Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。请仔细看这行代码:
return h('h1', {}, this.blogTitle)
h() 到底会返回什么呢?其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为 VNode。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。
h函数的基本使用
h函数可以在两个地方使用:
render中
vue2
<script>
export default {
name: 'App',
render(h){
return h('div',{},"okkkkkkkkkk")
}
}
</script>
注意:必须删除模板,只保留export default里面的内容
<script>
export default {
name: 'App',
render:function(h){
return h('div',{},"okkkkkk")
}
}
</script>
其他写法:
<script>
export default {
name: 'App',
render(h){
return h('div',{},"okkkkkk")
}
}
</script>
<script>
export default {
name: 'App',
render:h => h('div',{},"okkkkkk")
}
</script>
Vue3
<script >
import { h } from "vue"
export default{
render:function(){
return h ('div',null,'我是render函数里面的h函数渲染出来的')
}
}
</script>
export default{
render(){
return h ('div',null,'我是render函数里面的h函数渲染出来的')
}
}
<script >
import { h } from "vue"
export default{
render : () => h ('div',null,'我是render函数里面的h函数渲染出来的')
}
</script>
注意:必须删除模板,只保留export default里面的内容
vue2和Vue3的写法中区别在于vue2中的render函数需要参数h(h是一个函数),而vue3中render不需要参数,但是需要组件内引入h函数
实际上,每个组件编译后返回的实例vc对象中也具有render这个方法,这个方法返回的是我们所需要的虚拟节点。结构类似这样
{
render:function(){}
}
因此,创建一个组件时,我们完全可以使用一个函数式组件。这个组件是一个对象,对象中有render方法即可。
<template>
<div><vNode></vNode></div>
</template>
<script >
import { h } from 'vue';
//注意 vNode是一个对象,对象需要包含render函数
const vNode = {
render:() => h ('div',null,'我是render函数里面的h函数渲染出来的')
}
export default{
components:{ vNode:vNode},
setup: () => {return {}}
}
</script>
setup中
setup函数的两种返回值:
- 若返回一个对象,则对象中的属性、方法, 在模板中均可以直接使用。
- 若返回一个渲染函数:则可以自定义渲染内容。
<template>
<div>
22222222222222
</div>
</template>
<script >import { h } from "vue"
export default{
setup(){
return function () {
return h ('div',{},"hahahhahah")
}
},
}
</script>
<style lang="less" scoped>
</style>
<template>
<div>
22222222222222
</div>
</template>
<script >import { h } from "vue"
export default{
setup(){
return () => h ('div',{},"hahahhahah")
},
}
</script>
这两种写法都正常返回了”hahahhahah”,可以发现,setup中定义的渲染函数优先级高于template
h() 参数
h() 函数是一个用于创建 VNode 的实用程序。也许可以更准确地将其命名为 createVNode(),但由于频繁使用和简洁,它被称为 h() 。它接受三个参数:
h(tag,props,children)
参数名 | 类型 | 标签释义 | 是否必须 | ||
---|---|---|---|---|---|
tag | {String | Object | Function} | 一个 HTML 标签名、一个组件、一个异步组件、或一个函数式组件 | 必须 |
props | Object | 与 attribute、prop 和事件相对应的对象。 这会在模板中用到。 | 可选 | ||
children | {String | Array | Object} | 子 VNodes, 使用 h() 构建,如h(‘div’,{},’哈喽!’)或标签内的html字符’我是span标签里面的文字’或这一个嵌套的数组,这个数组可以继续嵌套h函数的三个参数,也可以是一个Vnode数组,如[Vnode1,Vnode2,Vnode3]有插槽的对象。 |
可选 |
第二个参数也可以不传, 此时可以将 children 作为第二个参数传入。
如果会产生歧义,可以将 null 作为第二个参数传入,将 children 作为第三个参数传入。
createVNode
<template>
<vNodeB></vNodeB>
</template>
<script >
import { createVNode, h } from 'vue';
const vNodeB = {
render:() => createVNode ('div',null,'我是render函数里面的createVNode函数渲染出来的')
}
export default{
name: "",
components:{vNodeB},
setup: () => {}
}
</script>
从以上实例我们也可以看出, createVNode()就是h函数的别名,h()是createVNode()的简写形式而已
tag参数
String类型: 传入一个html标签,将来被渲染成DOM,
Object类型: 传入一个组件
<script >
import { h } from 'vue';
import vNodeA from './components/a.vue'
export default{
components:{ vNodeA,},
setup(){
return () => h(vNodeA)
}
}
</script>
实际上,这里的对象类型也就是vNodeA组件, 其实质
Function类型:
props
当tag为一个h5标签时
<script >
import { h } from 'vue';
export default{
setup(){
return () => h('h1',{calss:'title',id:"1",style:"color:red"},'我是由h函数创造出来的')
}
}
</script>
当tag为一个组件时
<script >
import { h } from 'vue';
import vNodeA from './components/a.vue'
export default{
setup(){
return () => h(vNodeA,{calss:'title',id:"1",style:"color:red"})
}
}
</script>
<style lang="less" scoped>
</style>
<template>
<div>
<header>我是a组件标题</header>
<p style="color: blue;"> 我是a组件内容</p>
</div>
</template>
<script>
export default {
}
</script>
可以看出来,h函数写的class,style内容均渲染在了组件的最外层!
如果,我们在加几个div不认识的属性
return () => h(vNodeA,{a:"1",b:'2',style:{color:"red"},calss:'title',id:"1"})
意料之中,这些东西被渲染在了元素的最外层
如果,我们在a组件中的setup函数中打印context
<template>
<div>
<header>我是a组件标题</header>
<p style="color: blue;"> 我是a组件内容</p>
</div>
</template>
<script>
export default {
setup(props,context){
console.log(context);
}
}
</script>
会惊奇的发现,这些属性都包含在attrs中!可见,h函数的第二个参数,可以给子组件传props值。
children
String类型:作为html元素的文本内容使用
Array类型:子 VNodes, 使用 h()
构建,或者直接传入Vnode
[ 'Some text comes first.', h('h1', 'A headline'), h(MyComponent, { someProp: 'foobar' }) ]
Object类型:有插槽的对象。
渲染函数使用插槽
假如,我们的父组件给自组件传递了一个插槽内容
<template>
<div>
<children>
<span><p>标题</p></span>
<p>内容</p>
</children>
</div>
</template>
<script >
import children from './components/children.vue'
export default{
components:{children},
}
</script>
如果子组件在template中定义了slot标签,那么solt标签会被插槽内容替换
<template>
<slot></slot>
</template>
//编译后
<template>
<span><p>标题</p></span>
<p>内容</p>
</template>
但是,如果子组件没有template标签,而是用渲染函数返回了一个虚拟节点,那么插槽内容如何被替换呢?
<script>
import { h } from "vue";
export default {
setup(props,context){
return () => h( /这里如何接受插槽内容?/ )
}
}
</script>
我们知道,context是一个形参,包含四个子对象
-
- attrs
- emit
- slots
- expose
我们分别打印一下 mounted时期的vc实例对象(this)和setup里面的context看看
<script>
import { h } from "vue";
export default {
mounted(){
console.log('vc',this);
},
setup(props,context){
console.log('context: ', context);
}
}
</script>
可以发现,这里的 slots 就是 vue2写法中的 this.$slots,
且this.$slots .default() 和slots .default() 返回的恰好是父组件传递过来的 Vnode数组
<script>
import { h } from "vue";
export default {
setup(props,context){
let vNode = context.slots.default()
console.log('vNode: ', vNode);
}
}
</script>
结合h函数的用法
h( tag, props, children)
参数名 | 类型 | 标签释义 | 是否必须 | ||
---|---|---|---|---|---|
tag | {String | Object | Function} | 一个 HTML 标签名、一个组件、一个异步组件、或一个函数式组件 | 必须 |
props | Object | 与 attribute、prop 和事件相对应的对象。 这会在模板中用到。 | 可选 | ||
children | {String | Array | Object} | 子 VNodes, 使用 h() 构建,如h(‘div’,{},’哈喽!’)或标签内的html字符’我是span标签里面的文字’或这一个嵌套的数组,这个数组可以继续嵌套h函数的三个参数,也可以是一个Vnode数组,如[Vnode1,Vnode2,Vnode3]有插槽的对象。 |
可选 |
我们可以这么渲染插槽内容
<script>
import { h } from "vue";
export default {
setup(props,context){
let vNode = context.slots.default()
return () => h('div',{},vNode)
}
}
</script>
甚至去掉第二个参数
<script>
import { h } from "vue";
export default {
setup(props,context){
let vNode = context.slots.default()
return () => h('div',vNode)
}
}
</script>
上面的代码实际是这个意思
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {}
</script>
但是突然,有个问题来了,如果子组件比较复杂怎么办?
<template>
<div>
<span clsss = 'fly'>22</span>
<slot></slot>
<span><a>11</a></span>
</div>
</template>
难道我们使用渲染函数的话只能这么写吗?
<script>
import { h } from "vue";
export default {
setup(props,context){
let vNode = context.slots.default()
return () => h('div',[
h('span',{clsss:'fly'},'22'),
vNode,
h('span',{},h( 'a',{},'11')),
])
}
}
</script>
这样写也太复杂了!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
但是,有一个react的语法,叫做jsx,它允许我们直接这么写
<script>
import { h } from "vue";
export default {
setup(props,context){
let vNode = context.slots.default()
return (
<div>
<span clsss = 'fly'>22</span>
{ vNode } //虚拟节点数组
<span><a>11</a></span>
</div>
)
}
}
</script>
艾玛,太香了!!不过要记得,我们不能使用.vue文件了,必须写在jsx中,因此,我们的项目中还要引入babel-plugin-jsx插件才行。
当然,上面的代码我们可以在精简一些
<script>
export default {
setup(props,{slots}){
return (
<div>
<span clsss = 'fly'>22</span>
{ slots.default?.() } //链式函数调用
<span><a>11</a></span>
</div>
)
}
}
</script>
原文链接:https://juejin.cn/post/7246777363256803385 作者:石小石Orz