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>
渲染行为差异的原因
- 函数式组件的无状态特性:Vue 2 中的函数式组件没有独立实例和响应式状态,渲染完全依赖父组件上下文与传入的props,且不做单独依赖收集。只要父组件触发任何更新(哪怕与当前组件props无关),函数式组件都会强制重新执行
render函数,引发全量重渲染。 - 父组件模板的动态方法调用:父组件模板中
:disable="isDisabled(row[0], cell, idx+1)"是实时调用方法,每次父组件渲染都会执行该方法,扩大了父组件的更新范围,进而带动所有函数式组件重渲染。而有状态组件自带依赖追踪,仅在自身props或内部状态变化时更新,不受父组件无关更新影响。 - 数组切片的不稳定性:
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
相关产品推荐
相关产品推荐

