Vue.js 3+FullCalendar递归更新超限问题排查求助
问题描述
我正在开发一个Laravel 9与Vue.js 3的项目,使用FullCalendar组件展示从Laravel数据库获取的日历事件。
Calendar.vue 代码如下:
<template> <div class="demo-app"> <div class="demo-app-main"> <FullCalendar ref="schedule" :options='calendarOptions' > <template v-slot:eventContent="arg"> <b>{{ arg.timeText }}</b><br/> </template> </FullCalendar> </div> </div> </template> <script> import '@fullcalendar/core/vdom' // 解决Vite相关问题 import FullCalendar from '@fullcalendar/vue3'; import timeGridPlugin from '@fullcalendar/timegrid'; import nlLocal from '@fullcalendar/core/locales/nl'; import bootstrap5Plugin from '@fullcalendar/bootstrap5'; export default { components: { FullCalendar }, data: function () { return { calendarOptions: { locale: nlLocal, plugins: [ timeGridPlugin, bootstrap5Plugin ], themeSystem: 'bootstrap5', headerToolbar: null, dayHeaderFormat: { weekday: 'short', month: 'long', day: 'numeric', omitCommas: true }, slotLabelFormat: { hour: '2-digit', minute: '2-digit', omitZeroMinute: false, meridiem: 'short' }, titleFormat: { year: 'numeric', month: 'long', day: 'numeric' }, nowIndicator: true, initialView: 'timeGridWeek', events: this.getEvents, editable: false, selectable: false, selectMirror: true, dayMaxEvents: true, weekends: false, eventClick: this.openLessonModal, slotDuration: '00:15:00', eventColor: '#4782f6' }, } }, methods: { getEvents(info, successCallback, failureCallback) { axios.get(this.route('student.schedules.get-lessons'), { params: { start: this.$dayjs(info.start).format('YYYY-MM-DD'), end: this.$dayjs(info.end).format('YYYY-MM-DD'), class: this.params.class, teacher: this.params.teacher } }) .then((response) => { successCallback(response.data); }) // 数据库返回示例: // successCallback([ // { // "id": 1, // "subject_id": 3, // "teacher_id": 1, // "classroom_id": 1, // "start": "2022-09-08 15:00:00", // "end": "2022-09-08 15:55:00", // "subject": { // "id": 3, // "name": "Engels" // }, // "teacher": { // "id": 1, // "abbreviation": "TEA" // }, // "classroom": { // "id": 1, // "name": "S101" // } // }, // { // "id": 3, // "subject_id": 2, // "teacher_id": 1, // "classroom_id": 1, // "start": "2022-09-08 09:00:00", // "end": "2022-09-08 09:55:00", // "subject": { // "id": 2, // "name": "Nederlands" // }, // "teacher": { // "id": 1, // "abbreviation": "TEA" // }, // "classroom": { // "id": 1, // "name": "S101" // } // }, // ]) }, openLessonModal() { // 业务逻辑代码 } } } </script>
遇到的问题:
每次从数据库获取事件时,控制台抛出以下警告:
[Vue warn]: Maximum recursive updates exceeded. This means you have a reactive effect that is mutating its own dependencies and thus recursively triggering itself. Possible sources include component template, render function, updated hook or watcher source function.
执行npm run build构建生产版本后,页面完全冻结,推测与无限循环有关。
问题根源
核心原因是在响应式对象calendarOptions中直接引用了组件方法this.getEvents:
calendarOptions是Vue组件data中的响应式对象,Vue的响应式系统会追踪其所有属性的变化。当你把events: this.getEvents写入其中时,Vue会把getEvents方法的引用纳入响应式依赖追踪。- 当FullCalendar调用
getEvents获取数据并通过successCallback渲染事件时,会间接触发组件的响应式状态更新,导致calendarOptions被重新评估,进而再次触发getEvents调用,形成无限循环,直到达到Vue的递归更新上限,最终在生产环境中导致页面冻结。
解决方案
方案1:使用匿名函数包装getEvents调用
修改calendarOptions中的events配置,用匿名函数包裹对getEvents的调用,避免直接引用组件方法触发响应式追踪:
data: function () { return { calendarOptions: { // ...保留原有所有配置 initialView: 'timeGridWeek', // 用匿名函数包裹,避免响应式追踪 events: (info, successCallback, failureCallback) => { this.getEvents(info, successCallback, failureCallback) }, // ...保留原有所有配置 }, } }
方案2:将calendarOptions设置为非响应式对象
把calendarOptions移出data,在mounted钩子中创建非响应式的配置对象,避免Vue对其进行响应式追踪:
data: function () { return { // 移除calendarOptions } }, mounted() { const calendarOptions = { locale: nlLocal, plugins: [timeGridPlugin, bootstrap5Plugin], themeSystem: 'bootstrap5', headerToolbar: null, dayHeaderFormat: { weekday: 'short', month: 'long', day: 'numeric', omitCommas: true }, slotLabelFormat: { hour: '2-digit', minute: '2-digit', omitZeroMinute: false, meridiem: 'short' }, titleFormat: { year: 'numeric', month: 'long', day: 'numeric' }, nowIndicator: true, initialView: 'timeGridWeek', events: this.getEvents, editable: false, selectable: false, selectMirror: true, dayMaxEvents: true, weekends: false, eventClick: this.openLessonModal, slotDuration: '00:15:00', eventColor: '#4782f6' } // 通过ref设置日历配置 this.$refs.schedule.calendar.setOptions(calendarOptions) }
额外注意事项
如果this.params是响应式数据,建议通过watch监听其变化,手动调用FullCalendar的事件刷新方法,而不是依赖FullCalendar自动触发:
watch: { params: { deep: true, handler() { // 刷新日历事件 this.$refs.schedule.calendar.refetchEvents() } } }
内容的提问来源于stack exchange,提问作者Jacob

