292
209

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Vue #2Advent Calendar 2019

Day 4

きたるべきvue-nextのコアを理解する

Last updated at Posted at 2019-12-03

この記事は、Vue #2 Advent Calendar 2019の4日目。
@nagimaruxxxさんのフロントエンド開発をjQueryからVue.jsへ乗り換えたので比較してみるの次の記事です。

この記事でわかること

jQueryからVue.jsにはじめて移行したとき、「thisのなんちゃらを書き換えると動くんだー!」とか「computedって、依存する値が更新されたら自動で更新されてすごい!」というのが感想でした。

今回は、vue-nextという、いわば、「次世代のvue」でそういった「自動で更新されてすごい」がどのように実装されているか、を解説します。

普段Vue.jsを使っている人は、その裏の仕組みに感銘を受けるでしょう。日々の実装を少しだけ、いつもと違う視点で見れるようになると思います。
普段React.jsを使っている人はきっと、Vue.jsを使いたくなることでしょう。
皆さんもぜひリアクティブシステムの世界を少し味わってみてください。

目次

背景などを書いていたらかなり長くなってしまったため、「hooksとかvue-nextの話は聞いたことがある!俺は最低限の内容だけ読みたいんだ!」という方は「vue-nextのリアクティブシステム」まで飛ばすことをおすすめします。

そもそもvue-nextとは?

Vue.jsは2019年12月3日現在、v2.6.10がリリースされています。おそらく、世の中のVue.jsプログラムのほとんどは2系でしょう。
その一方、2018年の秋あたりから、Vue.jsはv3.xへのプラン1を予告していました。
各種カンファレンスだけでなく、公式のブログ2でも言及されており、「徐々に2系でも使えるようにしていく」と公言しています。

実際、vue-nextのリアクティブシステムのいくつかはvue-composition-apiを通じてvue 2系でも利用することができます。
リリースの時期は決まっていませんが、2019年の終わりから2020年の初めにかけてであろうと予想されています。(もしかしたら、このアドベントカレンダーの最中にリリースされるかもしれないですね...!)

背景

vue-nextに至るまでの動向をわかる範囲で書きます。
vue-nextが実装されるまでの経緯なので、ざっと流してもらっても構いません。

React hooksの登場

2018/10/25,26のReact Conf 2018で紹介された「React hooks」は、仮想DOM以来のフロントエンドでの大きな変化であるとみなされ、大きな注目を浴びました。3

React.jsフロントエンドの課題であった「型付けの困難さ」「ロジックの再利用の困難さ」を大幅に緩和し、フロントエンドの開発の様相を一変させたと言ってもいいでしょう。

RFCとしての登場

React hooksはそのままではVue.jsには移植できないものの、コンセプトや対処できる課題としてはVue.jsフロントエンドの課題と共通するものがあります。
とはいえ、そんなにすぐには対応なんてできません。
React界隈がhooksに湧いている中、Vue.jsではコアに特に大きな変更はなかったので、「あいつらいいなあ」と思ったのを覚えています。

状況が変わりだしたのは2019年の3月末です。
元々、v3.xでは今までの書き方を整備、発展させた「Class API」を提供する予定でした。
しかし、「型づけ大変だし、React hooksみたいなAPIがあればそんなの要らんくない?」ということでそのRFCは破棄されます。4今思うとなかなかすごい変更です。
Vue.jsでReact hooksに相当するRFCがここで登場します。5

実装の登場

2019年の4月時点では、vueのReact hooksに相当するものはRFCとしてはあるものの誰も使えないみたいな状況でした。
しかし、6月9日に「HooksライクなAPIのRFCを(ちゃんと)出したよ」というツイートがなされ、その上でVue.js 2系でも使える実装であるvue-function-apiが登場します。

実装が登場したことで一気にコミュニティーは活発になりました。6

そして、2019年の8月にVue.jsの本家に管理が移行7し、名前も「vue-composition-api」に変わり、現在に至ります。
現在、コミュニティーでVue.js 3ライクな書き方をしたい場合は、このvue-composition-apiを使うことになると思います。qiitaにも既にたくさんの記事がありますね。

vue-nextのアナウンス

