如何在Svelte中进行轮询?

4
我有一个特定的应用程序,应该实现以下功能:
  1. 你有一个作业列表需要检查
  2. 当你点击一个作业时,会进入到详细视图界面
  3. 详细视图将轮询API以查看实时进度更新
我在以下REPL中进行了基本实现:https://svelte.dev/repl/fcdce26dc0d843dbb4b394dcd2c838af?version=3.20.1 这种方法存在一些问题:
  1. Job.svelte 视图应该在提供新ID时基本重置,并清除任何先前的轮询器,但现在通过底部的反应式语句非常棘手。
  2. 由于轮询器执行异步获取,因此可能发生超时处理程序 poller 被清除的情况,即使处理程序已经在执行。这会导致多个轮询器循环出现(可以通过在0和2秒之间的随机时间间隔内点击作业列表来复现此问题)
  3. 当前方法不友好且容易破坏。上述 "错误" 可以通过跟踪引用 /锁定等方式进行修复,但这样更难理解。
针对此用例,在 Svelte 中有更好的实现方式吗?
非常感谢!
3个回答

3

当您提供新的 id 时,Job.svelte 视图应该基本重置,并清除任何先前的轮询器,但是现在它使用底部的反应语句非常尴尬。

正如以前的回复已经指出的那样,这是 Svelte 的方式。 起初很尴尬,直到你意识到它非常简单、方便和优雅。

在您的代码中,我会改进两件事:

  • 摆脱每个 ID 的混乱缓存 + 单个进度值赋值;这是导致您的延迟获取在视觉上显示的原因(晚获取的回复本身不是 bug,但将其显示给用户是),我只是将缓存对象重命名为 progress 并选择显示 progress[id],这样,晚获取的回复将在后台更新,但不会在视观上影响当前显示的 Job。
  • 使用 setInterval 而不是多个 setTimeout 进行定期轮询。
<script>
    export let id

    let progress = {}
    let poller

    const setupPoller = (id) => {
        if (poller) {
            clearInterval(poller)
        }
        poller = setInterval(doPoll(id), 2000)
    }

    const doPoll = (id) => async () => {
        console.log(`polling ${id}`)
        progress[id] = await new Promise(resolve => setTimeout(() => {
            resolve((progress[id] || 0) + 1)
        }, 500))
    }

    $: setupPoller(id)
</script>

<div>
    <p>
        ID: {id}
    </p>
    <p>
        Progress: {progress[id] || 0}
    <p>
</div>

查看此 REPL


非常感谢您的回复!它确实有助于更好地理解Svelte。您是否有一些资源(文章,博客)可以让我了解更多相关信息?关于您提出的改进建议:在实际使用中,我希望能够从后端响应完成/失败并实现诸如指数退避等功能,这就是为什么我选择setTimeout而不是setInterval的原因。 - Wilco
1
@Wilco 非常欢迎。我想不出任何具体的文章或博客,但我确实关注 Twitter 上的 @sveltejs@SvelteSociety 账户。Svelte Society 上周日举行了一个关于各种主题的会议(我还没有时间观看,但计划观看):https://www.youtube.com/watch?v=0rnG-OlzGSs。至于 setInterval 建议,那只是一个建议。如果您的需求需要不同的方法,那也是完全有效的理由。您也可以使用套接字而不是轮询并推送进度更新。有很多方法可以解决问题(尽管听起来很可怕)。 ;) - Thomas Hennes

1
我认为在Svelte中只有通过第一种方法来实现,这与Angular中的ngOnChanges不同。
我认为2./3.是独立于Svelte的问题。带有时间竞赛的异步操作总是很困难的。像rxjs这样的库使得处理这个问题更容易,但是具有陡峭的学习曲线。一个例子:
<script>
  import { interval, Subject } from 'rxjs';
  import { switchMap, take, map, startWith, tap } from 'rxjs/operators';

  const cache = {};
  const id$ = new Subject();
  const progress$ = id$.pipe(
    // every time id$ gets a new id, start new interval
    switchMap(id => {
      return interval(1000).pipe(
        // every time the interval emits, do an api call
        switchMap(() => {
          // fake api call
          return interval(200).pipe(
            take(1),
            map(() => id + '::' + Math.random()),
            // store value in cache
            tap(value => cache[id] = value)
          );
        }),
        // start with cached value
        startWith(cache[id]),
      );
    })
  );

  let id = 1;
  function switchPoll() {
    id = id === 2 ? 1 : 2;
    id$.next(id);
  }
</script>

<p>{$progress$}</p>
<button on:click={switchPoll}>Switch</button>

这很酷!我以前玩过rxjs,但我没有想到。感谢您的回复!如果API调用需要很长时间或者我想做像指数退避这样的事情,固定间隔1秒钟可能不是一个好主意。这就是为什么我在示例中使用了setTimeout,你能在rxjs中做类似的事情吗?再次感谢! - Wilco
如果你担心你的API可能会花费太长时间,你可以使用exhaustMap代替switchMap。这意味着如果API花费超过1秒钟,下一个传入的间隔将被忽略,只要内部可观察对象(也就是API调用)没有完成。指数退避有点难以实现,更多信息请参见此处:https://dev59.com/DFQJ5IYBdhLWcg3w05f2。 - dummdidumm

-1
我建议使用来自 Svelte 的 await blocks。通过它,您可以直接获得所有异步功能所需的逻辑,例如生成加载屏幕或捕获错误。我在您的 REPL 中使用了自定义超时函数(sleep)来模拟从远程服务器获取数据:
<script>    
    let selectedJob;
    let progress = 0
        function sleep(ms) {
      return new Promise(resolve => setTimeout(resolve, ms));
    }

    const pollProgress = async (id) => {    
        if(progress < 10) {;
      await sleep(300)
            progress += 1
            return pollProgress(id)
        }
        return "Done"
    }   

    function handleClick(id) {
        selectedJob = id
        promise = pollProgress(id);
    }
    let promise
</script>

<style>
    .list {
        background-color: yellow;
        cursor: pointer;
    }

    .details {
        background-color: cyan;
    }
</style>

<div class="list">
  {#each new Array(10).fill().map((_, i) => i) as i}
       <div on:click={() => (handleClick(i))}>
             Job {i}
         </div>
    {/each}
</div>

{#if selectedJob !== undefined}
    <div class="details">
        <h2>Job details</h2>
        <div>
        <p>
        ID: {selectedJob}
    </p>
{#await promise}
    <p>Progress: {progress}</p>
{:then res}
    <p>
        Progress: {res}
    </p>
{:catch error}
    <p style="color: red">{error.message}</p>
{/await}
</div>
    </div>
{/if}

在获取数据的过程中,它将显示一个更新进度以模拟轮询,并在完成时接收异步函数返回的字符串Done


非常感谢您的回复!我想要重复地轮询进度,而不仅仅是一次,所以这个解决方案并不适合我的使用情况。 - Wilco
1
我更新了 REPL 并使用递归函数实现了所需的轮询功能,请查看一下我的代码。 - Gh05d
感谢您更新的代码!递归生成新的Promise是一个有趣的方法,我之前没有想到过。然而,现在进度在作业之间共享,如果我切换作业并在控制台中打印,我可以看到Promise链没有被中断,这意味着您的示例遭受了我在最初的帖子中描述的相同问题。 - Wilco

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接