Vue学習まとめ

✍🏼 作成日 2016年06月01日   
❗️ 注意:この記事が作成されてから既に 日が経過しています。情報の鮮度にご注意ください

はじめに

この記事は Vue 2.0公式ドキュメント の「インスタンス」セクションから始め、いくつかの Vue API の使用方法や Vue が特定の機能を実装する原理について考察します。さらに、私自身の使用感や浅はかな視点から Vue の設計意図を分析する不遜な試みも含まれています。不適切な点があればご容赦ください。Vue に触れて間もないため、誤りがありましたら指摘していただけると幸いです。あらかじめお礼申し上げます。

注意: 基礎知識は省略し、私が説明が必要と考える点のみを述べます。

Vue インスタンス

すべての Vue.js アプリケーションは、コンストラクタを使って Vue のルートインスタンスを作成することで起動します。つまり、各ページのデータはこの1つのインスタンスによって管理されるべきであり、元データの送受信はすべてルートインスタンスを通じて一元管理され、ルートインスタンスは props でデータを配布したり、events でデータを監視したりします。子コンポーネントは watch/computed でデータの変化を監視し、適時更新すればよいのです。

ドキュメントには「すべての Vue.js コンポーネントは実際には拡張された Vue インスタンスである」と書かれています。この文を正しく理解するならば、コンポーネントでもインスタンスと同じメソッドやライフサイクルフックが使える(ただし data を除く)ということです。

コンポーネントの data は関数でなければなりません。コンポーネントは再利用されるため、毎回コンポーネントを呼び出すたびに新しいデータを生成する必要があるからです。

データプロキシ(proxy)とは、Vue インスタンスがその data オブジェクト内のすべての属性を代理することを指します。一方、インスタンスプロパティ $datadata 属性そのものを表し、プロキシされた data と区別されます。

つまり、vmdata 属性が {a: 'xheldon'} の場合、vm.a'xheldon' となり、vm.$data{a: 'xheldon'} となります。

コンポーネントも実際には(拡張された)Vue インスタンスです。以下は簡単な検証です:

list.vue コンポーネント(templatestyle は省略):

1
2
3
4
5
6
7
8
import Vue from 'vue'
export default{
data(){
console.log(this instanceof Vue);//true
return {}
}
name: 'com-list'
}

props vs data

コンポーネントを初期化する際、prop の属性と data の属性、そして computed のメソッドはすべて Vue インスタンスにバインドされます。しかし、props の属性は data の同名属性よりも優先度が高くなります。以下はその検証です:

1
2
3
4
5
6
7
8
9
10
11
export default {
data() {
console.log('first:', this); //list 的实例
return {
a: 'a', //属性 a 代表的值挂在 data 上, 但是被下面的 prop 属性同名覆盖(查看上面控制台输出的内容即可)
d: 'd',
};
},
props: ['a'], //和 data 中的 a 属性同名, 因此来自父级的数据将 data 中的同名属性 a 上的数据覆盖.(注:父级是根组件, 挂载在一个实例上)
name: 'com-list',
};

結果として警告が表示されます(エラーではなく、レンダリングには影響しません):

1
[Vue warn]: The data property "a" is already declared as a prop. Use prop default value instead.

props VS data

一方、computed で返される関数名と data の属性名は重複可能で、何の警告も出ません。しかし、同名で上書きされた後、初期化時にコンソールで確認すると、_data_computedWatcher より後にこの重複属性の gettersetter を設定しているようです(これが原因かどうかは不明で、詳細に調査した後でこの記事を修正する予定です)。そのため、上書きが発生します。関連する原理は この紹介記事 で確認できます。

これらはすべて初期化時にインスタンスの属性にバインドされ、同名の computed 属性は上書きされますが、Vue devtool には正しく表示されます)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default {
data() {
console.log('first:', this); //list 的实例
return {
ab: 'a', //属性 a 代表的值挂在 data 上, 但是被下面的 prop 属性同名覆盖(查看上面控制台输出的内容即可)
};
},
computed: {
ab() {
return 'computed a'; //方法名跟 data 上的 a 属性同名, 因此console 出来的 this 不会出现它的值, 用花括号输出的时候也是输出的 data 上的同名属性
},
f() {
return 'computed f'; //方法名没有重复的, 因此 console 出来的 this 会有它的同名属性, 且值为 'computed f', 我们提供的 function 被作为该属性的 getter(计算属性默认只有 getter, 可以手动添加 setter)
},
},
name: 'com-list',
};

computed VS data

Vue devtool には正しく表示されます:

computed VS data

もし computed のメソッド名と data の属性名が重複しない場合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default {
data() {
console.log('first:', this); //list 的实例
return {
a: 'a', //属性 a 代表的值挂在 data 上, 但是被下面的 prop 属性同名覆盖(查看上面控制台输出的内容即可)
};
},
computed: {
ab() {
return 'computed a'; //方法名跟 data 上的 a 属性同名, 因此 console 出来的 this 不会出现它的值, 但 Vue devtool 控制台正确显示了出来, 用花括号输出的时候也是其返回值 computed a.
},
f() {
return 'computed f'; //方法名没有重复的, 因此 console 出来的 this 会有它的同名属性, 且值为 'computed f', 我们提供的 function 被作为该属性的 getter
},
},
name: 'com-list',
};