さて、部分的に使えるようになり、あとは本体のバージョンアップを待つだけという状態になって、10/5に以下のツイートがなされます。

最初のアナウンスから1年の時を経てようやく、次世代のVue.jsを垣間見ることができるようになったわけです。

Vue.jsのcomposition-apiはReact hooksとどう違うのか?

「Vue版のHooks」とも言われる「vue-composition-api」(そしてvue-nextのシステム)ですが、先発にあたるReact hooksの長所を生かしつつ、短所を低減しています。

ここら辺はいろんな記事が書いているところでもあるので簡単に書きます。

React hooksはなぜ歓迎されたのか

Functional Componentの表現力向上

React hooksが登場するまで、React.jsのFunctional Componentはpropsしか受け取ることができませんでした。つまり、状態を持つことができなかったわけです。
Hooksが登場したことにより、今まではクラスベースのコンポーネントでしかできなかったこともFunctional Componentでできるようになりました。

型との相性

システムが大規模になればなるほど、型による静的な検査は効いてくるようになります。
関数の型をつけるのはクラス内でのそれよりも比較的簡単であるため、ライブラリの型定義などが改善され、より良い型づけがなされたプログラムを簡単に書けるようになりました。

ロジックの再利用性の向上

今まではライフサイクルごとの処理を再利用したい場合、React.jsの場合は基本的にHOC(Higher Order Component)しか手段がありませんでしたが、カスタムフックを作成することによりそれらを簡単に切り出すことができるようになりました。

React hooksの問題点

依存性を明示しなければいけない場合がある

useEffectがこの例にあたります。
公式にもありますが、useEffectはコンポーネントが更新されるたびに走るため、やや「走り過ぎ」なところがあります。8
それを間引き、useEffect内で参照した変数が更新されたタイミングでのみuseEffectを実行するためには、React hooksでは依存する物を明示的に示さなければいけません。これはややトリッキーです。

書ける場所の制限

当然といえば当然なのですが、ReactのHooksを使うことができ、その恩恵にあずかることができるのはReactのFunctional Component内のみです。

if,for,ネストした関数の中で使えない

Hooksを使う上で一番問題になるのはこのケースでしょう。フロントエンドにかかわらずこうした制御構造は大量に出てくるのにもかかわらず、Hooksはこれらの中では基本的には使うべきではないとされています。(繊細な注意を払えば使えなくはないですが、一歩間違えると変数の内容が「ズレる」という憂き目に遭います。)

これはReact hooksが、「呼ばれた順番でhookと値を紐づけている」という実装をしていることに基づく問題と考えられます。なかなか根が深いです。

vue-composition-apiの特徴

vue-composition-api並びにvue-nextでは、React hooksの長所を生かしつつ、上にあるような短所が現れないような実装になっています。

以下のコードを読んでみてください。(vue-composition-api公式のサンプルコードです。importは適宜省略しています。)

const state = reactive({
  count: 0
})

watch(() => {
  document.body.innerHTML = `count is ${state.count}`
})

state.countが更新されたらinnerHTMLを書き換える、という非常に単純なサンプルです。

このreactiveというところで、Vue.jsは「リアクティブな値」を作成しています。
そして、watchに関数を渡すことで、state.countが更新されたら実行されるという動作を宣言しています。

まず注意したいのは、このコードは別にコンポーネント内でなくても動くっちゃ動くというところです。コンポーネント内で宣言をしなければいけなかったReact hooksとは違い、リアクティブな値を生で扱うことができます。if文なども当然問題ありません。

また、特に依存性をwatchで宣言しなくても、「使ったものだけ監視する」という挙動をします。
これにより、開発者はよりロジックの実装に集中することができます。

もちろん、React hooksの長所である「ロジックの再利用」なども同じように実現できます。(vue-composition-apiの公式ではマウスの場所を追跡するというのを例にサンプルコードを解説を載せています。)

vue-composition-apiをReact hooksを異なるものにしているのは「リアクティブな値を生で扱うことができること」でしょう。

これにより、vue-nextでは非常にシンプルにフロントエンドを説明することができるようになります。
ドキュメントにもあるとおり、vueのレンダリングは究極的には「リアクティブな値をwatchしているだけ」と言えるようになるからです。9

