问题来自这样一个事实,即 Svelte 希望按照它们相互依赖的顺序对反应块进行排序:它希望首先计算那些被依赖但没有依赖关系的块,以避免不必要的计算......或陷入困境环形。
此外,Svelte 将反应式表达式的依赖性视为出现在其中的任何反应式变量。
因此,错误的实际来源是变量 a 和 b 出现在两个反应表达式中。
解决方案是从反应式表达式中删除不需要的变量,即分配给的变量。这可以通过将赋值移动到反应块之外的函数来完成。不幸的是,它更冗长......
<script>
let total = 100;
let a = 0
let b = 0
const setA = () => {
// the assignment to a (or b in the other function) are still
// reactive, of course, but Svelte won't propagate changes to
// variable that are assigned their current value, so this
// will break the loop
a = total - b
}
const setB = () => {
b = total - a
}
$: setA(total - b);
$: setB(total - a);
</script>
<pre>
a: {a}
b: {b}
</pre>
<label>
a <input type="number" bind:value={a} />
</label>
<label>
b <input type="number" bind:value={b} />
</label>
编辑
正如 @avf 在 cmets 中所说,上面的代码具有误导性,并且对于代码的读者来说不是那么明显。我当时是这样写的,因为我专注于演示 Svelte 反应性的原理。
在现实世界中,我宁愿推荐以下两种形式中的任何一种。
只要有可能,都倾向于显而易见:
// this function is just normal
const setA = (value) => {
a = value
}
// this reactive expression makes it obvious and straightforward that
// its dependencies are total and b, and only those
$: setA(total - b)
当显而易见性不是一个选项时(例如,因为函数的内容比单纯的赋值有更多的副作用),我会依赖以下对 Svelte 来说非常惯用的形式。不幸的是,对于通常不熟悉 Svelte 的人来说,它也很晦涩……但我想这只是属于你必须学习才能真正精通该框架的那些东西。
const recomputeEverythingOrWhatever = () => {
...
}
// in Svelte lingo, this is broadly understood as "whenever a, or b, or
// total changes, then recompute everything (or whatever)"
$: a, b, total, recomputeEverythingOrWhatever()
这种语法在 IMO 中很好,因为它通常为 Svelte 开发人员所熟悉和很好理解。
但是要小心不要在重新计算函数中重新实现依赖循环,例如通过将a 和b 更改为彼此的函数。这次代码可以正常编译,因为编译器不会面临必须对相互依赖的反应块进行排序的不可能完成的任务,并且在运行时 Svelte 甚至会打破无限更新循环。但是这样的代码的效果仍然很难理解和预测。