Svelte реактивность не срабатывает при изменении переменной в функции

Я немного запутался здесь, и, к сожалению, я не смог найти никакого решения на канале разногласий svelte, поэтому я иду ...

У меня есть довольно простой пример двух классов, пусть это будут App и Comp. App создает 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) никогда не реагирует на это изменение - ПОЧЕМУ?

Если я опущу дополнительный вызов функции updateInputValue в реактивном if (rendered) условии и помещу код тела updateInputValue функции непосредственно в условие, то validate(inputValue) сработает правильно, т. Е .:

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

Так почему же это не работает при обновлении в функции?


person Fygo    schedule 17.09.2020    source источник
comment
Обратите внимание, что он работает, если вы вводите что-то вручную (не нажимая кнопку).   -  person johannchopin    schedule 17.09.2020
comment
@johannchopin Это я считаю логичным и правильным из-за прямой привязки к inputValue, т.е. исключает вызов реактивного оператора if и, следовательно, функции updateInputValue. Но мне нужно иметь возможность обновлять значение извне компонента = ›изменение свойства value.   -  person Fygo    schedule 17.09.2020


Ответы (3)


ответ @ johannchopin выявил проблему.


Вы можете прочитать мой блог для более подробного объяснения как работает реактивная декларация, вот 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);
    

    1-й оператор зависит от validate и inputValue, 2-й оператор зависит от rendered, updateInputValue и value, объявление остается в том порядке, в каком оно есть.


Теперь, зная эти два поведения реактивного объявления, давайте взглянем на ваш REPL.

Когда вы меняете inputValue, rendered или value, Svelte пакетирует изменения и запускает новый цикл обновления.

Прямо перед обновлением DOM Svelte выполнит все реактивные объявления за один раз.

Поскольку нет зависимости между операторами validate(inputValue); и if (rendered) updateInputValue(value); (как объяснялось ранее), они будут выполняться по порядку.

Если вы измените только rendered или value, 1-й оператор (validate(inputValue)) не будет выполняться, и аналогично, если вы измените inputValue, 2-й оператор (if (rendered) updateInputValue(value)) не будет выполняться.

Теперь в updateInputValue вы меняете значение inputValue, но поскольку мы уже находимся в цикле обновления, мы не будем начинать новый.

Обычно это не проблема, потому что, если мы отсортируем реактивные объявления в порядке зависимости, оператор, обновляющий переменную, от которой зависит, будет выполнен перед оператором, который полагается на переменную.


Итак, зная, что не так, есть несколько решений, которые вы можете найти.

  1. Вручную измените порядок операторов реактивного объявления, особенно когда существует неявная зависимость порядка выполнения.

Посмотрите разницу в заказе реактивного объявления в этом REPL

Так что измените свой REPL на:

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

См. REPL

  1. Явное определение зависимости в реактивных объявлениях
$: validate(inputValue);

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

См. REPL

person Tan Li Hau    schedule 20.09.2020
comment
Отличный ответ;) Еще раз спасибо за это. - person johannchopin; 20.09.2020

Действительно странно (и я не мог этого объяснить), но если вы поместите реактивный оператор $: 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.

person johannchopin    schedule 17.09.2020
comment
Отличный улов, я об этом не подумал. Спасибо! Мне это кажется ошибкой ... Пожалуйста, пока что проголосуйте за. - person Fygo; 17.09.2020
comment
@Fygo Да, действительно любопытно, не могли бы вы открыть вопрос по репо? - person johannchopin; 17.09.2020
comment
Если вы посмотрите на скомпилированный js, вы заметите, что реактивность достигается путем пометки переменной как «грязная». Если у вас есть вызов validate()-функции перед присвоением значения переменной inputValue, тогда inputValue не является грязным и, следовательно, validate() не вызывается. (Чтобы уведомить также @johannchopin) - person grohjy; 17.09.2020
comment
Проблема, похоже, в том, что присвоение inputValue не помечает его как грязное, и это происходит, если inputValue присвоение находится в подфункции. Это отлично работает $: if (rendered) { inputValue=updateInputValue(value); }, и это также работает $: if (rendered) { inputValue=’’;updateInputValue(value);} В первой версии вы, конечно, должны вернуть правильное значение из функции. Я считаю, что это ошибка. - person grohjy; 17.09.2020
comment
Да, вы правы, но поскольку я ничего не читал об этом в официальной документации, было бы полезно открыть для этого проблему. У тебя есть на это время @grohjy? - person johannchopin; 17.09.2020
comment
@johannchopin Вчера я создал полуотчет / полу-вопрос по этому поводу, который, к сожалению, был закрыт, но я думаю, его можно было бы открыть снова. Если вам интересно, вот ссылка: github.com/sveltejs/svelte/issues/5408 < / а> - person Fygo; 17.09.2020

Вот минимальный пример проблемы исходного сообщения и несколько обходных путей:

<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) запускается из-за назначения. Второй журнал 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>

Я считаю это ошибкой.

person grohjy    schedule 17.09.2020