背景
- 最近仕事でVue.jsを使ったPJの保守・運用をする必要が生まれた
- React.jsは数年間触ってきたがVueは初めてである
- そのためでReactと比較した学習ログを残す
Reactとの違い
- どちらもVDOMのライブラリ
- 大きな違いは次になる
- ReactはJSX、Vueはディレクティブの記法
- Reactはhooks(イミュータブル)で、VueはComposition API(ミュータブル)
- Reactは何度も関数が実行される、VueはSetupが一度だけ実行される
React:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import React, { useState } from 'react';
function App() {
const [message, setMessage] = useState('Hello, React!');
return (
<div>
<p>{message}</p>
<button onClick={() => setMessage('Updated message')}>
Update Message
</button>
</div>
);
}
export default App;
|
Vue:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| <template>
<div>
<p>{{ message }}</p>
<button @click="updateMessage">Update Message</button>
</div>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const message = ref('Hello, Vue!');
const updateMessage = () => {
message.value = 'Updated message';
};
return {
message,
updateMessage
};
}
};
</script>
|
ディレクティブ
v-text
いわゆるタグの中身を表示する。
1
2
3
| <span v-text="msg"></span>
<!-- same as -->
<span>{{msg}}</span>
|
v-bind
いわゆるpropsを渡す記法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| <!-- 属性をバインドする -->
<img v-bind:src="imageSrc" />
<!-- 動的な属性名 -->
<button v-bind:[key]="value"></button>
<!-- 省略記法 -->
<img :src="imageSrc" />
<!-- 同名省略記法(3.4+), :src="src" のように展開する -->
<img :src />
<!-- 動的な属性名の省略記法 -->
<button :[key]="value"></button>
<!-- インラインの文字列連結 -->
<img :src="'/path/to/images/' + fileName" />
<!-- クラスのバインド -->
<div :class="{ red: isRed }"></div>
<div :class="[classA, classB]"></div>
<div :class="[classA, { classB: isB, classC: isC }]"></div>
<!-- スタイルのバインド -->
<div :style="{ fontSize: size + 'px' }"></div>
<div :style="[styleObjectA, styleObjectB]"></div>
<!-- 属性のオブジェクトをバインド -->
<div v-bind="{ id: someProp, 'other-attr': otherProp }"></div>
<!-- props のバインド。"prop" は子コンポーネントで宣言する必要がある -->
<MyComponent :prop="someThing" />
<!-- 親の props を子コンポーネントと共有するために渡す -->
<MyComponent v-bind="$props" />
<!-- XLink -->
<svg><a :xlink:special="foo"></a></svg>
|
v-else-if
if文。
1
2
3
4
5
6
7
8
9
10
11
12
| <div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>
|
v-for
テンプレートブロックを複数回レンダリングする記法。
1
2
3
4
5
6
7
8
9
10
11
| <div v-for="item in items">
{{ item.text }}
</div>
<div v-for="(item, index) in items"></div>
<div v-for="(value, key) in object"></div>
<div v-for="(value, name, index) in object"></div>
<div v-for="item in items" :key="item.id">
{{ item.text }}
</div>
|
v-model
- フォーム入力要素またはコンポーネントに双方向バインディングする記法
<input>
, <select>
, <textarea>
のみ対応
v-on
イベントリスナーを定義できる。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| <!-- メソッドハンドラー -->
<button v-on:click="doThis"></button>
<!-- 動的イベント -->
<button v-on:[event]="doThis"></button>
<!-- インラインステートメント -->
<button v-on:click="doThat('hello', $event)"></button>
<!-- 省略記法 -->
<button @click="doThis"></button>
<!-- 動的イベントの省略記法 -->
<button @[event]="doThis"></button>
<!-- stop propagation -->
<button @click.stop="doThis"></button>
<!-- prevent default -->
<button @click.prevent="doThis"></button>
<!-- 式なしで prevent default -->
<form @submit.prevent></form>
<!-- 修飾子の連鎖 -->
<button @click.stop.prevent="doThis"></button>
<!-- キーのエイリアスを用いたキー修飾子 -->
<input @keyup.enter="onEnter" />
<!-- 一度だけトリガーされるクリックイベント -->
<button v-on:click.once="doThis"></button>
<!-- オブジェクト構文 -->
<button v-on="{ mousedown: doThis, mouseup: doThat }"></button>
|
SFC
- 単一ファイルコンポーネントの略
- vue拡張子で終わり、HTML、CSS と JavaScriptをモジュール化したもの
例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| <script setup>
import { ref } from 'vue'
const greeting = ref('Hello World!')
</script>
<template>
<p class="greeting">{{ greeting }}</p>
</template>
<style>
.greeting {
color: red;
font-weight: bold;
}
</style>
|
Ref, Reactive, Watch, WatchEffect, Computed関数について
Ref, Reactive
ref, reactiveはReactでいうsetStateに近い。
- ref:
- プリミティブ値(文字列、数値、ブール値など)やオブジェクト、配列などのリアクティブな参照を作成
- valueを使って値にアクセス
- 単純なプリミティブ値や単一の変数をリアクティブにしたい場合
- refでオブジェクトを扱う場合は階層が深い部分ではリアクティブにならない
- reactive:
- オブジェクトや配列をリアクティブにするために使用
- 通常のオブジェクトや配列のように直接プロパティにアクセス
- 複数のプロパティを持つオブジェクトや配列をリアクティブにしたい場合
ref:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import { ref } from 'vue';
export default {
setup() {
const message = ref('Hello, Vue 3!');
const updateMessage = () => {
message.value = 'Updated message';
};
return {
message,
updateMessage
};
}
};
|
reactive:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| import { reactive } from 'vue';
export default {
setup() {
const state = reactive({
message: 'Hello, Vue 3!'
});
const updateMessage = () => {
state.message = 'Updated message';
};
return {
state,
updateMessage
};
}
};
|
ただし、ReactはSetStateでSetterを利用して値を変えていたが、Vueはtemplateの中で直接状態のvalueにアクセスしてミュータブルに値を更新できる。
1
2
3
4
5
6
7
8
9
10
| <template>
<p>You clicked {{ count }} times</p>
<button @click="count++">Click me</button>
</template>
<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);
</script>
|
Watch, WatchEffect
watchは値の変更監視で、ReactのuseEffectに近い。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| import { ref, watch } from 'vue';
export default {
setup() {
const message = ref(0);
watch(message, (newValue, oldValue) => {
console.log(`message changed from ${oldValue} to ${newValue}`);
});
return {
message
};
}
};
|
ただし、副作用のクリーンアップはWatchEffectを使う。
React:
1
2
3
4
5
| useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal }).then((resp) => ...)
return () => controller.abort();
}, [url]);
|
Vue:
1
2
3
4
5
6
| <script setup lang="ts">
watchEffect((onCleanUp) => {
const controller = new AbortController();
onCleanUp(() => controller.abort());
fetch(url.value, {signal: controller.signal}).then(resp => ...)
});
|
Computed
ReactのHooksとComposition APIには次の違いがある。
- Hooks
- 状態をイミュータブルに保つことを強制
- 専用のsetter関数を使う
- Composition API
- 値をrefやreactiveで覆うことで状態をミュータブルに更新するこができる
- weakMapやProxyを用いることでライブラリ側で効率的に依存関係を更新できる
故に、一見正しそうな、以下のvueのコードは間違い。inputlengthは入力しても更新はされない。
- Reactでは状態が更新されるたびに(memo化していなければ)コンポーネントの再生成が行われる
- 他方、Vueはsetupは一度のみ呼ばれるので、状態はイミュータブルではない
- そのため、状態の変化を反映するにはriggerEffectを発火させなければならない
- それが
computed
となる - 下記コードはsetterの部分を次にすると、うまく動く
const inputLength = computed(() => input.value.length);
1
2
3
4
5
6
7
8
9
10
11
12
| <template>
<div>
<input type="text" v-model="input" />
<div>{{ inputLength }}</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const input = ref("");
const inputLength = input.value.length;
</script>
|
スロット
slotとは親となるコンポーネント側から、子のコンポーネントのテンプレートの一部を差し込む機能。
例:
次のように、<navigation-link>
の定義にslotを定義する。
1
2
3
4
5
6
7
| <a
v-bind:href="url"
class="nav-link"
>
<font-awesome-icon name="user"></font-awesome-icon>
<slot></slot>
</a>
|
その後次のように使用する。
1
2
3
| <navigation-link url="/profile">
Your Profile
</navigation-link>
|
すると、innner contentsがslotと置き換わる仕組み。
1
2
3
4
| <navigation-link url="/profile">
<font-awesome-icon name="user"></font-awesome-icon>
Your Profile
</navigation-link>
|
糖衣構文
setup
vueの歴史的には、methodsとdataでtemplateに渡す値を決めていた。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| <script>
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
},
mounted() {
console.log(this.count) // 0
}
}
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
|
それが、vue3からはsetup関数でまとめるようになった。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| <script>
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
// テンプレートや他の Options API フックを公開
return {
count
}
},
mounted() {
console.log(this.count) // 0
}
}
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
|
そのsetupがグルーバルスコープになったのが、script setupというシンタックスシュガー。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| <script setup>
import { ref, onMounted } from 'vue'
const count = ref(0)
const increment = () => {
count.value++
}
onMounted(() => {
console.log(count.value) // 0
})
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
|
scoped
scopedをつけると、そのSFCのみでstyleが適用される。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| <template>
<div class="container">
<button @click="increment">{{ count }}</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => {
count.value++
}
</script>
<style scoped>
.container {
text-align: center;
}
button {
background-color: blue;
color: white;
border: none;
padding: 10px;
cursor: pointer;
}
button:hover {
background-color: darkblue;
}
</style>
|
その他
Proxyオブジェクトとは
- オブジェクトの基本操作(プロパティの設定や、値の列挙)を拡張してくれるオブジェクト
- 関数フッキング、またはAOPの一種
1
2
3
4
5
6
7
8
9
| const animal = { neko:'にゃん', inu:'わん' };
var proxy = new Proxy(animal , {
get(target, prop) {
return prop in target ? target[prop] : 'そんなのいないよ';
}
});
console.log(proxy.inu); //わん
console.log(proxy.helicopter); //そんなのいないよ
|
MapとWeakMapの違い
- WeakMap
- キーはオブジェクトのみ
- キーは弱参照で保持されるため、他に参照がなくなるとガベージコレクションされる
- sizeプロパティは存在しない
- Map
- キーは任意の値(オブジェクト、プリミティブ)を使用できる
- キーは強参照で保持される
- sizeプロパティがあり、現在のエントリ数を取得できる
Map:
1
2
3
4
5
6
7
8
9
10
11
12
| > a = new Map();
Map {}
> b = { hello: 11 };
{ hello: 11 }
> a.set(b, 1);
Map { { hello: 11 } => 1 }
> a.get(b)
1
> b = null;
null
> a.size
1
|
WeakMap:
1
2
3
4
5
6
7
8
9
10
11
12
| > a = new WeakMap();
WeakMap { <items unknown> }
> b = { hello: 11 };
{ hello: 11 }
> a.set(b, 1);
WeakMap { <items unknown> }
> a.get(b)
1
> b = null;
null
// この時点でaのbキーの値が削除されている
// また、weakmapにはsizeメソッドがない
|
WeakMapの使い道
例
- 次のように、Elementを受け取り処理を行いそれを監視する場合
- もし、SPAなどの場合はElementが何度も生成されてしまうため、消されたElementのゴミがMapに貯まる
- 故に、DOMエレメントのdeleteイベントにhookして、map変数からデータを削除する必要がある
- もし、完全にmapから不要なelのキーを削除しないと、メモリーリークとなってしまう
- weakmapを利用したら、keyがGCされたら、weakmapのKVも削除されるのでシンプルになる
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // NOTE: マップキーが文字列ではなく、オブジェクト
let map = new WeakMap();
// 複数回呼ばれる
function execute(elKey){
doSomethingWith(elKey);
// 既存の回数を取得
const called = (map.get(elKey) || 0) + 1;
map.set(elKey, called); // 設定したが、GCにより自動的に削除される
// 10回目で通知
if(called % 10 === 0) {
report(elKey);
}
}
|
参考文献