校招生问我在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 函数的应用场景和使用方法。

相关推荐
陈随易3 分钟前
10年老前端,分享20+严选技术栈
前端·后端·程序员
我的小月月8 分钟前
基于Canvas实现的网页取色器功能解析
前端
芝士加22 分钟前
还在用html2canvas?介绍一个比它快100倍的截图神器!
前端·javascript·开源
阿虎儿23 分钟前
React 引用(Ref)完全指南
前端·javascript·react.js
Ratten39 分钟前
解决 error when starting dev server TypeError crypto$2.getRandomValues
前端
coding随想42 分钟前
深入浅出DOM3合成事件(Composition Events):如何处理输入法编辑器(IME)的复杂输入流程
前端
六月的雨在掘金43 分钟前
狼人杀法官版,EdgeOne 带你轻松上手狼人杀
前端·后端
Ratten1 小时前
【npm 解决】---- TypeError: crypto.hash is not a function
前端
前端小大白1 小时前
JavaScript 循环三巨头:for vs forEach vs map 终极指南
前端·javascript·面试
晴空雨1 小时前
面试题:如何判断一个对象是否为可迭代对象?
前端·javascript·面试