LoginSignup
1
1

More than 5 years have passed since last update.

takeWhile はメモリリーク (無駄なメモリ消費) の原因になり得る

Posted at

takeWhile に参照透過でない式を渡すのは良くない。

上記が分かっていれば下記を読む必要はありません。

経緯

React開発ノウハウメモ(随時更新)
上記記事を読んで unsubscribecomponentWillUnmount に書かない方法を知ったので調べてみた。1

takeWhile はメモリリークの原因になり得る。

takeWhile は値が来たときのみ実行されるので値が来なければ unsubscribe されない。
無駄なネットワーク通信やDBへのアクセスを続けてしまう。
takeWhile は値に応じて処理を続けるべきか決まる場合で使うべき。

Observable を流れる値と関係なく処理を止めたいなら unsubscribe を呼ぶか takeUntil を使う。

テストしてみる

実際に試してみるためのコードを書いた。
それらで下記の 3 つの関数を使っている。

utils.ts
// s 秒待つ
const sleep = (s: number) => new Promise(r => setTimeout(r, s * 1000))
// タイムスタンプをつけてログを出す
const log = (...v: any[]) => {
    const ts = (performance.now() / 1000) | 0
    console.log(...v, `(${ts})`)
}
// キャンセルできる Observable を生成
const make = (name: string) => {
    return new Observable<number>(s => {
        let canceled = false
        const push = (v: number) => {
            log(name, 'sub1', v, '- canceled', canceled)
            s.next(v)
        }
        Promise.resolve().then(async () => {
            push(12) // すぐに 12 を渡す
            await sleep(10) // 10 秒待ってから
            push(13) // 13 を渡す
            push(14) // 14 を渡す
        })
        return () => {
            log(name, 'unsub1')
            canceled = true
        }
    })
}

テスト内容

上記 makeObservable を生成したのち 1 秒後に処理を止める。

unsubscribe する

一般的なやりかたであると思われる。
問題なく動いている。

test1.ts
const test1 = async () => {
    const read = make('test1').pipe(
        tap(v => log('test1', 'sub2', v)),
    ).subscribe()
    await sleep(1)
    read.unsubscribe()
}
test1() /*
test1 sub1 12 - canceled false (0)
test1 sub2 12 (0)
test1 unsub1 (1)
test1 sub1 13 - canceled true (10)
test1 sub1 14 - canceled true (10)
*/

takeWhile を使う

1 秒後に止めようとしたが、その後 9 秒経って次の値が来るまで止まっていない。

test2.ts
const test2 = async () => {
    let alive = true
    make('test2').pipe(
        takeWhile(() => alive),
        tap(v => log('test2', 'sub2', v)),
    ).subscribe()
    await sleep(1)
    alive = false
}
test2() /*
test2 sub1 12 - canceled false (0)
test2 sub2 12 (0)
test2 sub1 13 - canceled false (10)
test2 unsub1 (10)
test2 sub1 14 - canceled true (10)
*/

takeUntil を使う

問題なく動く。

test3.ts
const test3 = async () => {
    const unsub = new Subject<void>()
    make('test3').pipe(
        takeUntil(unsub),
        tap(v => log('test3', 'sub2', v)),
    ).subscribe()
    await sleep(1)
    unsub.next()
    unsub.complete()
}
test3() /*
test3 sub1 12 - canceled false (0)
test3 sub2 12 (0)
test3 unsub1 (1)
test3 sub1 13 - canceled true (10)
test3 sub1 14 - canceled true (10)
*/

unsubscribetakeUntil 、どちらを使うべきか?

こちらの記事 (英語)では takeUntil がオススメされている。

手続き的な unsubscribe よりも宣言的な takeUntil が良いという判断だろうか?

補足

ReactRxJS を組み合わせる場合

大規模なアプリなら redux-observable を、小規模なら rxjs-hooks を使うことを勧める。
古い React を使っているなら recompose も良い。


  1. useEffect のある今の ReactcomponentWillUnmount のような古い方法を使う必要はないと思う。 

1
1
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
1
1