computed VS data

さらに、methodscomputedメソッドの違いは、後者がキャッシュを持つ(つまり、依存するリアクティブデータが変更されない限り再計算されない)こと以外に、両方が補間値を返すことを目的としている場合、methodsのメソッドはfunctionName()として使用され、computedのメソッドはfunctionNameとして参照されます。つまり、前者は関数を実行する必要があり、後者は実行する必要がありません。

その理由は、computedプロパティに記述したメソッドがプロパティとしてVueインスタンスにマウントされ、提供した関数がこのメソッドのgetterとして扱われるためです。

一方、methodsは単なる関数であるため、補間参照であれイベントメソッドであれ、()を使用して呼び出す必要があります。

このため、本当の関数が必要な場合(ここで関数呼び出しを使用する場合、computedは単に値を返すだけでなく、パラメータを渡す必要がある関数を返す必要があります)、methodsが最適です。

以下は、公式サイトとは異なる別バージョンのtodo listです:

テンプレート:

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div>
<input type="text" @keyup.enter="addToList" v-model="todotext" />
<ul>
{% raw %}
<li v-for="(value, key) in todolist">
{{key}}:{{value}}<button @click="deletetodo(key)">X</button>
</li>
{% endraw %}
</ul>
</div>
</template>

ロジック:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export default {
data() {
return {
todolist: [],
todotext: '',
};
},
methods: {
addToList() {
this.todolist.push(this.todotext);
},
deletetodo(key) {
console.log(arguments);
this.todolist.splice(key, 1);
},
} /*,
computed:{
deletetodo(key){//不接收参数
console.log(arguments);
this.todolist.splice(key, 1);
}
}*/,
};

この例では、このようなテンプレートを使用する場合、buttonをクリックしたときに現在のliを削除するためにパラメータを渡す必要があるため、この状況ではmethodsを使用する必要があります。

