yanyue404 / blog

Just blog and not just blog.
https://yanyue404.github.io/blog/
Other
87 stars 13 forks source link

记录一次 watchEffect 错用导致的 “RangeError: Maximum call stack size exceeded” #276

Open yanyue404 opened 2 months ago

yanyue404 commented 2 months ago

下面的模块在画布页面的十几个模块里报错 (RangeError: Maximum call stack size exceeded),最终定位到这个模块存在问题(下面伪代码):

<script setup>
import { watchEffect, computed, ref } from 'vue'

const riskList = computed(() => [])

const optionalRiskList = ref([])

setTimeout(() => {
  riskList.value.optionalRiskList = [{ name: '附加险1', amount: 3000 }]
}, 3000)

watchEffect(() => {
  if (!optionalRiskList.value.length) {
    optionalRiskList.value = riskList.value?.optionalRiskList || []
  }
})
</script>

<template>
  <h1>{{ optionalRiskList }}</h1>
</template>

Why

这段代码可能会导致调用栈溢出的原因是在 watchEffect 中对 optionalRiskList 进行了监听,而在 optionalRiskList 变化时,又会触发 watchEffect 的回调函数。回调函数中对 optionalRiskList 进行了赋值操作,这会再次触发 watchEffect,从而形成了无限循环的调用,最终导致调用栈溢出。

更具体地说,在 watchEffect 的回调函数中,如果 optionalRiskList 发生变化,会重新给 optionalRiskList 赋值,而这个赋值操作又会触发 watchEffect 的回调函数,如此循环,导致调用栈溢出。

解决

解决这个问题的方法是在 watchEffect 中使用 watch,并且在回调函数中判断是否需要赋值,以避免无限循环的调用。

那么合理的情况如何改写呢

要避免调用栈溢出,你可以改写代码,确保在 watchEffect 中不会导致无限循环的调用。以下是一种合理的改写方式:

<script setup>
import { watch, ref, onMounted } from 'vue'

const riskList = ref([])
const optionalRiskList = ref([])

// 模拟异步获取数据,在数据获取后设置 optionalRiskList
onMounted(() => {
  setTimeout(() => {
    optionalRiskList.value = [1, 2, 3]
  }, 3000)
})

// 监听 riskList 的变化,在变化时更新 optionalRiskList
watch(
  () => riskList.value,
  (newValue, oldValue) => {
    if (oldValue.length === 0 && newValue.length !== 0) {
      optionalRiskList.value = newValue.optionalRiskList || []
    }
  },
  {
    immediate: true,
    deep: true
  }
)
</script>

<template>
  <h1>{{ optionalRiskList }}</h1>
</template>

复现

你可以将我最开始的方案帮我生成一个可运行的 index.html 使用 vue 3 bootcdn 链接的可以复现调用栈溢出的版本吗,我想在本地使用 live-server 启动的时候看到这个报错

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vue 3 Stack Overflow Example</title>
    <!-- 高版本的 vue 已经规避的了循环调用的问题
   vue.global.js:1616 [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.
    -->
    <!-- <script src="https://unpkg.com/vue@3.2.37/dist/vue.global.js"></script> -->
    <script src="https://unpkg.com/vue@3.2.9/dist/vue.global.js"></script>
  </head>
  <body>
    <div id="app">
      <h1>{{ optionalRiskList }}</h1>
    </div>

    <script>
      const { createApp, ref, watch, computed, watchEffect } = Vue

      const App = {
        setup() {
          const riskList = ref([])
          const optionalRiskList = ref([])

          watch(
            riskList,
            () => {
              optionalRiskList.value.push({ name: '附加险1', amount: 3000 })
            },
            {
              immediate: true,
              deep: true
            }
          )

          watch(
            optionalRiskList,
            () => {
              riskList.value.push({ name: '附加险2', amount: 4000 })
            },
            {
              immediate: true,
              deep: true
            }
          )

          return {
            optionalRiskList
          }
        }
      }

      createApp(App).mount('#app')
    </script>
  </body>
</html>