(コードは例によって公式から引用しています。)

const state = reactive({
  count: 0
})

function increment() {
  state.count++
}

const renderContext = {
  state,
  increment
}

watch(() => {
  // hypothetical internal code, NOT actual API
  renderTemplate(
    `<button @click="increment">{{ state.count }}</button>`,
    renderContext
  )
})

vue-nextを読み解くには

vue-nextのリポジトリ自体はGithubにあるわけですが、見ればわかるとおりどこにも説明などのdocsがないので、そこらへんは自力で知る必要があります。

まず、この記事でもたびたび引用していたVue Composition API RFCのページを一通り眺め、使い方を知っておくとよいでしょう。というのも、表面的な振る舞いはここから大きく変わらないとされているからです。

事前知識を仕入れたところで、次に見るのはvue-nextのContributing Guideです。ここでは基本的な開発コマンドの説明やコントリビューションの規則に加えて、プロジェクト構成が書かれているからです。

今回の場合はリアクティブシステムを読むわけなので、「packages/reactivity」フォルダを見ればいいことがわかります。

最後に見るべきは「テストコード」です。テストコードには、ライブラリの作者が想定した使いかたについて、満足すべき挙動がずらっと書いてあります。使い方のドキュメントがない現状では、おそらくこのテストコードが一番のドキュメントです。(マイナーOSSあるある)

また、vue-nextのコードは全てTypeScriptで書かれているため、型情報も読み解く上で有用なヒントになります。

あとは関数の呼び出しを遡って行って、時に自分で実際にテストケースを書いて動かしたりしていきましょう。それではようやく本題です。

vue-nextのリアクティブシステム

そもそも「リアクティブ」とは?

これから「リアクティブな値」の実装をみていくわけですが、その前に「リアクティブ」って何?とならないように改めて述べておきます。

例えば以下のコードがあったとしましょう。

let a = 5
let b = a * 10
a = 7
console.log(b) // => 50

これを実行すると50と出力されます。2行目で「bにaの10倍を代入する」としたものの、aに7を代入したことでその関連性が崩れています。bはもはやaの10倍ではなくなってしまいました。
これを直してやるにはもう一回代入してやるしかありません。

b = a * 10
console.log(b) // => 70

呆れるくらい当たり前ですね。

今回の例は、非常に簡単なものでしたので、一文だけ付け足すことで「bはaの10倍なんだよ」とすることができます。
ですが、例えばbにあたるものが100個、200個あったらどうでしょう。

let a = 5
let b_000 = a * 21
let b_001 = Math.ceil(a / 2.71828)
// ...
let b_173 = Math.sin(a)
// ...
// コードのどこかのタイミングでaを更新する
a = 8

とても更新が大変そうですね。いくつか忘れてしまいそうです。
もちろん、関数としてこれらの再代入を記述し、毎回書くというアプローチをとることができます。

a = 8
makeSideEffectA(a)

ですが、変数ごとにこんなことをしていたら全然捗りませんし、何よりめんどくさいですよね。

ここでリアクティブシステムを入れてみましょう。

a = ref(5)
const b_000 = computed(() => a.value * 21)
const b_001 = computed(() => Math.ceil(a.value / 2.71828))
// ..
a.value = 10
console.log(b_000.value) // => 210

特に再代入などをしなくても、bの方に値が反映されました。
このように、一旦関係性、例えば「bはaの10倍だよ」とcomputedで定めておけば、そのあとはaを書き換えればbも自動で更新されます。
こうすれば、「bは代入したときはaの10倍だったけど、この行だと分からんなー」みたいなことがなくなり、「いや、いつでも10倍です」とわかるので、考えることが減りますね。代入忘れもおきません。

このように、ある変数を書き換えた時に、事前に定めた関係性を元に、他の変数が適切に更新されたり、事前に定めた動作が発動することを「リアクティブである」と言います。10

リアクティブシステムの登場人物たち

vue-next及びvue-composition-apiでは様々な関数が利用可能ですが、基幹である関数はかなり少数です。
依存性を辿っていくと、本質的には以下の2つしかないことがわかります。

  • reactive
    • 普通の値をリアクティブな値に変換する
  • effect
    • 値が変更された時の動作を記述する