computedを使用する場合、パラメータを受け入れません(getterであるため)](http://whereswalden.com/2010/08/22/incompatible-es5-change-literal-getter-and-setter-functions-must-now-have-exactly-zero-or-one-arguments)、たとえ関数を返したとしても:

1
2
3
4
5
6
7
8
computed:{
deletetodo(key){
return function(){
console.log(arguments);//控制台输出当前组件实例
this.todolist.splice(key, 1);//报错
}
}
}

注意: 両方に同名の関数が存在する場合、computedの関数がmethodsよりも優先されます(gettersetterの処理順序の問題で、詳細はソースコードを参照してください)。この状況は補間参照とイベントバインディングの両方に適用されます。以下は検証です:

補間参照テスト:

テンプレート:

1
2
3
{%raw%}
<div>{{a()}}</div>
{% endraw%}
1
2
3
4
5
6
7
8
9
10
11
12
methods:{
a(){
return 'im from methods'
}
},
computed:{
a(){
return function(){
return 'im from computed'
}
}
}

関数im from computedが出力されます。computedが関数を返さない場合、補間参照で{{a}}のみを使用すると、明らかにcomputedの値が出力されます。検証はここでは省略します。

イベントバインディングの場合:

呼び出し時にインラインハンドラメソッドを使用:

1
2
3
{%raw%}
<div @click="a()">click me</div>
{% endraw%}

ロジック:

1
2
3
4
5
6
7
8
9
10
11
12
methods:{
a(){
console.log('methods')
}
},
computed:{
a(){
return function(){//computed 返回一个函数
console.log('computed')
}
}
}

computedが出力されます。

メソッドイベントハンドラを使用しても結果は同じです:

1
2
3
{%raw%}
<div @click="a">click me</div>
//注意此处不带括号 {% endraw%}

ロジック:

1
2
3
4
5
6
7
8
9
10
11
12
methods:{
a(){
console.log('methods')
}
},
computed:{
a(){
return function(){//computed 返回一个函数
console.log('computed')
}
}
}

computedが出力されます。

イベントバインディングの場合、computedは常に関数を返す必要があります。

したがって、優先順位は次のとおりです:

props > data > computed > methods

computedmethodsthis.xxx内の参照優先順位も同じであると推測されます。詳細を確認したい場合は検証してください。

また、どちらの場合もmethodは関数を返す必要がありませんが、computedは常に関数を返す必要があり、どちらの場合もパラメータを受け入れません(getterであるため)。以上のことから、イベント処理にはmethodsを使用し、データバインディング/補間処理にはcomputed(キャッシュがあるため)を使用するのが最適です。

さらに、methodでイベントをバインドする場合、()を付けるか付けないかは同じ効果を持ち、どちらも関数を実行します。違いは次のとおりです:

  1. ()付きのものをインラインステートメントと呼び、ネイティブイベントとカスタムイベントの2つのケースに分けられます。どちらの場合も引数を渡すことができ、引数リストが空の場合、デフォルト引数 arguments も空となり、デフォルト引数は存在しません。input/click のようなネイティブイベントによってトリガーされる場合、特別な $event パラメータをネイティブの event イベントハンドラとして渡すことができます。一方、カスタムイベントによってトリガーされる場合、イベントハンドラ関数の引数は実際に渡される値に依存し、カスタム関数には特別な $event オブジェクトは使用できません。$emit でカスタムイベントをトリガーする際に渡される引数は無視されます。

  2. ()なしのものをメソッドイベントと呼び、これも2つのケースに分けられます:ネイティブイベントの場合、ネイティブの event イベントが唯一のデフォルト引数として渡されます。カスタムイベントの場合、$emit イベント時にイベント名以外の第2引数から最後の引数まで任意の数の引数が渡されます。

言葉だけでは不十分、コードを見せてください

上記は4つのケースを指しています:

  1. ()付きのネイティブイベント
1
2
3
4
5
6
<input type="text" @click="test1()">//模板里面传参为空
methods:{
test1(){
console.log(arguments);//则逻辑中的参数也为空;
}
}
  1. ()付きのカスタムイベント
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//子模板
<input type="text" @input="shuru($event)"/>
<div>{{xheldon}}</div>
//子逻辑:
name: 'child',
props:['xheldon'],
method:{
shuru(e){
this.$emit('haha', e.target.value,'其他参数');
}
}
//父模板
<child :xheldon="blob" @haha="something()"></child>
//父逻辑:
methods:{
something(){
console.log('parent:',...arguments);//上面 something() 中传 xxx, 则输出 'parent:xxx', 即忽略了子组件 $emit 事件时候传递的参数.
this.blob = arguments;
}
}
  1. ()なしのネイティブイベント
1
2
3
4
5
6
<input type="text" @click="test1">
methods:{
test1(){
console.log(arguments);//结果: 输出原生的 MouseEvent 事件
}
}
  1. ()なしのカスタムイベント
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//子模板
<input type="text" @input="shuru($event)"/>
<div>{{xheldon}}</div>
//子逻辑:
name: 'child',
props:['xheldon'],
method:{
shuru(e){
this.$emit('haha', e.target.value,'其他参数');
}
}
//父模板
<child :xheldon="blob" @haha="something"></child>
//父逻辑:
methods:{
something(){
console.log('parent:',...arguments);//input 输入 xxx, 则输出 'parent: xxx 其他参数', 即与子组件 $emit 时候传递的参数相关.
this.blob = arguments;
}
}

ディレクティブとパラメータ(属性)

基本的な使い方:

1
<div v-directive:propNamed.modifiers="value"></div>

ここで、directive はディレクティブと呼ばれ、propName はディレクティブの「パラメータ」と呼ばれます。実際、パラメータの具体的な表現は html 上の属性です(Vue にはいくつかの組み込みパラメータ/属性があります。例えば v-bind:click="method"clickv-bind:href="/img/in-post/x.png"href など。前者はインラインに表示されませんが、後者は必須であるためインライン属性に表示されます。一方、自分で定義したパラメータ/属性は必ずインライン属性に表示されます)。value は変数(ダブルクォーテーション内に書かれていますが)で、ほとんどの場合、親テンプレートから来ます。

propName には修飾子を付けることができ、.prevent のようにデフォルトイベントを禁止するなどの動作を簡単に操作できます。

一部のディレクティブは v-if のように直接使用できますが、v-bind:href/v-on:click のようにパラメータを追加する必要があるものもあります。

注意点として、value にダブルクォーテーションを付けるかどうかは結果に影響しません。つまり、:propName="value":propName=value は同じです。特に断りのない限り、以下のすべてのケースに適用されます。

value がブール値に変換された後に false の場合、propName は削除されます。true の場合、propName が表示されます。実際、これは以下のルールに従います(カスタム属性に限ります):

  1. nullundefinedfalse のリテラルの場合、propName 属性は削除されます。
  2. value が未定義の変数の場合、例えば :propName="wxd" の場合、propName は削除され、警告が表示されます:
1
[Vue warn]: Property or method "s" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
  1. value が配列の場合、配列はオブジェクトであるため、propName は以下の2番目のケースを除いて常に存在します。value は以下のケースに分けられます:

  2. :propName =[]:propName ="[]":propName ="['']" の場合、value は削除され、属性のみが残ります。

  3. :propName =[""] の場合、構造が崩れます。

  4. :propName =["",""] または :propName ='["",""]' の場合、value の値は "," となります。

  5. value がネストされた配列の場合、その値が一次元化された後、value またはその再帰的な子要素に undefined または null リテラル(文字列内に記述されたものではない)が含まれている場合、その値は空になります。value またはその再帰的な子要素に Object が含まれている場合、その値は [object Object] になります。

  6. value がオブジェクトの場合、propName は保持され、値は [object Object] になります。

  7. value が数値または文字列の場合(例: :propName = "'fff'")、propName は保持され、value は文字列または数値の値になります。

ソースコードを確認したところ、実際にこのようなロジックになっています:

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
function setAttr(el, key, value) {
if (isBooleanAttr(key)) {
// set attribute for blank value
// e.g. <option disabled>Select one</option>
if (isFalsyAttrValue(value)) {
el.removeAttribute(key);
} else {
el.setAttribute(key, key);
}
} else if (isEnumeratedAttr(key)) {
el.setAttribute(
key,
isFalsyAttrValue(value) || value === 'false' ? 'false' : 'true'
);
} else if (isXlink(key)) {
if (isFalsyAttrValue(value)) {
el.removeAttributeNS(xlinkNS, getXlinkProp(key));
} else {
el.setAttributeNS(xlinkNS, key, value);
}
} else {
if (isFalsyAttrValue(value)) {
el.removeAttribute(key);
} else {
el.setAttribute(key, value);
}
}
}

これらのルールはカスタム属性に限定されます。組み込み属性の場合は異なります。例えば class 属性をバインドする場合:

1
v-bind:class="{active: isActive}"

これは、isActive の値が false またはブール値 false に変換可能な他の値の場合、active というクラス名が適用されず、それ以外の場合はクラス名が適用されることを意味します(if文の真偽判定と同じです)。

フィルター

フィルターを連結する場合、最初のフィルターのパラメータは初期値であり、以降のフィルターの最初のパラメータは前のフィルターの戻り値になります。戻り値がない場合は undefined になります。

テンプレート:

1
2
3
<div v-bind:prop="rawProp | filterOne | filterTwo">
控制台查看 filter 函数的参数
</div>

ロジック:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default {
data() {
return {
rawProp: 'this is raw prop',
};
},
filters: {
filterOne() {
console.log(arguments);
return 'this param is pass to next filter';
},
filterTwo() {
console.log(arguments);
},
},
name: 'com-header',
};

最初の filter を除き、後続の filter は初期値を取得できません。ただし、パラメータを渡したい場合、最初の filter が配列を返すなど、方法はいくらでもあります。

リストレンダリング

v-for でパラメータが2つの場合、ネイティブ jsforEach パラメータと一致し、value, key の順になります。of 演算子と in 演算子の効果は完全に同じです(ネイティブ js では異なりますが)。

もう一つ注意が必要な点は、コンポーネントで v-for を使用する場合、親からコンポーネントにデータが自動的に渡されないことです。コンポーネントには独自のスコープがあるためです。そのため、子コンポーネントにデータを渡すには、props プロパティを使用して少し手間をかける必要があります:

1
2
3
4
5
6
<my-component
v-for="(item, index) in items"
v-bind:item="item"
v-bind:index="index"
>
</my-component>

また、v-for をオブジェクトに使用する場合、反復値はオブジェクトの値であり、キーではありません。これはネイティブ js とは異なります。ネイティブ jsfor in ループで値を出力するには、手動で obj['i'] を反復処理する必要があり、キーを出力するには2番目のパラメータ (value, key) を記述する必要があります:

1
2
3
4
5
6
obj:{
first: 'xheldon',
last: 'cao',
age: '25'
}
<div v-for="(value, key) in obj">{% raw %}{{value}}-{{key}}{% endraw %}</div>// 输出 xheldon cao 25

注意:ネイティブ js では、Symbol.iterator を手動で実装しない限り、for of ループを使用できませんが、Vue では可能です(ただし効果は for in と完全に同じです)。

リストレンダリングには「就地复用原则」(その場での再利用原則)という小さな tips があります。これはどういう意味でしょうか? 前述の tololist の例で説明すると、各要素に一意の key 値を指定しない場合(以下のように):

1
2
3
4
5
6
7
<li v-for="(value, key) in todolist">
{%raw%}{{value.key}}:{{value.value}}{%endraw%}<button
@click="deletetodo(key)"
>
X
</button>
</li>

この場合、×ボタンをクリックして現在の li を削除するたびに、Vue はその場で現在の要素を再利用し、データを正しい位置に直接移動します。remove でDOM要素を削除せず、reflow を回避します。以下は、×ボタンをクリックして要素を削除するときのページの render 状況を chromedevtool で表示したものです:

没有key

reflow の部分は最下部のわずかな部分だけであることがわかります。

一方、key を追加すると:

1
2
3
4
5
6
7
<li v-for="(value, key) in todolist" :key="value.key">
{% raw %}{{value.key}}:{{value.value}}{% endraw%}<button
@click="deletetodo(key)"
>
X
</button>
</li>

「×」をクリックした後のブラウザの render の状況を見てみましょう:

有key

ここで疑問に思うかもしれません。なぜこの部分で Vue が提供する (value, key)key を使わず、自分で value 上に value.key を実装する必要があるのでしょうか?:

1
2
3
4
5
{% raw %}
<li v-for="(value, key) in todolist" :key="key">
{{value.key}}:{{value.value}}<button @click="deletetodo(key)">X</button>
</li>
{% endraw %}

答えは、Vue が提供する key は一見 key のように見えますが、実際には現在のデータとは無関係なものです。そのため、li を削除する際に、key は単に再計算されるだけで、削除された要素や残された要素に伴って移動したり削除されたりしません。上記の書き方を使用すると、効果は最初の key なしの場合と同じで、依然として就地再利用戦略が使われ、変更されるのはデータだけで、dom 構造は変わりません。したがって、自分で key を実装する必要があります。おおよそ以下のようになります:

1
2
3
4
5
6
7
8
9
10
11
12
data(){
return{
toolist:[],
truekey: 0,//手动实现一个 key
todotext: ''
}
},
methods: {
addTolist(){
this.todolist.push({value:this.todotext, key: ++this.truekey});// 把 key 放到 data 上
}
}

イベントハンドラ

イベントハンドラはチェーンできますが、一部の要素はそもそもサポートしていないため、バインドしても意味がありません。例えば divkeyup イベントをバインドする場合:

1
<div @keyup.alt="pressalt">div alt 按键测试</div>

そのため、一般的には div でイベントをバブリング処理し、inputalt+ctrl イベントをバインドします:

1
2
3
<div @keyup.alt="presskey">div冒泡按键测试
<input type="text">
</div>

ここで私が懸念しているのは、input@keyup.space のリスナーを使用している場合、中国語入力法ではスペースは通常単語を選択するために使われるため、実際に出力されるのはまだ選択されていないピンイン文字なのか、それともスペースを押した後の候補リストの最初の単語なのかということです(これは Vue の問題ではないかもしれませんが、ここで提起しておきます)。

答えは、ほとんどの場合、スペースを押した後の最初の単語が出力されます。ただし、一文が長い場合、2回スペースを押して文を出力する必要がある場合、最初に押した時には何も出力されず、空になり、2回目にスペースを押した時に初めてすべての語彙が出力されます。これはテストの極端なケースを想定しているため、基本的にはスペースを押した後の最初の単語が出力され、空白やピンイン文字ではないと考えてよいでしょう。私は搜狗 mac 入力法の単行候補モードを使用しており、上記の todolist でテストできます(省略)。

注: 公式ドキュメントで v-model について説明する際に IME について触れていますが、これがまさにこの問題です。入力法を使用している際に v-model も即座に反応させたい場合は、input イベントをバインドすることができます。

フォームコントロール

v-model は一般的に input に使用され、テンプレート内の inputvalue 値は無視されます。v-modeljs 内の初期値のみを認識し、それにバインドします。そのため、v-model を記述し、さらに value 属性も記述した場合、後者は dom 構造には現れますが、js がその値を取得する際には無視され、v-model がバインドした値が取得されます:

1
2
3
4
<input
type="text"
value="我出现在 dom 结构的 value 属性中, 但是只能通过 getAttribute 获取到我, .value 并不能获取到, 伤心~"
/>
1
2
3
4
5
data(){
return{
todotext:'我是真正的初始值, js 获取的是我, 虽然我并不出现在 dom 中的 value 属性中~',
}
}

両者の違いは、jQueryにおける.data.attrの違いに似ています------インラインで記述されたものはattr('data','xxx')の値であり、DOM構造を確認してもxxxの値が表示されますが、jsで実際に取得される値はjsでバインドされた.data('yyy')の値です------もちろん、attrを使用してDOM構造を読み取る場合を除きます。(注:インスタンス化後にattrの値を変更すると、jsで取得されるのはattrの値になります。ここでの初期値の無視は、あくまで初期値のみを無視するものです。例を挙げると、初期化後のv-modelvalueにバインドされた後、手動でDOM構造のvalue値を変更すると、v-modelがその要素のvalue値を取得する際には、変更後のattr属性値が使用され、data上の値ではありません)。

もし要件が特殊で、v-modelを使用せずにinputの値を取得してリアルタイムで更新したくない場合、またはinputの値を処理してから更新したい場合、$refを試してみてください(e.target.valueも使用可能です):

テンプレート:

1
<input @input="input" ref="inputvalue" />

ロジック:

1
2
3
4
5
6
7
8
data(){
message: ''
},
methods:{
input(){
this.message = this.$refs.inputvalue.value;
}
}

公式ドキュメントにも記載されているように、v-modelは双方向データバインディングを実現するためのシンタックスシュガーに過ぎません:

1
<input v-bind:value="something" v-on:input="something = $event.target.value" />

ただし、inputイベントを監視すると、入力法を使用している際にスペースを押して単語を選択する前にも入力イベントがトリガーされるため、この要件がない場合は、素直にv-modelを使用するのが良いでしょう。

複数の要素に同じ値をバインドして出力する必要がある場合、一般的な要件は一連のcheckboxです。この場合、配列を使用する必要があります:

1
2
3
<input v-model="messages" type="checkbox" value="xheldon" />
<input v-model="messages" type="checkbox" value="xiaodan" />
<p>{{messages}}</p>
1
2
3
4
5
data(){
return{
messages: []
}
}

(私が発見した限りでは)このように簡潔な配列の使用方法は、複数のcheckboxタイプのinputに対してのみ有効です------つまり、複数の要素が同じv-modelにバインドされていても、これらの要素の状態は同期されず、対応するvalueが配列に格納されます。もちろん、methodsを使用して任意の入力タイプの要素で同様の効果を実現できると強く主張する場合は------それについては触れません。

単一のcheckboxの場合、v-modelvalue値にバインドされ、trueまたはfalseになります。:true-value:false-valueを使用して、選択時の値と非選択時の値をカスタマイズできます。

一方、複数のラジオボタンradioの場合、v-modelnameと同様の役割を果たします------つまりグループ化に使用されるため、radioタイプのinputv-modelを使用する場合、name属性を記述する必要はありません。

selectタイプの場合、各optionvalue属性が指定されていない場合、バインドされるのはoption内の値です。指定されている場合はvalue属性の値になります。selectタイプの複数選択ボックスでv-modelにバインドするdataは配列型でなければなりません。そうでない場合、警告が表示されます(エラーにはなりませんが、Vueが自動的に変換し、正常に動作します):

1
Vue warn]: <select multiple v-model="selectM"> expects an Array value for its binding, but got String

