当函数中的变量更改时不会触发 Svelte 反应

Svelte reactivity not triggering when variable changed in a function

我在这里有点困惑,不幸的是我在 svelte 的 discord 频道上找不到任何解决方案,所以我开始了...

我有两个 类 的相当基本的例子,假设它们是 AppCompApp 创建一个 Comp 实例,然后在单击按钮后更新此实例的 value

Comp 实例应将此值设置为不同的变量 (inputValue),并且在更改该变量时应触发 validate(inputValue),这是反应性的。这是一个 REPL:https://svelte.dev/repl/1df2eb0e67b240e9b1449e52fb26eb14?version=3.25.1

App.svelte:

<script>
    import Comp from './Comp.svelte';
    
    let value = 'now: ' + Date.now();
    
    function clickHandler(e) {
        value = 'now ' + Date.now();
    }
</script>

<Comp
    bind:value={value}
/>
<button type="button" on:click={clickHandler}>change value</button>

Comp.svelte:

<script>
    import { onMount } from 'svelte';

    export let value;

    let rendered = false;
    let inputValue = '';

    $: validate(inputValue); // This doesn't execute. Why?

    function validate(val) {
        console.log('validation:', val); 
    }

    onMount(() => {
        rendered = true;
    });

    $: if (rendered) {
        updateInputValue(value);
    }

    function updateInputValue(val) {
        console.log('updateInputValue called!');
        if (!value) {
            inputValue = '';
        }
        else {
            inputValue = value;
        }
    }
</script>

<input type="text" bind:value={inputValue}>

所以一旦值改变:

  1. 被动if (rendered) {...}条件被调用
  2. updateInputValue 被调用,inputValue 被改变。 HTML 输入元素更新为此值。
  3. validate(inputValue) 从未对此更改做出反应 - 为什么?

如果我在反应式 if (rendered) 条件中省略对 updateInputValue 函数的额外调用并将 updateInputValue 函数体的代码直接放在条件中,那么 validate(inputValue) 是正确触发,即:

// works like this  
$: if (rendered) {
    if (!value) {
        inputValue = '';
    }
    else {
        inputValue = value;
    }
}

那么为什么在函数中更新时不起作用?

真的很奇怪(我真的无法解释)但是如果你把响应语句 $: validate(inputValue); 放在函数 updateInputValue 声明之后,它会按预期工作:

<script>
    import { onMount } from 'svelte';
    
    export let value;
    
    let rendered = false;
    let inputValue = '';

    function validate(val) {
        console.log('validation:', val);
    }
    
    onMount(() => {
      rendered = true;
   });
    
    $: if (rendered) {
        updateInputValue(value);
    }
    
    function updateInputValue(val) {
        console.log('updateInputValue called!');
        if (!value) {
            inputValue = '';
        }
        else {
            inputValue = value;
        }
    }
    
    $: validate(inputValue);
</script>

检查此 REPL

这里是原始 post 问题的最小示例和几个解决方法:

<script>
  let nb
  let n=0
$: console.log("nb1:",nb)
$: update(n)
$: console.log("nb2:",nb)
function update(v) {
  console.log("update",v)
  nb = v
}
</script>
<h1 on:click={()=>n=Math.floor(Math.random()*100)}>click: {nb}</h1>
<h1 on:click={()=>update(Math.floor(Math.random()*100))}>click: {nb}</h1>

nb 的第一个日志不会在 n 被分配一个新值并且 $: update(n) 运行s 由于分配而被触发。 nb的第二条日志是运行,因为它在赋值给nb.
之后 如果直接调用 update-函数(最后 h1)一切正常。

如果您在顶层为 nb 赋值,则一切正常:

 <script>
      let nb
      let n=0
    $: console.log("nb1:",nb)
    $: nb = update(n)
    $: console.log("nb2:",nb)
    function update(v) {
      console.log("update",v)
      return v
    }
    </script>
    <h1 on:click={()=>n=Math.floor(Math.random()*100)}>click: {nb}</h1>
    <h1 on:click={()=>update(Math.floor(Math.random()*100))}>click: {nb}</h1>

我认为这是一个错误。

@johannchopin 的 揭示了这个问题。


您可以阅读 my blogpost for slightly in-depth explanation of how reactive declaration works,这里是 tl;dr:

  • 响应式声明批量执行

    svelte 批处理所有更改以在下一个更新周期更新它们,并且在更新 DOM 之前,它将执行反应声明以更新反应变量。

  • 响应式声明按照它们的依赖顺序执行。

    响应式声明是批量执行的,每个声明执行一次。一些声明相互依赖,例如:

    let count = 0;
    $: double = count * 2;
    $: quadruple = double * 2;
    

    在这种情况下,quadruple 取决于 double。因此,无论您的反应性声明的顺序如何,$: double = count * 2;$: quadruple = double * 2 之前或相反,前者应该是并且 在后者之前执行。

    Svelte 将按依赖顺序对声明进行排序

    相互之间没有依赖关系的情况:

    $: validate(inputValue);
    $: if (rendered) updateInputValue(value);
    

    第一个语句依赖于validateinputValue,第二个语句依赖于renderedupdateInputValuevalue,声明在顺序中保留原样。


现在,了解了响应式声明的这 2 个行为,让我们来看看您的 REPL

当您更改 inputValuerenderedvalue 时,Svelte 将对更改进行批处理,并开始新的更新周期。

就在更新 DOM 之前,Svelte 将一次性执行所有响应式声明。

因为 validate(inputValue);if (rendered) updateInputValue(value); 语句之间没有依赖关系,(如前所述),它们将按顺序执行。

如果你只改变renderedvalue,第一条语句(validate(inputValue))将不会被执行,同样,如果你改变inputValue第二条语句(if (rendered) updateInputValue(value)) 不会被执行。

现在,在 updateInputValue 中您更改了 inputValue 的值,但是因为我们已经处于更新周期中,所以我们不会开始新的周期。

这通常不是问题,因为如果我们按依赖顺序对响应式声明进行排序,更新依赖变量的语句将在依赖变量的语句之前执行。


所以,知道出了什么问题,您可以寻求一些“解决方案”。

  1. 手动重新排序反应式声明语句,尤其是当执行顺序存在隐式依赖时。

在此 REPL

中查看对响应式声明进行排序的区别

因此将您的 REPL 更改为:

$: if (rendered) {
  updateInputValue(value);
}
$: validate(inputValue);

See REPL

  1. 在响应式声明中明确定义依赖关系
$: validate(inputValue);

$: if (rendered) {
    inputValue = updateInputValue(value);
}
    
function updateInputValue(val) {
    console.log('updateInputValue called!');
    if (!value) {
        return '';
    }
    else {
        return value;
    }
}

See REPL