他の利用可能な関数はほとんどこれをちょっと変更した程度で実装されているので、この2つをメインに追いつつ、重要な関数や変数を追っていきます。

普通の値からリアクティブな値を作り出す「refreactive

vue-next(vue-composition-api)ではリアクティブな値を作るための手段としてcomputed, readonly, ref, reactiveの4つがありますが、computedはリアクティブな値だけというよりは、リアクティブな値同士を組み合わせて作るという側面が強く、readonlyはほとんど実装がreactiveと同じため、実質的には後者の2つがメインです。
では順にみていきます。

プリミティブな値をリアクティブにする「ref

実装箇所:https://github.com/vuejs/vue-next/blob/dec444ef04e4a3aca72b965f497ae4c2f73df09c/packages/reactivity/src/ref.ts#L33

ここでの「プリミティブ」とは、「オブジェクト(typeofした時に'object'かつnullでない)でない」くらいの意味です。もしオブジェクトの場合、リアクティブな値を作る際に内部的に自動でreactiveが用いられます。
メインのコードとしては以下のような感じです。(適宜コメントを付与しています。)

export function ref(raw?: unknown) {
  // ... 略 ...
  const r = {
    _isRef: true,
    get value() {
      track(r, OperationTypes.GET, 'value')
      return raw
    },
    set value(newVal) {
      raw = convert(newVal)
      trigger(
        r,
        OperationTypes.SET,
        'value',
        __DEV__ ? { newValue: newVal } : void 0
      )
    }
  }
  return r
}

「プロパティーにgetとかsetがある?」となった人はJavaScriptのGetter/Setterについて調べておくとよいでしょう。
get value()(ゲッター)では、オブジェクトのvalueプロパティーを参照した時に実行される関数を記述しており、set value(newVal)(セッター)では逆に代入を行った時に実行される関数を記述しています。

先ほどの『そもそも「リアクティブ」とは?』でも述べたように、リアクティブな値は代入した時に他の変数を更新して欲しいので、triggerのところがそれをやっているのではないかという想像がつきます。
では、Getter内にあるtrackは何をしているかというと、例えばcomputed内でtrack関数が呼ばれた時に、「このcomputedはこの値に依存しているぞ!」という記録を行っています。具体的に中がどうなっているかはまた後で述べます。

プリミティブではない値をリアクティブにする「reactive

実装箇所:https://github.com/vuejs/vue-next/blob/dec444ef04e4a3aca72b965f497ae4c2f73df09c/packages/reactivity/src/reactive.ts#L52

export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  // 既にリアクティブなオブジェクトの一覧に登録されている場合、それを返す
  if (readonlyToRaw.has(target)) {
    return target
  }
  // target is explicitly marked as readonly by user
  if (readonlyValues.has(target)) {
    return readonly(target)
  }
  return createReactiveObject(
    // 元々の値
    target,
    // 元々の値から対応するリアクティブな値を引っ張り出せるWeakMap
    rawToReactive,
    // 上の逆の写像ができるWeakMap
    reactiveToRaw,
    // Proxyで使うハンドラー
    mutableHandlers,
    // 元々の値がSet,Mapなどの「コレクション」だった時に使うProxyのハンドラー
    mutableCollectionHandlers
  )
}

リアクティブな値にする値だけでなく、「値とリアクティブな値の対応が入ったWeakMap」を渡しているのは、同じオブジェクトから異なるリアクティブな値を作り出さないためと考えられます。
「あらゆるリアクティブな値への参照が入った変数」は一見メモリ的にやばそうですが、WeakMapなので、このページにあるとおり、格納しているオブジェクトを参照する手段が消滅した時に自動でガベージコレクションされるため大丈夫というわけです。

createReactiveObjectの中身は以下のようになります。

function createReactiveObject(
  target: unknown,
  toProxy: WeakMap<any, any>,
  toRaw: WeakMap<any, any>,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  // ... 略 ...
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
  // リアクティブな値の作成
  observed = new Proxy(target, handlers)
  // ... 略 ...
  // 依存する変数、関数を記録する領域の確保
  if (!targetMap.has(target)) {
    targetMap.set(target, new Map())
  }
  return observed
}