注意:上記のすべてのタイプでv-modelvalueをバインドする際に、value属性が動的(:value="xxx")にdata上の他の属性(xxx)にバインドされている場合、v-modelに対応する属性と:valueに対応する属性は同一(厳密に等しい)です。

コンポーネント

まず区別すべきは、DOMテンプレートと文字列テンプレートの違いです。

HTMLテンプレートとは、通常のhtml要素を指し、これらの要素はVueインスタンスのelオプションによってバインドされます:
文字列テンプレートの部分:

1
2
3
4
5
6
7
8
9
10
<div id='tpl'>
{' '}
实例挂载元素 html 自有组件:
<ul>
<li></li>
<li></li>
</ul>
自定义组件:
<my-component></my-component>
</div>

文字列テンプレートとは:

  1. js内でtemplateによって登録されるテンプレート、例えば:
1
2
3
4
5
6
Vue.component('my-component', {
template: '<span>{{message}}</span>',
data() {
message: 'hello, xheldon';
},
});

または:

1
2
3
4
5
6
7
8
var Child = {
template: '<div>'hello, xheldon'</div>'
}
new Vue({
components: {
'my-component': Child
}
})
  1. <script type="text/x-tempalge"></script>によって登録されるテンプレート(Handlebarと同じ)

  2. .vueコンポーネント内の<template>タグの内容。

