You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vue函数式组件与有状态组件重渲染差异问题及优化

问题

在 Vue 2.7 + Webpack 5.11 项目中,表格单元格使用函数式组件TableCell.vue时,修改任意单元格值并触发后端请求后,所有单元格会全量重渲染(createFunctionalComponent被调用802次);但切换为有状态模板组件时,仅受影响的单元格会重渲染。

表格通过v-for渲染每行单元格,单元格值变更会发起后端请求,响应结果指定需更新的单元格。核心代码如下:

Table.vue

<tr v-for="row in cells" :key="row[0].id">
  <TableCellHeader />
  <TableCell v-for="(cell, idx) in row.slice(1)"
    :key="row[0].data.id+'_'+idx"
    :cell="cell"
    :col-index="idx+1"
    :row-id="row[0].id"
    :disable="isDisabled(row[0], cell, idx+1)"
    @saved="save" />
</tr>

TableCell.vue(函数式组件)

<script>
import Stack from '@ej/components/stack';
import { updateValue } from '@ej/spo/api';
import dompurify from 'dompurify';

export default {
    functional: true,

    props: {
        cell: {
            type: Object,
            required: true,
        },
        disable: {
            type: Boolean,
            default: false,
        },
        rowId: {
            type: Number,
            required: true,
        },
    },

    render(h, { props, $style, listeners }) {
        let element;
        let setClass = (className) => {
            if (!element) return;
            let classNames = ['error', 'saved', 'saving'];
            classNames.forEach((name) => {
                element.classList.remove(name);
            });

            if (!className) return;
            element.classList.add(className);
        };

        let save = (updatedValue, cell, rowId) => {
            updateValue(updatedValue)
                .then((result) => {
                    setClass('saved');
                    setTimeout(() => {
                        setClass('');
                    }, 3000);
                    listeners.saved?.(result);
                })
                .catch(e => {
                    setClass('error');
                });
        };
        let onFocusOut = (event) => {
            element = event.target.closest('td');
            let updatedValue = event.target.innerText.trim();
            setClass('saving');
            event.target.innerHTML = dompurify.sanitize(updatedValue);
            save(updatedValue, props.cell, props.rowId);
        };

        return h('td', {
            class: [$style.cell, 'spo-table-cell'],
            attrs: {
                'data-disabled': props.disable,
            },
        }, [
            h(Stack, {
                props: {
                    verticalAlign: 'center',
                    fill: true,
                },
            }, [
                h('div', {
                    attrs: {
                        contenteditable: !props.disable,
                    },
                    class: $style.content,
                    on: {
                        blur: onFocusOut,
                    },
                    domProps: {
                        innerHTML: props.cell.value || '',
                    },
                }),
            ]),
        ]);
    },
};
</script>

<style lang="less" module>
// 样式代码
</style>

渲染行为差异的原因

  1. 函数式组件的无状态特性:Vue 2 中的函数式组件没有独立实例和响应式状态,渲染完全依赖父组件上下文与传入的props,且不做单独依赖收集。只要父组件触发任何更新(哪怕与当前组件props无关),函数式组件都会强制重新执行render函数,引发全量重渲染。
  2. 父组件模板的动态方法调用:父组件模板中:disable="isDisabled(row[0], cell, idx+1)"是实时调用方法,每次父组件渲染都会执行该方法,扩大了父组件的更新范围,进而带动所有函数式组件重渲染。而有状态组件自带依赖追踪,仅在自身props或内部状态变化时更新,不受父组件无关更新影响。
  3. 数组切片的不稳定性:row.slice(1)每次执行都会返回新数组,即便v-for设置了唯一key,函数式组件也无法像有状态组件那样复用已有实例,只能重新创建所有组件实例。

实现函数式组件局部更新的方案

1. 缓存动态计算值,避免模板实时调用方法

在父组件中预处理cells数据,将isDisabled的计算结果提前缓存到每个cell对象中:

// 父组件预处理cells
this.cells = this.cells.map(row => {
  return [
    row[0],
    ...row.slice(1).map((cell, idx) => ({
      ...cell,
      disabled: this.isDisabled(row[0], cell, idx + 1)
    }))
  ]
})

模板中直接绑定缓存好的属性:

<TableCell v-for="(cell, idx) in row.slice(1)"
  :key="row[0].data.id+'_'+idx"
  :cell="cell"
  :col-index="idx+1"
  :row-id="row[0].id"
  :disable="cell.disabled"
  @saved="save" />

2. 稳定数组引用,避免重复生成新数组

将row.slice(1)的结果缓存到row对象的属性中,避免每次渲染生成新数组:

// 父组件预处理cells时添加cachedCells属性
this.cells = this.cells.map(row => ({
  ...row,
  cachedCells: row.slice(1).map((cell, idx) => ({
    ...cell,
    disabled: this.isDisabled(row[0], cell, idx + 1)
  }))
}))

模板中遍历缓存后的数组:

<TableCell v-for="(cell, idx) in row.cachedCells"
  :key="row[0].data.id+'_'+idx"
  :cell="cell"
  :col-index="idx+1"
  :row-id="row[0].id"
  :disable="cell.disabled"
  @saved="save" />

3. 精准更新数据,避免全量修改数组

后端返回更新结果后,仅修改cells中需要更新的单个cell对象,不重新生成整个数组:

// 父组件save方法中更新数据
save(result) {
  const targetRow = this.cells.find(row => row[0].id === result.rowId);
  if (targetRow) {
    targetRow.cachedCells[result.colIndex - 1] = {
      ...targetRow.cachedCells[result.colIndex - 1],
      value: result.newValue
    };
  }
}

4. 精简render函数的依赖访问

确保函数式组件的render函数只访问当前组件必需的props,让Vue精准追踪依赖,缩小更新范围。

内容的提问来源于stack exchange,提问作者DocGashe

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 00:05:39