ほとんどの行は「対応するリアクティブな値があるか」「もとの値とリアクティブな値をちゃんと紐づける」で消費されていますが、2つだけ違うものがあります。

まず一つは、Proxyによるリアクティブな値の作成です。見ての通り、vue-nextのリアクティビティーの核はProxyによるものです。MDNのProxyの解説を見るとわかりますが、Proxyはとても広範な操作に介入することができます。
これを用いて、各プロパティーの変更などを検出しています。
その際も、refの時と同じように、取得の時にはtrack。設定の時はtriggerが呼び出されます。

特に重要なのは、「array[1] = 5」や「object['存在しなかったキー'] = value」のような操作も検出できるようになったことです。
これにより、VueやReact初心者が詰まりがちな**「代入でキーを追加する」といった動作でもリアクティビティーが保たれるようになります。**11

もう一つは「依存している変数や関数の記憶領域」の作成です。
後でまたでてくると思いますが、vue-nextのリアクティブな値たちは、**自分が使われた関数を記憶しています。**そのための記憶領域です。

関係性と動作を宣言する「effect」

実装箇所:https://github.com/vuejs/vue-next/blob/dec444ef04e4a3aca72b965f497ae4c2f73df09c/packages/reactivity/src/effect.ts#L44

effectは関数を受け取り、関数内で参照したリアクティブな値が更新された時に関数を再度実行します。
実際のテストコードを見るとこんな感じです。

it('should be reactive', () => {
  const a = ref(1)
  let dummy
  effect(() => {
    dummy = a.value
  })
  expect(dummy).toBe(1)
  a.value = 2
  expect(dummy).toBe(2)
})

なかなか面白いことをしますね。effect内で参照したaを更新すると関数ももう一回実行され、dummyも更新されます。watchcomputedの裏側にはこんな奴が控えています。

少々長いのですが、まずはいったん関連する処理をピックアップします。

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  // ... 略 ...
  const effect = createReactiveEffect(fn, options)
  // ... 略 ...
  return effect
}

// ... 中略 ...

function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(...args: unknown[]): unknown {
    return run(effect, fn, args)
  } as ReactiveEffect
  // ... 略 ...
  return effect
}

function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown {
  // ... 略 ...
  if (!effectStack.includes(effect)) {
    // ... 略 ...
    try {
      effectStack.push(effect)
      // 関数を実際に実行している箇所
      return fn(...args)
    } finally {
      effectStack.pop()
    }
  }
}

初見だと理解が難しい箇所です。ですが、creativeReactiveEffectrunに注目すると多少はマシになります。

creativeReactiveEffectでは、関数をラップしてReactiveEffectなる関数を作り出しています。
runではこれをeffectStackというスタック(配列)の一番上に積んで、渡された関数を実行し、またpopしています。

はっきりいってスタックを積んでから関数を実行だけしてまたpopしているだけで、これがどう「リアクティブ」につながるのかはここからだと全くわかりません。
強いていうなら、「関数が実行されている間は、対応するReactiveEffectが配列の一番上にいる」くらいな物です。

この意味を把握するためには、先ほどスルーした「track」と「trigger」に分け入る必要があります。

関係性と動作を記録する「track」、再生する「trigger

先ほど「trackでは『このcomputedはこの値に依存しているぞ!』という記録を行っている。」「triggerでは事前の関数や変数を元に値を再計算している」ということを述べましたが、それの詳細を書いていきます。

関係性を再構築する「trigger

実装箇所:https://github.com/vuejs/vue-next/blob/dec444ef04e4a3aca72b965f497ae4c2f73df09c/packages/reactivity/src/effect.ts#L148