Vueはブラウザが解析を完了した後にDOMテンプレートの解析を開始するため、DOMテンプレートは特定の子要素を必要とするタグではコンポーネントを使用できません。例えば、selectタグの子要素はoptionでなければならず、カスタムタグcom-optionは認識されません。そのため、is="component-name"属性を追加して、そのタグが使用するテンプレート名を指定します。

リテラル構文 VS 動的構文

ここで注意すべき問題は、ネイティブのjsではオブジェクトのプロパティに数字を使用できますが、それは文字列として扱われます(ES5のみ、ES6ではオブジェクトのプロパティは任意の値にできますが、これ以降の説明と矛盾しません)。しかし、Vueでは、dataプロパティに数字を属性として使用できません。リテラル構文の場合、数字を渡すと先にtoString処理されますが、動的構文の場合は直接数字として処理され、dataにバインドされたプロパティ値は検索されません:

子コンポーネントテンプレート:

1
2
<div>{%raw%}{{dynamic}}{%endraw%}</div>
<button @click="alertProp">点我看上面 props 传参的类型</button>

子コンポーネントロジック:

1
2
3
4
5
6
props:['dynamic'],
methods:{
alertProp(){
alert(typeof this.dynamic)//上面说过了, props 属性也是绑定到 Vue 实例上的, 因此可以直接使用 this
}
}

