6
8

More than 5 years have passed since last update.

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

Posted at

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

ズバリ

HTML
<ul>
    <li v-for="attachment in attachments()">
        <div>{{attachment.name}}</div>
        <div>{{attachment.size / 1024}} KB</div>
    </li>
</ul>
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

  • Vueは、data()内プロパティに何らかの状態変化を検知するとDOMを再描画する仕組みです。

    • (全ての状態変化を検知可能な訳ではありません)
  • バインディングにメソッドが含まれる場合は、再描画の度にメソッドが再実行されます。

<div v-for="method()"></div>
<span>{{method()}}</span>

Bad: fetch()を戻した場合

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になっています。

files[](return直後)
// [aaa.bin, bbb.bin, ccc.bin]
[Promise<pending>, Promise<pending>, Promise<pending>]

そして時間経過と共に、ダウンロードが完了した順から

files[](時間経過後)
// [aaa.bin, bbb.bin, ccc.bin]
[Promise<pending>, Promise<resolved>(File), Promise<pending>]

と言うようにPromiseは解決し、ファイルの実体へと変化します。

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

HTML
<li v-for="attachment<Promise> in attachments()<Promise[]>">
    <div>{{attachment<Promise>}}</div>
</li>

そしてVueは、"配列内の値"の変化は検出不可能なので、時間経過でPromiseからFileに変化しても、再描画はされません。

Good: 配列を戻した場合 (最初のコード)

上から順を追って見て行きます。

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

親コンポーネントから受け取ったresultが空だった時のエスケープです。

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

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を受け取った時だけとなる為、識別が可能となります。

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に逃げても良いんじゃない」っていう悪魔の囁きが聞こえた気がしましたが、何とか打ち勝てました。

6
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
8