likes
comments
collection
share

vue渲染函数从入门到入土

作者站长头像
站长
· 阅读数 13

在介绍渲染函数的使用前,我们需要先了解一些官网提供的基础概念

DOM 树

在深入渲染函数之前,了解一些浏览器的工作原理是很重要的。以下面这段 HTML 为例:

<div>
  <h1>My title</h1>
  Some text content
  <!-- TODO: Add tagline -->
</div>

当浏览器读到这些代码时,它会建立一个 ”DOM 节点“ 树 来保持追踪所有内容,如同你会画一张家谱树来追踪家庭成员的发展一样。

上述 HTML 对应的 DOM 节点树如下图所示

vue渲染函数从入门到入土每个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。一个节点就是页面的一个部分。就像家谱树一样,每个节点都可以有孩子节点 (也就是说每个部分可以包含其它的一些部分)。

高效地更新所有这些节点会是比较困难的,不过所幸你不必手动完成这个工作。你只需要告诉 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>

vue渲染函数从入门到入土

setup中

setup函数的两种返回值:

  1. 若返回一个对象,则对象中的属性、方法, 在模板中均可以直接使用。
  2. 若返回一个渲染函数:则可以自定义渲染内容。
<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{StringObjectFunction}一个 HTML 标签名、一个组件、一个异步组件、或一个函数式组件必须
propsObject与 attribute、prop 和事件相对应的对象。 这会在模板中用到。可选
children{StringArrayObject}子 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>

vue渲染函数从入门到入土

从以上实例我们也可以看出, 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>

vue渲染函数从入门到入土

实际上,这里的对象类型也就是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>

vue渲染函数从入门到入土

当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>

vue渲染函数从入门到入土

可以看出来,h函数写的class,style内容均渲染在了组件的最外层!

如果,我们在加几个div不认识的属性

return () => h(vNodeA,{a:"1",b:'2',style:{color:"red"},calss:'title',id:"1"})

意料之中,这些东西被渲染在了元素的最外层

vue渲染函数从入门到入土

如果,我们在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值。

vue渲染函数从入门到入土

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>

vue渲染函数从入门到入土

可以发现,这里的 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>

vue渲染函数从入门到入土

结合h函数的用法

h( tag, props, children)

参数名类型标签释义是否必须
tag{StringObjectFunction}一个 HTML 标签名、一个组件、一个异步组件、或一个函数式组件必须
propsObject与 attribute、prop 和事件相对应的对象。 这会在模板中用到。可选
children{StringArrayObject}子 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>