Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
yanyue404 opened this issue May 17, 2024 · 0 comments

Comments

@yanyue404
Copy link
Owner

yanyue404 commented May 17, 2024

下面的模块在画布页面的十几个模块里报错 (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/[email protected]/dist/vue.global.js"></script> -->
    <script src="https://unpkg.com/[email protected]/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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant