校招生问我在vue中,什么时候该用 render 函数?

前言

最近有一位校招生入职两个多月了,开始参与组件库封装工作,其中就包含了 <overflow-list><space> 组件,这两个组件本身并不难。

他在实现过程中遇到了一些困惑,特别是对于何时该使用 render 函数感到迷茫。

我建议他先参考其他成熟组件库的实现方式,并结合 render 函数来完成需求。

他又问:那什么时候该用 render 函数?

什么时候该用 render 函数?

在 Vue 中,render 函数是一种用 JavaScript 代码来描述组件渲染内容的方式,它是 Vue 模板编译后的底层实现形式。

简单来说,template 模板最终会被编译成 render 函数,而 render 函数的执行结果会生成虚拟 DOM(VNode),Vue 再根据虚拟 DOM 渲染出真实 DOM。

如果你有如下三种场景,那么你就可以考虑使用 render 函数来实现。

需要更高的灵活性和控制权时

模板语法是声明式的,它描述的是"在某种状态下,视图应该是什么样子"。

Render 函数是命令式的,它用 JavaScript 的完整能力来"命令"Vue 如何构建视图。

这种根本性的差异使得后者对 VNode 具有极致控制性和灵活性。

比如前面提到的 <overflow-list> 就符合这个场景,<space> 组件比较简单,不过多介绍。

<overflow-list> 折叠列表

  • 作用 :根据当前的宽度下动态判断能正常展示多少个子组件,无法展示的子组件个数通过一个 tag 来表示被收起
  • 用法

    html 复制代码
    <overflow-list>
        <div v-for="item of tags" :key="item">Tag{{item}}</div>
        <template #overflow>
         <div>自定义tag</div>
        </template>
    </verflow-list>

组件内部需要做什么?

从组件的用法上来看,在组件内部需要做下面几件事:

  • 获取 默认插槽 中的所有子节点,为不同子节点间设置间隔 margin
    • 子节点间要实现两两间隔,可以使用 marginpadding 实现,但最好不要直接修改子节点上的样式,应该要为每个子节点包裹一层 <div class="overflow-list-item"> 容器,所有的样式变更都在它上面设置
  • 父容器宽度 发生变化时,计算当前最大能够显示子节点的个数,其他的通过 tag 的方式,表示还有剩余未展示
    • 可以通过 ResizeObserver API 实现对当前容器宽度的监听,然后进行计算
  • 支持通过 from 设置折叠方向,意味着 自定义折叠元素 的位置会发生变化
  • 通过 overflow 插槽 支持外部自定义折叠元素

基于以上要完成的内容,如果直接使用 template 模版是不好实现的,render 函数版本如下:

js 复制代码
<script>
import { h, onBeforeUnmount, onMounted, ref } from 'vue'

export default {
  name: 'OverflowList',
  props: {
    from: {
      type: String,
      default: 'end',
    },
    margin: {
      type: Number,
      default: 8,
    },
  },
  emits: ['change'],
  setup(props, { slots, emit }) {
    const showCount = ref(Infinity)
    let childrenWidths = []

    const overflowListRef = ref(null)

    const initObserve = (el, callback) => {
      // 保存初始子节点宽度
      childrenWidths = Array.from(el.children).map((child) => child.clientWidth)

      const resizeObserver = new ResizeObserver(callback)
      resizeObserver.observe(el)
      onBeforeUnmount(() => resizeObserver.unobserve(el))
    }

    const resizeAction = debounce(() => {
      const parentWidth = overflowListRef.value.clientWidth

      let count = 0,
        currentWidth = 0

      for (const childrenWidth of childrenWidths) {
        currentWidth += childrenWidth

        if (currentWidth < parentWidth) {
          count++
        } else {
          break
        }
      }

      showCount.value = count

      emit('change', showCount.value)
    })

    onMounted(() => {
      initObserve(overflowListRef.value, resizeAction)
    })

    return () => {
      console.log('render')

      // 获取默认插槽子节点集合,作为初始值
      let newChildren = slots.default()[0].children.slice()

      //  计算出折叠元素的个数
      const overflowCount = newChildren.length - showCount.value

      let hasReverse = false

      //  有折叠元素,才需要 tag
      if (overflowCount > 0) {
        const overflowChild = slots.overflow
          ? slots.overflow()[0]
          : h('div', { class: 'overflow-tag' }, `+${overflowCount + 1}`)

        // 定义 overflow 元素位置
        if (props.from === 'start') {
          newChildren.push(overflowChild)
          // 翻转是为了方便下面的判断
          newChildren.reverse()
          hasReverse = true
        } else {
          newChildren.unshift(overflowChild)
        }
      }

      // 根据 showCount 值计算能够显示多少子元素,多余的就不需要渲染
      newChildren = newChildren.filter((child, index) => {
        if (index + 1 > showCount.value) return null
        return child
      })

      // 处理完成,恢复子节点顺序
      if (hasReverse) {
        newChildren.reverse()
      }

      // 为每个子节点包裹一层容器,并设置间距
      newChildren = newChildren.map((child, index) => {
        const margin = (index + 1 < newChildren.length ? props.margin : 0) + 'px'

        return h(
          'div',
          {
            class: 'overflow-list-item',
            style: {
              marginRight: margin,
            },
          },
          child,
        )
      })

      // 最终渲染
      return h('div', { class: 'overflow-list', ref: overflowListRef }, newChildren)
    }
  },
}
</script>

