十二、虚拟 DOM 和 render() 函数(1)

本章概要

  • 虚拟DOM
  • render()函数

Vue.js 之所以执行性能高,一个很重要的原因就是它的虚拟 DOM 机制。

12.1 虚拟 DOM

浏览器在解析 HTML 文档时,会将文档中的元素、注释、文本等标记按照它们的层级关系组织成一棵树,这就是熟知的 DOM 树。元素、文本等是作为一个个 DOM 节点而存在的,对元素、文本的操作就是对 DOM 节点的操作。

一个元素要想呈现在页面中,必须在 DOM 树中存在该节点,这也是在使用 DOM API 创建元素后,一定要将该元素节点添加到现有 DOM 树中的某个节点下才能渲染到页面中的原因。同样的,删除某个元素实际上就是从 DOM 树中删除该元素对应的节点。每一次对 DOM 的修改都会引起浏览器对网页的重新渲染,这个过程是比较耗时的。

因为早期的 Web 应用中页面的局部刷新不会很多,所以对 DOM 进行操作的次数也就比较少,对性能的影响微乎其微,而现阶段由于单页应用程序的流行,页面的跳转、更新等都是在同一个页面中完成的,自然对 DOM 的操作也就愈发频繁,作为一款优秀的前端框架,必然要考虑 DOM 渲染效率的问题。

Vue.js 2.0 与 React 采用了相同的方案,在 DOM 之上增加一个抽象层来解决渲染效率的问题,这就是虚拟 DOM 。Vue 3.0 重写了虚拟 DOM 的实现,性能更加优异。虚拟 DOM 使用普通的 JavaScript 对象描述 DOM 元素,在 Vue.js 中,每一个虚拟节点都是一个 VNode 实例。

虚拟 DOM 是普通的 JavaScript 对象,访问 JavaScript 对象自然比访问真实 DOM 要快很多。Vue 在更新真实 DOM 钱,会比较更新前后虚拟 DOM 结构中有差异的部分,然后采用异步封信队列的方式将差异部分更新到真实 DOM 中,从而减少了最终要在真实 DOM 上执行的操作次数,提高了页面渲染的效率。

12.2 render() 函数

Vue 推荐在大多数情况下使用模板构建 HTML 。然而,在一些场景中,可能需要 JavaScript 的编程能力,这时可以使用 render() 函数,它比模板更接近编译器。

后面做一个实际应用中的例子。一个问答页面,用户可以单击某个问题链接,跳转到对应的回答部分,也可以单击“返回顶部”链接,回到页面顶部。这可以通过 a 标签的锚链接实现的。

带有锚点的标题的基础代码

<h1>
    <a name="hello-world" href="#hello-world">
        Hello world!
    </a>
</h1>

如果采用组件实现上述代码,考虑到标题可以变化(h1-h6),将标题的级别(h1-h6)定义成组件的 prop ,这样在调用组件时,就可以通过 prop 动态设置标题元素的级别。组件的使用形式如下。

<anchored-heading :level="1">Hello world!</anchored-heading>

接下来就是组件的实现代码。

const app = Vue.createApp({})
app.component('anchored-heading',{
  template:`
    <h1 v-if="level === 1">
        <slot></slot>
    </h1>
    <h2 v-else-if="level === 2">
        <slot></slot>
    </h2>
    <h3 v-else-if="level === 2">
        <slot></slot>
    </h3>
    <h4 v-else-if="level === 2">
        <slot></slot>
    </h4>
    <h5 v-else-if="level === 2">
        <slot></slot>
    </h5>
    <h6 v-else-if="level === 2">
        <slot></slot>
    </h6>
  `,
  props:{
    level:{
      type:Number,
      required:true
    }
  }
})

虽然模板在大多数组件中都非常好用,但在此处不太合适,模板代码冗长,且 slot 元素在每一级元素中都重复书写了。当添加锚元素时,还必须在每个 v-if/v-else-if 分支中再次复制 slot 元素。

下面改用 render() 函数重写上述代码。如下:

const app = Vue.createApp({})
app.component('anchored-heading', {
    render() {
        const { h } = Vue

        return h(
            'h' + this.level, // tag name
            {}, // props/attributes
            this.$slots.default() // array of children
        )
    },
    props: {
        level: {
            type: Number,
            required: true
        }
    }
})

slots 用于以编程方式访问由插槽分发的内容。每个命名的插槽都有其相应的属性(例如,v-solt:foo 的内容将在 this.slots.foo() 中找到)。this.slots.default() 属性包含了所有未包含在命名插槽中的节点或 v-solt:default 的内容。
组件的调用代码如下:

<anchored-heading :level="3">
    <a name="hello-world" href="#hello-world">
        Hello world!
    </a>
</anchored-heading>

完整代码如下:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title></title>
</head>

