Vue渲染时递增响应式state触发重渲染循环的原因排查
死循环触发原因
Vue的模板渲染过程中,所有绑定的表达式、方法调用都会在每次重渲染时全量执行。你在getNumber这个渲染阶段调用的方法里修改了响应式状态decrementAmount,会触发下面的连锁反应:
- 首次渲染启动,遍历
sections逐个调用getNumber计算传入组件的number属性 - 遍历到第一个不参与计数的区块时,执行
this.decrementAmount++,响应式状态变更被Vue捕获 - Vue检测到组件依赖的响应式数据变化,会丢弃当前未完成的渲染,调度一次新的组件重渲染
- 新的重渲染启动后,又从头遍历
sections调用getNumber,此时decrementAmount已经不是初始值0,再次遇到非计数区块时继续累加,又触发新的重渲染 - 上述流程反复触发,直到达到Vue的重渲染递归上限才停止,此时
decrementAmount已经被累加了数百次,计算出的序号自然会出现负值这类错误。
你给decrementAmount写固定值时不会触发循环,本质是因为渲染过程中没有修改响应式状态,不会触发新的重渲染调度,但固定值无法动态适配不同页面非计数区块的数量,不符合业务需求。
正确实现方案
核心原则:渲染阶段执行的方法不能有修改响应式状态的副作用,序号计算要做成纯逻辑,不要依赖渲染过程中累加的响应式变量。
最稳妥的实现是提前把序号算好,不要等渲染的时候边渲染边改值。
推荐方案:计算属性预处理区块列表
把序号计算逻辑放到计算属性中,仅在sections数据变化时重新计算一次,渲染时直接读取预处理好的结果,完全没有副作用。
修改后的代码如下:
- 调整模板,直接遍历预处理后的列表,不需要在模板里调用计算序号的方法:
<template> <div class="sections"> <component :is="getSection(section.__component)" v-for="(section, index) in processedSections" :key="index" :section="section.rawData" :number="section.number" class="section" /> </div> </template>
- 移除data中不需要的
decrementAmount字段,新增计算属性processedSections:
export default { name: "SectionStructure", components: { HeroStyle1: () => import("~/components/hero/HeroStyle1.vue"), CommunityBlock: () => import("~/components/sections/CommunityBlock.vue"), FiftyFiftyMedia: () => import("~/components/sections/FiftyFiftyMedia.vue"), FullscreenGame: () => import("~/components/sections/FullscreenGame.vue"), GameRow: () => import("~/components/sections/GameRow.vue"), NewsBlock: () => import("~/components/sections/NewsBlock.vue"), TitleSideImage: () => import("~/components/sections/TitleSideImage.vue"), TwoImagesCta: () => import("~/components/sections/TwoImagesCTA.vue"), CareerList: () => import("~/components/sections/CareerList.vue"), }, props: { sections: { type: Array, default: () => [], }, }, data() { return { allowedNumerableSections: [ "CommunityBlock", "FiftyFiftyMedia", "FullscreenGame", "GameRow", "NewsBlock", "TitleSideImage", "TwoImagesCta", ], }; }, computed: { processedSections() { // 计数器放在计算属性内部,非响应式,修改不会触发重渲染 let count = 0 return this.sections.map(section => { // 组件名转换逻辑和原有逻辑一致 let component = this.getSection(section.__component); component = component.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); }); component = component.charAt(0).toUpperCase() + component.slice(1); let number = false if (this.allowedNumerableSections.includes(component)) { count++ number = count < 10 ? `/0${count}` : `/${count}` } return { ...section, rawData: section, number } }) } }, methods: { getSection(str) { return str.substring(str.indexOf(".") + 1); }, }, }
这个方案下,序号是按列表顺序逐一枚举的,遇到非计数区块计数器不增长,天然能保证序号连续,而且计算逻辑只在sections更新时触发,不会产生额外重渲染。
备选方案:非响应式变量计数
如果不想预处理列表,也可以把计数变量改成非响应式的(不要放在data里),这样修改变量不会触发Vue的重渲染。注意需要在每次渲染前重置计数器,避免多次渲染累加出错:
export default { // ...组件名、组件引入、props、data配置保持不变,移除data中的decrementAmount created() { // 非响应式计数器,挂在实例上但不交给Vue托管 this._numerableCount = 0 }, beforeUpdate() { // 每次重渲染前重置计数器,从头开始计数 this._numerableCount = 0 }, methods: { getSection(str) { return str.substring(str.indexOf(".") + 1); }, getNumber(section) { let component = this.getSection(section.__component); component = component.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); }); component = component.charAt(0).toUpperCase() + component.slice(1); if (this.allowedNumerableSections.includes(component)) { this._numerableCount++ return this._numerableCount < 10 ? `/0${this._numerableCount}` : `/${this._numerableCount}` } return false } } }
这个方案也能解决循环问题,但可维护性不如计算属性方案,渲染阶段的方法调用时机受Vue渲染策略影响,后续加逻辑容易出边界问题,优先用第一种方案。
内容的提问来源于stack exchange,提问作者Donnie Berry
相关产品推荐
相关产品推荐

