【Vue 2.x】Composition API の readonly と shallowReadonly で気を付けること

# TL;DR
- `@vue/composition-api` [v1.0.0-beta.21](https://github.com/vuejs/composition-api/releases/tag/v1.0.0-beta.21) から `readonly()`, `shallowReadonly()` が提供されるようになった
- `readonly()` は引数で受け取ったオブジェクトの型を **DeepReadonly 化して返しているだけ**である
- `shallowReadonly()` は Vue 3 のものとほぼ同じように使える

# `readonly()` と `shallowReadonly()` とは？
`readonly()` は Vue 3 ではリリース時から提供されているリアクティビティ API で、リアクティブな状態を受け取ってリードオンリーなリアクティブな状態を返すことを目的とする関数です。

<details>
<summary>`readonly()` のコード例[^1]</summary>
<div>

```javascript
// Vue 3
import { reactive, readonly } from 'vue';

const original = reactive({
  countA: 0,
  foo: {
    countB: 0,
  },
});
const copy = readonly(original);

original.countA++;
copy.countA++; // Set operation on key "countA" failed: target is readonly.
original.foo.countB++;
copy.foo.countB++; // Set operation on key "countB" failed: target is readonly.

console.log(original.countA); // 1
console.log(copy.countA); // 1
console.log(original.foo.countB); // 1
console.log(copy.foo.countB); // 1
```
</div>
</details>

`shallowReadonly()` も Vue 3 ではリリース時から提供されているリアクティビティ API で、`readonly()` との違いはリードオンリーになる範囲がオブジェクトの第1階層の値に限定される点です。

<details>
<summary>`shallowReadonly()` のコード例[^1]</summary>
<div>

```javascript
// Vue 3
import { reactive, shallowReadonly } from 'vue';

const original = reactive({
  countA: 0,
  foo: {
    countB: 0,
  },
});
const copy = shallowReadonly(original);

original.countA++;
copy.countA++; // Set operation on key "countA" failed: target is readonly.
original.foo.countB++;
copy.foo.countB++; // ブラウザのコンソール上に警告が出ない

console.log(original.countA); // 1
console.log(copy.countA); // 1
console.log(original.foo.countB); // 2
console.log(copy.foo.countB); // 2
```

</div>
</details>

## ユースケース
`readonly()` を用いることで `provide()` や props などによって子コンポーネントに状態を渡す際、子コンポーネント側からの状態の変更を防ぐことができます。
このことは Vue 3 ドキュメントの[リアクティブの基礎 - readonly でリアクティブオブジェクトの変更を防ぐ](https://v3.ja.vuejs.org/guide/reactivity-fundamentals.html#readonly-%E3%81%A6%E3%82%99%E3%83%AA%E3%82%A2%E3%82%AF%E3%83%86%E3%82%A3%E3%83%95%E3%82%99%E3%82%AA%E3%83%95%E3%82%99%E3%82%B7%E3%82%99%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E5%A4%89%E6%9B%B4%E3%82%92%E9%98%B2%E3%81%8F%E3%82%99)にも記載があります:

> 例えば、provide されたリアクティブオブジェクトがある場合、それが注入された場所からの変更は防ぎたいことがあります。そうするために、元のオブジェクトに対する読み取り専用のプロキシを作成します

# Vue 2 の `readonly()` と `shallowReadonly()` について
Vue 2 では Composition API は `@vue/composition-api` プラグインとして提供されていますが、`@vue/composition-api` [v1.0.0-beta.21](https://github.com/vuejs/composition-api/releases/tag/v1.0.0-beta.21) から Vue 2 向けに `readonly()`, `shallowReadonly()` が提供されるようになりました。

## `readonly()` は Vue 3 のものとは全く異なる
`@vue/composition-api` での `readonly()` の実装[^2]を確認すると、引数で受け取ったオブジェクトの型を DeepReadonly 化して返しているだけであることがわかります。

そのため TypeScript による型チェックをすり抜けてしまった場合[^3]、Vue 3 の `readonly()` とは異なり**状態の変更ができてしまいます**:

```javascript
// Vue 2
import { reactive, readonly } from '@vue/composition-api';

const original = reactive({
  countA: 0,
  foo: {
    countB: 0,
  },
});
const copy = readonly(original);

original.countA++;
copy.countA++; // ブラウザのコンソール上に警告が出ない
original.foo.countB++;
copy.foo.countB++; // ブラウザのコンソール上に警告が出ない

console.log(original.countA); // 2
console.log(copy.countA); // 2
console.log(original.foo.countB); // 2
console.log(copy.foo.countB); // 2
```

## `shallowReadonly()` は Vue 3 のものとほぼ同じように使える
`shallowReadonly()` は Vue 3 のものと同様にリードオンリーになる範囲がオブジェクトの第1階層の値に限定されますが、Vue 3 のものとほぼ同じように使うことができます。

Vue 2 では場合によっては `readonly()` よりも `shallowReadonly()` を使った方が良いことがあります。
例えば、各プロパティの値がすべてプリミティブ型であるようなオブジェクトには `shallowReadonly()` を用いることで `readonly()` よりも確実に状態の変更を防ぐことができます:

```javascript
// Vue 2
import { reactive, readonly, shallowReadonly } from '@vue/composition-api';

// 各プロパティの値がすべてプリミティブ型(string | number | boolean)である
const original = reactive({
  text1: '',
  text2: '',
  text3: '',
  number1: 0,
  number2: 0,
});

// Not good
const copy1 = readonly(original);

// Better
const copy2 = shallowReadonly(original);
```

<details>
<summary>Vue 2 の `shallowReadonly()` にはできないことについて</summary>
<div>
`@vue/composition-api` での `shallowReadonly()` の実装[^4]を確認すると、引数で受け取ったオブジェクトの(第1階層の)キーでループを回し[^5]、別途定義したオブジェクトに対して同じキーを持つプロパティを定義する[^6]実装となっていることがわかります。
<br>
そのため `shallowReadonly(obj)` 実行時に `obj` が持っているプロパティの値に限ってリードオンリーとすることができます。

```javascript
// Vue 2
import { reactive, shallowReadonly } from '@vue/composition-api';

const original = reactive({
  countA: 100,
});
const copy = shallowReadonly(original);

original.countB = 200;

console.log(copy.countA); // 100
console.log(copy.countB); // undefined
```

```javascript
// Vue 3
import { reactive, shallowReadonly } from 'vue';

const original = reactive({
  countA: 100,
});
const copy = shallowReadonly(original);

original.countB = 200;

console.log(copy.countA); // 100
console.log(copy.countB); // 200
```

</div>
</details>

[^1]: 2020/12/27 時点でリリースされていない Vue 3 の IE11 互換ビルドを利用すると Vue 3 の `readonly()`, `shallowReadonly()` の挙動が本記事で想定しているものと異なるものとなる可能性があります
[^2]: `@vue/composition-api` v1.0.0-beta.22 での `readonly()` の実装: https://github.com/vuejs/composition-api/blob/v1.0.0-beta.22/src/reactivity/readonly.ts#L42-L46
[^3]: 特に Vue では `<template>` 上の `v-model` など型チェックが効きにくい場所があったりします
[^4]: `@vue/composition-api` v1.0.0-beta.22 での `shallowReadonly()` の実装: https://github.com/vuejs/composition-api/blob/v1.0.0-beta.22/src/reactivity/readonly.ts#L48-L97
[^5]: `for (const key of Object.keys(obj)) { ... }` の部分
[^6]: `Object.defineProperty(readonlyObj, key, { ... })` の部分