Svelte props or component events?

在 Svelte 中,一个组件的事件具体行为如果必须由外部来定义的话,有两种解决办法,一种是用 props, 一种是组件的自定义事件。

使用 props 的写法:

<!-- App.svelte -->
<script>
  import Inner from './Inner.svelte';
  let name = 'Hamburger';
  function alertName() {
    alert(name);
  }
</script>
<Inner name={name} sayMyName={alertName}/>

<!-- Inner.svelte -->
<script>
  export let name;
  export let sayMyName;
</script>

<form on:submit|preventDefault={sayMyName}>
  <input bind:value={name}/>  
  <button type='submit'>Say it!</button>
</form>

使用 component events 的写法:

<!-- App.svelte -->
<script>
  import Inner from './Inner.svelte';

  function alertName(e) {
    alert(e.detail.name);
  }
</script>
<Inner on:say={alertName}/>

<!-- Inner.svelte -->
<script>
  import { createEventDispatcher } from 'svelte';
  let name = 'Hamburger';
  const dispatch = createEventDispatcher();
  function sayMyName() {
    dispatch('say', {name});
  }
</script>

<form on:submit|preventDefault={sayMyName}>
  <input bind:value={name}/>  
  <button type='submit'>Say it!</button>
</form>

这里边很大的一个区别,是数据传递的方式。前者是将定义在外部 <App/>name 作为 <Inner/> 组件的属性值传递到内部;后者则是将 <Inner/> 内部的属性通过 event.detail 暴露给外部。在多数情况下,这两种方法没有太大区别。我们很容易首选 props 的方案,因为从代码上看,明显这种方式写起来更方便,也更符合我们的直觉。我们都知道 JavaScript 里的 function 是可以被赋值给任意变量的。

但是当这个属性是个数组或其它对象时,二者的表现就可能很不一样,下面用一个数组来演示:

<!-- App.svelte -->
<script>
  import Inner from './Inner.svelte';

  let children = [];
  
  function count() {
    alert(`I have ${children.length} children!`);
  }
</script>
<Inner children={children} countChildren={count}/>

<!-- Inner.svelte -->
<script>
  export let children;
  export let countChildren;
  
  function addChild() {
    children = [...children, {}];
  }
</script>

<form on:submit|preventDefault={countChildren}>
  <div>
    <button type='button' on:click={addChild}>Add</button>
  </div>
  <ul>
    {#each children as child}
      <li><input bind:value={child.name}/></li>
    {/each}
  </ul>
  <button type='submit'>Count</button>
</form>

<App/>childrencount() 函数都当作 props 传递给了 <Inner/>. 这种情况下,我们期望的效果是:点击 3 次 Add 按钮之后,再点击 Count 按钮,会弹出一个 alert 框,显示 "I have 3 children!". 但结果显示的却是 "I have 0 children!".

问题出在 addChild() 这个函数。经过 Svelte 基础知识的学习,我们知道对于非基本类型的对象以及数组,要触发它的响应性(改变值时界面跟着变化),不能期望通过直接操作这个变量的方法来触发,而应该对该变量或者该变量的属性进行赋值操作。在 addChild() 这个例子里,就应该使用 children = [...children, {}] 的写法,而非 children.push({}).

另一方面,同时我们也知道,将响应性的变量赋值给另一个变量时,通过第 2 个变量来改变其属性,将不会触发响应性。在该例子当中,<App/>children 赋值给 <Inner/> 之后,addChild() 函数操作的是 <Inner/> 内部的 children, 因此不会引起 <App/>children 的变化,所以就有了我们看到的结果。

这时候,如果使用 component events, 将 children 作为 <Inner/> 内部的变量,通过自定义事件的方式传递到 <App/> 就可以避免这个问题:

<!-- App.svelte -->
<script>
  import Inner from './Inner.svelte';
  function count(e) {
    alert(`I have ${e.detail.children.length} children!`);
  }
</script>
<Inner on:count={count}/>

<!-- Inner -->
<script>
  import { createEventDispatcher } from 'svelte';
  let children = [];
  
  const dispatch = createEventDispatcher();
  
  function addChild() {
    children = [...children, {}];
  }
  
  function countChildren() {
    dispatch('count', {children});
  }
</script>

<form on:submit|preventDefault={countChildren}>
  <div>
    <button type='button' on:click={addChild}>Add</button>
  </div>
  <ul>
    {#each children as child}
      <li><input bind:value={child.name}/></li>
    {/each}
  </ul>
  <button type='submit'>Count</button>
</form>
1