<body>

    <div id="app">
        <anchored-heading :level="3">
            <a name="hello-world" href="#hello-world">
                Hello world!
            </a>
        </anchored-heading>
    </div>

    <script src="https://unpkg.com/vue@next"></script>
    <script>
        const app = Vue.createApp({})
        app.component('anchored-heading', {
            render() {
                const { h } = Vue

                return h(
                    'h' + this.level, // tag name
                    {}, // props/attributes
                    this.$slots.default() // array of children
                )
            },
            props: {
                level: {
                    type: Number,
                    required: true
                }
            }
        })
        app.mount('#app')
    </script>
</body>

</html>

渲染结果如下:
在这里插入图片描述

render() 函数中最重要的就是 h() 函数了,下面是 return 语句的代码:

return h(
    'h' + this.level, // tag name
    {}, // props/attributes
    this.$slots.default() // array of children
)
  • h() 函数返回的并不是一个真正的 DOM 元素,它返回的是一个纯 JavaScript 对象,其中包含想 Vue 描述应该在页面上渲染的节点类型的信息,包括任何子节点的描述,也就是虚拟节点(VNode)。
  • h() 函数的作用是创建 VNode ,或许将其命名为 createVNode() 更准确,但由于频繁且为了简洁,将其命名为 h()。
  • h() 函数可以带 3 个参数,第一个参数是必须的,形式为 {String|Object|Function} ,即该参数可以是字符串(HTML 标签名)、对象(组件或一个异步组件)、函数对象(解析前两者之一的 async 函数);第二个参数是可选的,形式为 {Object},表示一个与模板中元素属性对应的数据对象;第三个参数也是可选的,用于生成子虚拟节点,形式为 {String|Array|Object},即该参数可以是字符串(文本虚拟节点)、数组(子虚拟节点的数组)、对象(带插槽的对象)。

以下是 h() 函数可以接收的各种参数的形式:

// @returns {VNode}
h(
  // ----------------第一个参数,必填项----------------
  // {String|Object|Function} tag
  // 一个HTML标签名、组件或异步组件,或者解析上述任何一种的一个 async() 函数
  'div',
  // ----------------第二个参数,可选----------------
  // {Object} props
  // 一个与模板中元素属性(包括普通属性、prop 和事件属性)对应的数据对象
  {},
  // ----------------第三个参数,可选----------------
  // {String|Array|Function} children
  // 子虚拟节点(VNode) 由 h() 函数构建而成
  // 也可以使用字符串来生成“文本虚拟节点”或带有插槽的对象
  [
    '先写一些文本',
    h('h1','一级标题'),
    h(MyComponent,{
      someProp:'foobar'
    })
  ]
)

简单来说:
h() 函数的第一个参数是要创建的元素节点的名字(字符串形式)或组件(对象形式);
第二个参数是元素的属性集合(包括普通属性、prop、事件属性等),以对象形式给出;
第三个参数是子节点信息,以数组形式给出,如果该元素只有文本子节点,则直接以字符串形式给出即可,如果还有子元素,则继续调用 h() 函数。

下面继续完善 anchored-heading 组件,将标题元素的子元素 a 也放到 render() 函数中构建。如下:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title></title>
</head>

<body>
    <div id="app">
        <anchored-heading :level="3">
            Hello world!
        </anchored-heading>
    </div>

    <script src="https://unpkg.com/vue@next"></script>
    <script>
        const app = Vue.createApp({})

        function getChildrenTextContent(children) {
            return children
                .map(node => {
                    return typeof node.children === 'string'
                        ? node.children
                        : Array.isArray(node.children)
                            ? getChildrenTextContent(node.children)
                            : ''
                })
                .join('')
        }

        app.component('anchored-heading', {
            render() {
                // 从子节点的文本内容创建kebab-case 风格的 ID
                const headingId = getChildrenTextContent(this.$slots.default())
                    .toLowerCase()
                    .replace(/\W+/g, '-')     // 将非单词字符替换为短划线
                    .replace(/(^-|-$)/g, '')  // 删除前导和尾随的短划线

                return Vue.h('h' + this.level, [
                    Vue.h(
                        'a',
                        {
                            name: headingId,
                            href: '#' + headingId
                        },
                        this.$slots.default()
                    )
                ])
            },
            props: {
                level: {
                    type: Number,
                    required: true
                }
            }
        })
        app.mount('#app')
    </script>
</body>

</html>

渲染结果如下:
在这里插入图片描述

组件树中的所有 VNode 必须是唯一的。例如下面的 render() 函数是不合法的。

rener(){
  const myVNode = Vue.h('p','hi')
  return Vue.h('div',[
    // 错误 : 重复的 VNode
    myVNode,myVNode
  ])
}

如果真的需要重复很多相同元素或组件,可以使用工厂函数实现。例如,下面的 render() 函数用完全合法的方式渲染了 20 个相同的段落。

rener() {
  return Vue.h('div',
    Array.apply(null,{length:20}).map(() => {
      return Vue.h('p','hi')
    })
  )
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只小熊猫呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值