親コンポーネント-リテラル構文:

1
<com-child dynamic="11"></com-todolist>

親コンポーネント-動的構文:

1
<com-child :dynamic="11"></com-todolist>

上記の両方の構文における親コンポーネントのロジックは同じ:

1
2
3
4
5
data(){
return {
11:'属性-动态语法'
}
}

結果として、親コンポーネントのリテラル構文を使用した場合、buttonをクリックすると子コンポーネントに1が渡され、ドキュメントにもある通り、文字列の1であるため、alertで表示されるのはstringです。一方、親コンポーネントの動的構文でv-bindを使用して親コンポーネントのdata1プロパティをバインドしても、子コンポーネントは1プロパティに対応する属性-動的構文の値を受け取らず、依然として1という値であるため、buttonをクリックしたときにalertで表示されるのはnumberです。

結論:dataオブジェクトの属性として数字を使用しない方が良いです。

注意:子コンポーネントに渡される属性が配列またはオブジェクトの場合、子コンポーネントでこの属性値を変更すると、親コンポーネントに反映されます------これは通常すべきではありません。なぜなら、よく言われるように:props down, events up(例は省略)、ベストプラクティスは親コンポーネントから渡される参照型のディープコピーを使用することです------ただし、子コンポーネントが親コンポーネントの状態に影響を与える必要がある場合は、幸運を祈ります。

events upの際、子コンポーネントが$emitするときにイベント名以外のパラメータを渡すと、これらのパラメータは親コンポーネントのイベントリスナー関数に渡されます:

子コンポーネントテンプレート:

1
2
<input type="text" @input="someEventHandlerForOriginalEventLikeClickOrInput($event)"/>
<span>{{data}}</span>

子コンポーネントロジック:

1
2
3
4
5
6
props:['data'],
methods:{
someEventHandlerForOriginalEventLikeClickOrInput(e){
this.$emit('eventPasstoParent', e.target.value, 'someOtherArgus');
}
}

親テンプレート:

1
2
3
4
<com-todolist
:data="someParentData"
@eventPasstoParent="someParentEventHandler"
></com-todolist>

親ロジック:

1
2
3
4
5
6
someParentData: ''
methods:{
someParentEventHandler(){
this.someParentData = [...arguments];
}
}

非同期更新キュー

Vueがあれば、もはやjQueryは必要ありません。フレームワークの最大の利点は、同じ操作に対して繰り返しコードを書く必要がなくなることです。双方向データバインディングは多くのDOM操作問題を解決してくれますが、場合によってはjQueryがより優れていることもあります。例えばDOMを操作する際、jQueryはコールバック関数として関数を受け取り、アニメーションの実行完了時にトリガーされます。一方、私たちが使用する双方向データバインディングでは、データを設定した後、DOMがいつ更新されたかをどうやって知るのでしょうか?答えはjQueryと同じで、非同期更新キューです。