export function trigger(
  target: object,
  type: OperationTypes,
  key?: unknown,
  extraInfo?: DebuggerEventExtraInfo
) {
  const depsMap = targetMap.get(target)
  // ... 略 ...
  const effects = new Set<ReactiveEffect>()
  const computedRunners = new Set<ReactiveEffect>()
  // ... 略 ...
  if (type === OperationTypes.CLEAR) {
    // collection being cleared, trigger all effects for target
    depsMap.forEach(dep => {
      addRunners(effects, computedRunners, dep)
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      addRunners(effects, computedRunners, depsMap.get(key))
    }
    // ... 略 ...
  }
  const run = (effect: ReactiveEffect) => {
    scheduleRun(effect, target, type, key, extraInfo)
  }
  // Important: computed effects must be run first so that computed getters
  // can be invalidated before any normal effects that depend on them are run.
  computedRunners.forEach(run)
  effects.forEach(run)
}

typekeyごとの条件分岐を全部載せると冗長なので、一部だけ載せています。
このaddRunnersという部分で「こういう操作をされたんだけど、どんな関数を実行すべきかな?」というのを構築します。
例えば、コレクション(SetやMapの場合)がクリアされた場合(type === OperationTypes.CLEARの部分です)、それが使われた変数を全て再計算しないといけないため、関連する関数を全部拾い出しています。
depsMapには、「targetが計算に使われた関数」の情報が入っているので、それをいったん変数にまとめて、最後に一気に実行しています。

記憶を司る「track

export function track(target: object, type: OperationTypes, key?: unknown) {
  // ... 略 ...
  const effect = effectStack[effectStack.length - 1]
  // ... 略 ...
  let depsMap = targetMap.get(target)
  // ... 略 ...
  let dep = depsMap.get(key!)
  // ... 略 ...
  if (!dep.has(effect)) {
    dep.add(effect)
    effect.deps.push(dep)
    // ... 略 ...
  }
}

targetMapは「ある値が依存している関数や値の一覧」と先ほど書きました。
ここでは、そんなtargetMapになぜか「effectStackというスタックの一番上」を追加しています。これは一体どういうことでしょう。

ここで、先ほどの「関数が実行されている間は、対応するReactiveEffectがeffectStackの一番上にいる」という性質が生きてきます。

順を追うとこうなります。

  1. effectが関数fnを受け取る.fnの中ではリアクティブな値が参照されており、実行するとtrackが呼び出される。
  2. effectStackの一番上に「fnをラップしたReactiveEffect」が積まれる
  3. fnを実行する。
  4. fnの中ではリアクティブな値が参照されており、実行するとtrackが呼び出される。
  5. この間effectStackの一番上は「fnをラップしたReactiveEffect」なので、trackにより、targetMapに「fnをラップしたReactiveEffect」が入る。
  6. 実行が終わるとeffectStackはpopされる

そう、effectStackの正体はその名の示す通り、effectだけが関与する。vue-nextが保有するコールスタックだったのです!

effectの中でさらにcomputedを介してeffectが呼ばれた場合、そのスタックは積み上がっていきますし、これにより「リアクティブな値が、『自分が参照された時、今どんな関数が実行されているか』を知ることができる」ということです。

React hooksではこれを単純に呼ばれた順番で管理しているため、if文などによる「ズレ」が発生しますが、vue-nextではコールスタックを自前で持つことでこの問題を回避しています。

まとめ

  • vue-nextのリアクティブな性質はProxyを核に実装されている
  • リアクティブな値は「自分の値が使われた関数のリスト」を記憶している
  • vue-nextは自分でコールスタックを持つことで、上の記録をすることを可能にしている

vue-nextのリアクティブシステムの陥穽

vue-nextのリアクティブシステムは、React hooksの課題をいくつか解消しましたが、完璧ではありません。その最たる例として「非同期処理」を挙げます。

非同期処理

コールスタックを作っているところのコードを再掲します。

function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown {
  // ... 略 ...
  if (!effectStack.includes(effect)) {
    // ... 略 ...
    try {
      effectStack.push(effect)
      // 関数を実際に実行している箇所
      return fn(...args)
    } finally {
      effectStack.pop()
    }
  }
}

冷静に考えるとこれはおかしくないですか?
例えば、複数のfnが同時に走ったり、なんてことがあったら「関数が実行されている間は、対応するReactiveEffectがeffectStackの一番上にいる」なんて前提はたやすく崩壊してしまいますよね?

しかしながら、普通JSは同期実行の場合、一時に関数を一つしか実行していない。という前提が成り立つため、これで何とかなります。例えば、Node.jsやブラウザの場合、イベントループでこうなっていることがわかります。12

一見安心なのですが、fnが非同期処理になった途端に、例えばasync functionになった瞬間におかしなことが起こります。

fnasync functionだった場合、fn(...args)の行は一瞬で実行され、Promiseが返ってしまうため13、その中身のtrackなどはとんちんかんなタイミングで実行されてしまうからです。

まあ幸いなことに、他の同期的なeffectが実行された場合、そちらがeffectStackの一番上に積まれるので、「参照した奴が更新されてるのに再計算されない!」は起こらないと考えられます。多少無駄な計算が走るくらいでしょう。

ただ、当の非同期処理がどのようになるかは細心の注意を払う必要があります。少なくともasync functionにするのはやめるべきですし、promise.thenの内部でリアクティブな値を使って計算するのはやめた方がいいでしょう。いづれの場合もリアクティブにはならないと考えられます。

現状一番マシな手段は、「返り値のPromiseの状態が更新されたら変化するリアクティブな値」を作ることです。14

最後に

長々と書いてしまいました。いかがでしたでしょうか。
vue-nextは「リアクティブな値を生で扱える」という非常に優れた性質を持っています。
関数型プログラミングなどとの相性もよく、これからの発展が非常に楽しみな技術ですね。
Vue 3系がリリースされてフロントエンドのデファクトになる日も遠くないかもしれません。
それでは、よきVue.jsライフを。

そして、友人であり、React.jsプログラマーでありながら確認・指摘をしてくださった@hikarinotomadoiにこの場を借りて感謝を申し上げます。

  1. 昨年のアドベントカレンダーの時点でかなり議論が進んでいるのがわかります。

  2. ただし、この公式ブログの予告内容は日付を見てもわかるように少し古いところがあります。

  3. 「Concurrent React」もこの時期に提案されているのですが、こちらの実装はHooksと比べるとかなりゆっくりと行われた印象です。当時の様子はこちら

  4. 「React hooksにインスパイアされてるけれど、うちらのやり方でやるよ」とも明記されています。https://github.com/vuejs/rfcs/pull/17#issuecomment-494242121

  5. 議論を追っていくと、今のvue-nextやvue-composition-apiに相当するものがどう決まっていったのかが伺えてなかなか面白いです。

  6. もちろんいろんな混乱もありました。「今までのコードは全部書き直し!?」をはじめとするデマも発生したりしています。https://dev.to/danielelkington/vue-s-darkest-day-3fgh

  7. https://github.com/liximomo/vue-function-api/issues/14

  8. https://ja.reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect

  9. もちろん実際には中でより複雑に更新判定などを行ってます。あくまでも仮想的なコードです。

  10. 完全に余談ですが、関数型プログラミングに慣れ親しんだ人ならこの書き方で「副作用」を連想するかもしれません。vue-nextの「リアクティブな値」は、「代入による副作用」を扱うことができ、実際にモナド則を満たす系を構成することができます。Vueのrefの命名がhaskellのSTRefモナドにちなんでいると考えるのは...流石に考えすぎでしょうか。

  11. ProxyはIE11では使えないので、何らかの代替手段を提供はするようです。ただしパフォーマンス、機能ともに落ちるとのこと。これを機に脱IEが進むといいですね。同じような戦略をとっているMobxも似たことを言っているのでそうなりそうです。

  12. https://developer.mozilla.org/ja/docs/Web/JavaScript/EventLoop

  13. ここを勘違いしている人はそこそこいるようです。例えば全ての行を実行するのに3秒かかるasync functionがあったとしても、async functionの返り値であるPromiseが生成されるのは一瞬です。3秒かかるのはその返り値のPromiseが解決されるのにかかる時間です。awaitで実行が止まっているように見えるのは、このPromiseが解決するのを待っているからです。

  14. しかしそういう値は今度はawaitで待てたりしないなど、Promiseの持つ性質を持たないという問題を抱えます。これを解決するコードは頻出かつ単純なので、これに特化したnpmパッケージを出す予定です。

292
209
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
292
209

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?