Vueで非同期な配列をv-forする

Vueで、ファイルを複数ダウンロードして`v-for`リストレンダリングさせる機能を実装したかったのですが、少し工夫が必要だったのでメモ。


# ズバリ
```HTML:HTML
<ul>
    <li v-for="attachment in attachments()">
        <div>{{attachment.name}}</div>
        <div>{{attachment.size / 1024}} KB</div>
    </li>
</ul>
```
```javascript:JavaScript
module.exports = {
    // 描画トリガーは親コンポーネントからの受け渡しと仮定
    // result: ["aaa.bin", "bbb.bin", "ccc.bin"]
    props: {
        result: Array
    },

    data(){
        return {
            files: [],
            reset: false
        };
    },

    method: {
        attachments(){
            if(!this.result){
                this.files = [];
                return this.files;
            }

            if(this.reset){
                this.files = [];
            }

            const filenames = this.result;
            this.reset = (this.files.length === filenames.length) ? true : false;
        
            if(this.files.length === 0){
                filenames.map((filename)=>{
                    fetch(`https://download.link/files/${filename}`, {method: "GET"})
                    .then((res)=>{
                        if(res.ok){
                            this.files.push(new File([res.blob()], filename));
                        }
                    })
                    .catch(error => console.log(error));
                });
            }

            return this.files;
        }
    }
};
```


# 解説
## Vueおさらい
- [Vueインスタンス - Vue.js](https://jp.vuejs.org/v2/guide/instance.html#%E3%83%87%E3%83%BC%E3%82%BF%E3%81%A8%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)

- Vueは、`data()`内プロパティに何らかの状態変化を検知するとDOMを再描画する仕組みです。
    - (全ての状態変化を検知可能な訳ではありません)
- バインディングにメソッドが含まれる場合は、再描画の度にメソッドが再実行されます。

```html:例
<div v-for="method()"></div>
<span>{{method()}}</span>
```


## Bad: `fetch()`を戻した場合
```javascript:JavaScript
attachments(){
    const filenames = this.result;
    this.files = filenames.map((filename)=>{
        return fetch(`https://download.link/files/${filename}`, {method: "GET"})
        .then(res => new File([res.blob()], filename));
    });
    return this.files;
}
```
`attachments()`が終了した直後の`files[]`内は、ファイルの実体にはなっておらず、未解決状態の`Promise`になっています。

```typescript:files[](return直後)
// [aaa.bin, bbb.bin, ccc.bin]
[Promise<pending>, Promise<pending>, Promise<pending>]
```
そして時間経過と共に、ダウンロードが完了した順から

```typescript:files[](時間経過後)
// [aaa.bin, bbb.bin, ccc.bin]
[Promise<pending>, Promise<resolved>(File), Promise<pending>]
```
と言うように`Promise`は解決し、ファイルの実体へと変化します。

つまり`v-for`がループ処理を開始する瞬間の`files[]`内はまだ未解決`Promise`なので、目的の情報は何も取り出せません。

```HTML:HTML
<li v-for="attachment<Promise> in attachments()<Promise[]>">
    <div>{{attachment<Promise>}}</div>
</li>
```
そしてVueは、"配列内の値"の変化は検出不可能なので、時間経過で`Promise`から`File`に変化しても、再描画はされません。


## Good: 配列を戻した場合 (最初のコード)
上から順を追って見て行きます。

```javascript:attachments()
if(!this.result){
    this.files = [];
    return this.files;
}
```
親コンポーネントから受け取った`result`が空だった時のエスケープです。

`v-for`には`[]`が渡されるので、何も描画されません。

```javascript:attachments()
if(this.reset){
    this.files = [];
}
const filenames = this.result;
this.reset = (this.files.length === filenames.length) ? true : false;
```
重要な部分その1。
次の再描画で`files[]`をリセットするかどうかの判定です。

詳しく言うと、次の再描画(再実行)が親コンポーネントからの`result`受け渡しによるものか、`files[]`の変化によるものかを識別する為に存在します。

親コンポーネントから`result`を受け渡された時、もしくはファイル追加による再描画であれば、`files[]`(ダウンロード済ファイル)と`filenames[]`(全ファイル)の配列長は一致しないので、フラグは立たず次の再描画時もリセットはされません。

そして全てのダウンロードが完了した時の再描画で、2つの配列長が一致し、フラグが立ちます。

ファイル追加による再描画はそれ以上発生しないので、もし次に再描画が発生するとしたら、親コンポーネントから`result`を受け取った時だけとなる為、識別が可能となります。

```javascript:attachments()
if(this.files.length === 0){
    filenames.map((filename)=>{
        fetch(`https://download.link/files/${filename}`, {method: "GET"})
        .then((res)=>{
            if(res.ok){
                this.files.push(new File([res.blob()], filename));
            }
        })
        .catch(error => console.log(error));
    });
}
return this.files;
```
重要な部分その2。

まず、`files[]`(ダウンロード済ファイル)が`0`個の時のみ`fetch()`が実行されます。
(無限ループ防止用)

そして`fetch()`は、`files.push(new File)`する`.then()`ハンドラの登録だけ行い、そのまま手付かずの`files[]`を戻して終了します。

この直後の`v-for`には`[]`が渡るので、何も描画はされません。

しかしVueは、"配列内の値"の変化は検知不可能ですが、`.push()/.pop()`など、"配列自体"の変化は検知可能です。

なので、ダウンロードが完了しファイル実体が`.push()`される度に`attachments()`が再実行され、中身が追加された`files[]`が`v-for`に渡り、リストレンダリングが可能となる仕組みです。


# まとめ
これに辿り着くまで、無限ループや無検知など、割と試行錯誤がありましたが結構勉強になりました。

途中「もうFetchは諦めて同期XHRに逃げても良いんじゃない」っていう悪魔の囁きが聞こえた気がしましたが、何とか打ち勝てました。