1
2
3
4
5
6
7
8
9
10
11
var vm = new Vue({
el: '#example',
data: {
message: '123',
},
});
vm.message = 'new message'; // 更改数据
vm.$el.textContent === 'new message'; // false
Vue.nextTick(function () {
vm.$el.textContent === 'new message'; // true
});

この書き方は個人的にはお勧めしません。なぜなら、最良のロジックはインスタンスのプロパティ/メソッド内に記述すべきで、インスタンスの外側に書くべきではないと考えるからです。幸いVueはこれを実現する方法を提供してくれています:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Vue.component('example', {
template: '<span>{{ message }}</span>',
data: function () {
return {
message: 'not updated',
};
},
methods: {
updateMessage: function () {
this.message = 'updated';
console.log(this.$el.textContent); // => '没有更新'
this.$nextTick(function () {
console.log(this.$el.textContent); // => '更新完成'
});
},
},
});

アニメーション

アニメーションについては特に言うことはありませんが、JavaScriptフック関数の中の2つのフックについて説明が必要です。それはenterCanceledleaveCancelledです。enterCancelledv-ifv-showの両方で使用され、どちらでもトリガーされる可能性があります。トリガーのタイミングは、enterイベントが発生した後、アニメーションがまだ完了していない過程で、他のアニメーションを実行する必要があるときです。一方、leaveCancelledv-showでのみ使用され、v-ifで使用した場合は無効で、決してトリガーされません。そのトリガーのタイミングは、離脱アニメーション(つまりxxx-leave-activeアニメーション)が再生中でまだ完了していないときに、他のアニメーションを実行したときです。

テストコード:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<button type="button" @click="show=!show">点击我切换状态</button>
<transition
name="go"
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@enter-cancelled="enterCancelled"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@leave-cancelled="leaveCancelled"
>
此处使用 v-show, 修改成 v-if 的时候发现, leave-cancelled 不会触发.
<p v-show="show">点击展示我, 再点击一下隐藏我.</p>
</transition>

ロジック:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
data(){
return{
show: true
}
},
methods: {
beforeEnter(){alert(1);},
enter(){ alert(2);},
afterEnter(){ alert(3);},
enterCancelled(){ alert(4);},
beforeLeave(){ alert(5);},
leave(){alert(6);},
afterLeave(){ alert(7);},
leaveCancelled(){ alert(8);}
},

スタイル:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.button-animate button {
margin-left: 20px;
margin-top: 40px;
transition: all 1s ease;
position: absolute;
}
.go-enter-active {
transition: all 5s ease;
}
.go-leave-active {
transition: all 5s cubic-bezier(1, 0.5, 0.8, 1);
}
.go-enter,
.go-leave-active {
transform: translateX(10px);
opacity: 0;
}

フックのパラメータは、enterleaveel要素自体(ネイティブのElement型要素)とdoneコールバック関数であるのに対し、他のフックのパラメータはすべてel要素自体です。

要素のトランジションでは、各要素にkeyを追加するのが最善です。なぜなら、前述の就地再利用戦略により、切り替え時にデータを直接置き換えてしまい、アニメーション効果が得られない可能性があるからです。

公式ドキュメントで全く説明されていない点の1つは、Vue transitionのアニメーションクラス名cssの書き方には順序制限があることです。v-enterv-leavev-enter-activev-leave-activeの後ろに書かなければならず、そうでないと無効になります。例えば、ボタンをクリックしたときのフェードイン/フェードアウト効果を作りたい場合、ボタンをクリックすると左から右へボタンがフェードインし、同時に現在クリックしたボタンが左から右へフェードアウトするようにしたいとします:
ロジック:

1
2
3
4
5
6
7
8
data:{
isShow: true
},
methods:{
animakkey(){
this.isShow = !this.isShow;
}
}

構造:

1
2
3
4
5
6
<div class="button-animate">
<transition name="go">
<button key="a" v-if="isShow" @click="animakkey">on</button>
<button key="b" v-else @click="animakkey">off</button>
</transition>
</div>

もしあなたのスタイルがこうなっている場合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.button-animate button {
margin-left: 20px;
margin-top: 40px;
transition: all 1s ease;
position: absolute;
}
.go-enter {
opacity: 0;
transform: translateX(-25px);
}
.go-leave {
opacity: 1;
transform: translateX(25px);
}
.go-enter-active {
opacity: 1;
transform: translateX(0px);
}

.go-leave-active {
opacity: 0;
transform: translateX(50px);
}

アニメーション効果が期待通りでないことに気付くでしょう:

animate

しかし、enterenter-activeの後ろに移動すると:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.button-animate button {
margin-left: 20px;
margin-top: 40px;
transition: all 1s ease;
position: absolute;
}
.go-enter-active {
opacity: 1;
transform: translateX(0px);
}
.go-enter {
opacity: 0;
transform: translateX(-25px);
}
.go-leave-active {
opacity: 0;
transform: translateX(50px);
}
.go-leave {
opacity: 1;
transform: translateX(25px);
}

完璧に動作します:

animate

