Subscribe on changes!

在使用watchEffect赋值的过程中出现 无法理解的错误

avatar
Jul 27th 2023

Vue version

3.3.4

Link to minimal reproduction

https://play.vuejs.org/#eNqVVctu00AU/ZXBGzvImjTqLk0ioKoESEBVKljUWRh7nLi1Z6yZsRsUecOGb0J8D+I3uPPyIwmVuojluffOuY9zrrP3XlcVbmrizb2FSHheSSSIrKtVRPOyYlyiPeIkQy3KOCuRD6F+55JESGvHU3WYKSj/IqIJo+AqxQYt1fXAf0uKgqGvjBfpC38S0YgupiYfZIKDJGVVxJLACaFFmjf6BV51jrl6fomLZeTdzdaRt1rodCZ4aqKVyWJ4oScFlJDlG3wvGIXm9io08hJWVnlB+KdK5lBi5M2R9ihfDBU+vtc2yWsSOnuyJcnDCfu92Clb5F1zIghvSOR1PhnzDZHGffX5I9nBe+csWVoXEP2E84YIVtSqRhP2pqYplD2I09W+0zzkdHMrrnaSUOGaUoWqyFbHRx7QcvlE63255/hc34toq6boOB3Io4jpBniQgOCkwgdiqThr8pSEqKaHurkYBdaC3CpuXQSeWssoThlml9u8SMdC0yarNohWP6M5yF8JUF1KspySa3Va7K185rQuvxF+t25XwWR8bY9gACoZhN2QLBwcQ9Vm74KCl674QGfroGzvga/zlX4IEwYzGoEZywhSmdoOZfjrl8QZ+kXROGZV9CtC+4MuUNs616If5NRdNYujswxXx/aGpQDWXd6ONrVBtSRpqPb6FMvuBtnpG2a8juwlMiMD7emvhyIFOVZQO0HLlZuZuWiDVDPmS6KvY2vuZubC+/ZVqiEcgtuy5tQIxAEYn57SaRiT2DUdGMg7jLFuPOjLm4RodrY+KmgsHagpjWU8RzH9Pi6uB8JNXNREKRgij+qzTQyJHmpVS/dArlpd6uEWutud44/+kOmc3pNEDnkO0WMsk+1VloHjeLX7bWpKqN/c75ZhtHCsILhgm6ApD/bQzsGS3TmtuZuNYqApcd94MFmfwtfJTWqDz+hlkScPWh3L1eH4DT6uarENPsRyi3lMU1YC6y+B3TON05ExmIXVhYWDwkYUBCOtTAyKfj53vS3EYK3Bj14lqiX4Jtvm4P/x749ff37+7jf8f9ve/gOjg8fz

Steps to reproduce

在demo中无法复现这一问题 代码和我本地相同 环境vite 4.3.9 @vitejs/plugin-vue-jsx

image 在我本地中如果这里使用解构亦或者是lodash deepClone方法后 只要触发热更新就无限执行setup的所有逻辑 如果直接使用当前变量 是正常的

What is expected?

请帮我排查一下 是否是我的写法有问题 还是某些隐藏的bug

What is actually happening?

只要修改案例中 test1Child.vue 的组件 就会触发无限循环整个setup image 这是我本地的报错

System Info

System:
    OS: Windows 10 10.0.22621
    CPU: (16) x64 11th Gen Intel(R) Core(TM) i9-11900K @ 3.50GHz
    Memory: 11.60 GB / 31.32 GB
  Binaries:
    Node: 18.16.0 - C:\Program Files\nodejs\node.EXE
    npm: 9.5.1 - C:\Program Files\nodejs\npm.CMD
    pnpm: 8.5.1 - C:\Program Files\nodejs\pnpm.CMD
  Browsers:
    Edge: Spartan (44.22621.1992.0), Chromium (115.0.1901.183)
    Internet Explorer: 11.0.22621.1

Any additional comments?

No response

avatar
Jul 27th 2023

如果改成watchPostEffect 也不会出现无限循环的错误 但我想知道这是为什么

avatar
Jul 27th 2023

@jc840031139

Reason

首先watchEffect会立即执行回调,执行时机是Vue组件更新之前被调用,在子组件test1Child修改testValRef的值,testValRef发生改变,会立即触发父组件test1组件的更新(在父组件test1中使用了testValRef,所以会更新),父组件test1组件更新时先执行setup,执行setup时,watchEffect会立即执行回调,触发父组件test1组件的更新,此时updateQueue一直在增加,并没有真正执行组件渲染,来消费update。此时updateQueue增加至最大值RECURSION_LIMIT,抛出警告,这个值是100,可以在源码中搜索。

可以在项目中Vue源码这里打上断点看一下,queue会一直增加,而flushJobs没有机会执行。

function queueJob(job) {
  debugger
  if (!queue.length || !queue.includes(
    job,
    isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
  )) {
    if (job.id == null) {
      queue.push(job);
    } else {
      queue.splice(findInsertionIndex(job.id), 0, job);
    }
    queueFlush();
  }
}
function queueFlush() {
  debugger
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true;
    currentFlushPromise = resolvedPromise.then(flushJobs);
  }
}

而使用watchPostEffect,则是在组件渲染之后再执行,此时flushJobs已经执行完成,此时在子组件test1Child修改testValRef的值,触发test1的更新,flushJobs会正常执行。

这个异常的重点是,updateQueue还未执行完毕,就又来了新的update进入queue,最后会超过最大的update限制 ,然后抛出警告。

Docs

https://cn.vuejs.org/guide/essentials/watchers.html#callback-flush-timing https://cn.vuejs.org/api/reactivity-core.html#watcheffect

Code

检查是否超出RECURSION_LIMIT并抛出警告的部分

function checkRecursiveUpdates(seen, fn) {
  if (!seen.has(fn)) {
    seen.set(fn, 1);
  } else {
    const count = seen.get(fn);
    if (count > RECURSION_LIMIT) {
      const instance = fn.ownerInstance;
      const componentName = instance && getComponentName(instance.type);
      warn(
        `Maximum recursive updates exceeded${componentName ? ` in component <${componentName}>` : ``}. 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.`
      );
      return true;
    } else {
      seen.set(fn, count + 1);
    }
  }
}