效果如下:

在运行时,动态生成模板时

如果你在某个 js、ts 文件中,想要在运行某段逻辑之后要展示某些视图(如 Notification ),并且想要自定义其对应的 title、content、footer 时,也可以使用 render 函数来实现。

如下:

js 复制代码
import { h } from 'vue'
import HandTitle from './HandTitle.vue'
import HandContent from './HandContent.vue'

function action(data) {
    Notification.info({
        id, 
        showIcon: false,
        position: 'bottomLeft',
        closable: false, 
        duration: 0, 
        style: { width: '260px' },
        title: () => h(HandTitle, {
            getName: getShowName(),
        }),
        content: () => h(HandContent, {
            onOk() {},
            onRefuse() {},
        })
    });
}

此处使用 render 函数的好处在于:

  • 可以在 JavaScript 逻辑中直接创建组件实例
  • 支持动态传递 props 和事件处理函数
  • 避免在模板中编写复杂的条件渲染逻辑

需要复杂的条件渲染内容时

当条件渲染逻辑非常复杂,使用模板会导致代码难以维护时,适用场景有:

  • 多重嵌套的条件渲染

  • 基于复杂业务逻辑的视图渲染

  • 需要编程式生成大量相似但略有不同的元素

比如实现最常见 <h1>、<h2>、<h3>、<h4>、<h5>、<h6> 的组件效果。

template 模版需要如下的实现:

js 复制代码
<template>
  <h1 v-if="level === 1">{{ title }}</h1>
  <h2 v-else-if="level === 2">{{ title }}</h2>
  <h3 v-else-if="level === 3">{{ title }}</h3>
  <h4 v-else-if="level === 4">{{ title }}</h4>
  <h5 v-else-if="level === 5">{{ title }}</h5>
  <h6 v-else-if="level === 6">{{ title }}</h6>
</template>

render 函数只需要如下的实现:

js 复制代码
<script>
import { h } from 'vue'

export default {
  name: 'Title',
  props: {
    level: {
      type: Number,
      default: 1,
    },
    title: {
      type: String,
      default: '',
    },
  },
  setup(props, context) {
    return () => {
      h(`h${props.level}`, {}, props.title)
    }
  },
}
</script>

总结

render 函数是 Vue 提供的强大工具,它在以下场景中特别有用:

  • 需要高度控制组件渲染逻辑时
  • 需要根据运行时数据动态生成组件结构时
  • 需要实现复杂的条件渲染时
  • 开发可复用组件库时

对于刚接触 Vue 的开发者,建议先从模板语法开始,当遇到模板无法优雅解决的问题时,再考虑使用 render 函数。

随着经验的积累,你会逐渐体会到 render 函数在复杂场景下的价值。

希望这篇文章能帮助你更好地理解 Vue render 函数的应用场景和使用方法。

相关推荐
IT·小灰灰15 小时前
深度解析重排序AI模型:基于硅基流动API调用多语言重排序AI实战指南
java·大数据·javascript·人工智能·python·数据挖掘·php
我叫张小白。15 小时前
Vue3 Props 的使用:组件间数据传递的桥梁
前端·javascript·vue.js·vue3
r***869815 小时前
Nginx解决前端跨域问题
运维·前端·nginx
广州华水科技15 小时前
单北斗GNSS在桥梁变形监测中的关键应用与技术优势分析
前端
IT_陈寒15 小时前
Python 3.12新特性实战:10个让你效率翻倍的代码优化技巧
前端·人工智能·后端
z***948415 小时前
Redis 6.2.7安装配置
前端·数据库·redis
2301_8072886315 小时前
MPRPC项目制作(第四天)
java·服务器·前端
J***793915 小时前
前端在移动端中的React Native Windows
前端·react native·react.js
阿雄不会写代码15 小时前
PPTX报错AttributeError: module ‘collections‘ has no attribute ‘Container‘
前端
前端程序猿i15 小时前
前端判断数据类型的所有方式详解
开发语言·前端·javascript