これら4つのcssクラス名の可能な順序を比較した後、v-enterv-enter-activeの後ろに置くだけで効果が得られることがわかりました。他のクラス名は任意です。

transitionタグ内にはアニメーションが必要な要素だけを配置でき、他の要素を入れることはできません。もし上記の例の構造がこのようになっている場合:

1
2
3
4
5
6
<transition name="go">
<div class="button-animate">
<button key="a" v-if="isShow" @click="animakkey">on</button>
<button key="b" v-else @click="animakkey">off</button>
</div>
</transition>

アニメーション効果は全く得られません。また、通常のcssクラス名でいくつかのcssプロパティを使用して要素のスタイルを規定し、v-enterのようなアニメーションクラス名で同じプロパティを使用した場合も効果がありません。ドキュメントは「それらの優先度は通常のクラス名よりも高い」と述べていますが、実際にはそうではありません(私の理解が間違っているのでしょうか?ご指摘をお待ちしています)。前の例で、スタイルの中でbuttonの通常のcssプロパティを設定すると:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.button-animate button {
margin-left: 20px;
margin-top: 40px;
transition: all 1s ease;
position: absolute;
transform: translateX(50px);
}
.go-enter-active {
opacity: 1;
transform: translateX(0px);
}
.go-enter {
opacity: 0;
transform: translateX(-25px);
}
.go-leave-active {
opacity: 0;
transform: translateX(50px);
}
.go-leave {
opacity: 1;
transform: translateX(25px);
}

結果:

animate

見ての通り、opacity だけがアニメーションし、transform はアニメーションしません!(皆さんも試してみてください。animate.css を使用する際に、Animate.css と同じプロパティを事前に要素に設定した場合、アニメーション効果がまだあるかどうか、ぜひ issue を提出してください。)

transition-grouptransition と少し異なります。見た目上、transition 自体は単なるラッパーコンテナで、ページ構成には関与しませんが、transition-groupVue によってタグ(デフォルトでは span タグ)に置き換えられます。また、置き換えるタグ名をカスタマイズすることも可能です。

render 関数

render 関数を使用すると、テンプレートを書く代わりになります。その引数 createElement は一般的に h と記述されます。コンポーネントやタグ内の各種バインディングや属性などは、createElement 内で対応する JavaScript の書き方を見つけることができます。もし見つからない場合は、ネイティブの書き方を使用できます。例えば、.stop.prevent などは、直接 event.stopPropagation()event.preventDefault() を使用すればよいです。

その他

ミックスイン(mixin)とは、通常のコンポーネント作成プロセス(つまりコンポーネントのライフサイクル内)で、追加の機能を修正または追加することを指します。

一部の プラグイン は上記の mixin に基づいて書かれています。それ以外のプラグインとしては、Vue.prototype にメソッドを追加したり、config を通じてグローバルなメソッドやプロパティを追加するものがあります。

ルーティング は、componentis 属性を使用して簡単に実装できます。また、render 関数を直接書いて、異なるパスに応じて異なるテンプレートをレンダリングすることも可能です。もちろん、より複雑な場合はサードパーティライブラリが必要になります。

状態管理 については、サンプルを見ると、各状態変更のプロセスを記録するために wrap を追加する必要があるようです。公式が推奨するベストプラクティスは、直接インスタンスプロパティに代入できる場合でも、関数を通じて状態を変更することです。これにより、状態の追跡が可能になります。

ユニットテスト は、通常のユニットテストであり、特に説明する必要はありません。

サーバーサイドレンダリング(Server Side Render

考え方を確認すると、基本的に非常にシンプルです。まず、app.jsVue インスタンスを exports し、次に新しいページテンプレートファイル index.htmlVue.js をインポートし、Vue インスタンスをマウントするメソッド $mount を含む。公式サイトでは app.js もインポートしていますが、必要かどうかは私が検証してから説明します)を作成します。このテンプレートにはインスタンスのマウントポイント(id 属性を持つ空でない要素)が含まれています。その後、サーバー側の server.js でこれらをすべて require し、vue-server-renderer を使用して、app.js から exports された Vue インスタンスをレンダリングし、クライアントに返す際にマウントポイント(id 属性を持つ空でない要素、app.js 内のテンプレートが既に存在するため)を置き換えます。

サーバーサイドレンダリングの結果、上記のマウントポイント(id 属性を持つ空でない要素)に server-rendered="true" 属性が追加されます(ページソースを右クリックして確認すると存在することがわかります。これは js で動的に追加されたものではないことを示しています)。

サーバーサイドもストリーミングレンダリングをサポートしています。まず、htmlをマウントポイント(id属性を持つ空でない要素、例えば<div id="app"></div>)で分割点としてsplitし、abの2つの部分に分けます。先ほど使用したvue-server-rendererapp.jsrenderToStringしていましたが、ストリーミングレンダリングをサポートするためには、renderToStreamという別のメソッドを使用する必要があります。その後、dataイベントを監視し、htmla部分の後ろに追加します。endイベントの後、htmlb部分を連結し、最終的にres.sendで送信します。

後記

帝都所在、cn.vuejs.orgping:

ping

自社のFQ VPSping:

ping

digを実行:

dig

cloudflareのサービスを使用していることが確認できます。国際的なウェブサイトは難しいですね、はは。

- EOF -
この記事の初出: Vue学習まとめ - Xheldon Blog