巻頭言
最近、Vueに関するトリック記事が大ブームで、私も1つ書きましたが、この記事にあるトリックはVueのドキュメントでたどることができるのに対して、Vueのドキュメントにはまったく載っていないトリックについて語っている記事もあります!なぜでしょう?
ソースコードを読み始めると、実はこれらのトリックと呼ばれるものはソースコードの理解に過ぎないことに気づきました。
ソースコードに隠されたヒント
Vueを使うときはnewキーワードで呼び出すということは、Vueはコンストラクタだということです。つまり、ソースにはVueのコンストラクタが定義されています!
でこのコンストラクタを見つけました。
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
// ... _init メソッドの関数本体は、ここでは省略する
}
}
コンストラクタで行うことはただひとつ。
そして、_init()関数はinitMixin(Vue)で定義されています。
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
// ... _init メソッドの関数本体は、ここでは省略する
}
}
これをメインスレッドとして、その過程でどんな楽しいヒントが得られるか見てみましょう。
課題サブコンポーネントデータのパラメータの分解
公式文書によると、これが一般的なサブコンポーネントデータオプションの書き方です:
props: ['parentData'],
data () {
return {
childData: this.parentData
}
}
でもね、そう書くこともできるんですよ:
data (vm) {
return {
childData: vm.parentData
}
}
// または、分解された割り当てを使用する
data ({ parentData }) {
return {
childData: parentData
}
}
propsの変数は分解された代入によってデータ関数に渡され、これはデータ関数の引数が現在のインスタンス・オブジェクトであることを意味します。
これは、データ関数がcall()メソッドで実行され、現在のインスタンス・オブジェクトへのバインディングが強制されるためです。これはデータ・マージ・フェーズで発生します!
init()関数は、主に一連の初期化を行います。
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
Vueインスタンスに$options属性が追加され、これらの初期化メソッドでは、インスタンスの$options属性が例外なく使用されます。
データのマージは mergeOption で行われます。
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
if (childVal && typeof childVal !== 'function') {
process.env.NODE_ENV !== 'production' && warn(
'The "data" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
)
return parentVal
}
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
}
上のコードは data オプションのマージ戦略関数で、まず vm が存在するかどうかを判断して親子コンポーネントかどうかを決定し、存在すれば親コンポーネントとなります。何があっても、結局はmergeDataOrFn実装の結果を返します。違いは、親コンポーネントを扱うときに、vmが通過することです。
次にmergeDataOrFn関数を見てください。
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
// in a Vue.extend merge, both should be functions
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
// when parentVal & childVal are both present,
// we need to return a function that returns the
// merged result of both functions... no need to
// check if parentVal is a function here because
// it has to be a function to pass previous merges.
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
} else {
return function mergedInstanceDataFn () {
// instance merge
const instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal
const defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
}
関数全体は、vmを判定し、mergeDataOrFnに親子コンポーネントを区別させるif判定分岐文ブロックで構成されています。
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
mergedDataFn関数はmergeData関数を返します。
childVal.call(this, this)
parentVal.call(this, this)
mergeData関数では、親子コンポーネントのデータ・オプション関数が実行されます。最初のthisはデータ関数のスコープを指定し、2番目のthisはデータ関数にデータパラメータを渡すためのものです。thisはデータ関数に渡される引数です。これが、最初に分解された代入を使用できる原理です。
続きを読む
関数が返されたため、mergedDataFn関数はまだ実行されていないことに注意してください。
上記は、サブコンポーネントのデータオプションを処理するときに行われるもので、サブコンポーネントオプションを処理するときに返されるものは、常に関数であることに気づくことができます。
サブコンポーネントオプションの処理について述べたところで、サブコンポーネント以外のオプション、つまりnew演算子を使ってインスタンスを生成する場合の処理について見てみましょう。
if (!vm) {
...
} else {
return function mergedInstanceDataFn () {
// instance merge
const instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal
const defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
mergedInstanceDataFn
else 分岐に進むと、関数に直接戻ります。親子コンポーネント・データ・オプション関数も call(vm, vm) メソッドで実行され、現在のインスタンス・オブジェクトに強制的にバインドされます。
const instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal
const defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal
mergedInstanceDataFn
この時点では、関数はまだ実行されていないことに注意してください。したがって、mergeDataFn関数は常に関数を返します。
なぜ関数を返すことをそんなに強調するのですか?つまり、strats.dataは関数として終わるのですか?
これは、関数から返されるデータオブジェクトによって、各コンポーネントインスタンスがデータの一意なコピーを持つことが保証され、コンポーネント間のデータが互いに影響し合うことが回避されるためです。
mergeData(childVal, parentVal)
mergeDataFn は、親コンポーネントと子コンポーネントのデータ・オプションをマージした結果として返されます。これは、props とオブジェクトオプションの初期化がデータオプションより先に行われるためで、props を使用してデータを初期化できることを保証します。
これは、props またはオブジェクトの値をデータオプションで呼び出す唯一の方法です!
ライフサイクルフックは配列として書くことができます。
ライフサイクルフックは配列形式で書くことができます!
created: [
function () {
console.log('first')
},
function () {
console.log('second')
},
function () {
console.log('third')
}
]
これはどのように書けばいいのでしょうか?ライフサイクルフックのマージ処理を見てみましょう!
mergeHook は、ライフサイクルフックをマージするために使用されます。
/**
* Hooks and props are merged as arrays.
*/
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
}
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
Hooks and props are merged as arrays
実際、それは.NETのコメントにあります。
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured'
]
forEach文は、stratsポリシーオブジェクトの個々のライフサイクルフックオプションをマージするための関数を追加します。
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
関数本体は3組の三項演算子で構成され、mergeHook関数で処理された後、コンポーネントオプションのライフサイクルフック関数が1つの配列にマージされます。
最初の三項演算子では、まずchildValがあるかどうか、つまりコンポーネントのオプションがライフサイクルフック関数で書かれているかどうかを判断し、もしそうでなければ直接parentValを返します。関数は実行されません。作成されたライフサイクルフック関数を例にとってみましょう:
new Vue({
created: function () {
console.log('created')
}
})
options.created = [
function () {
console.log('created')
}
]
下の例を見てください:
const Parent = Vue.extend({
created: function () {
console.log('parentVal')
}
})
const Child = new Parent({
created: function () {
console.log('childVal')
}
})
ここで Child は new Parent を使って生成されます:
created: function () {
console.log('childVal')
}
Parent.options.created
Parent.options.created
parentValはもはやVue.options.createdではありません。実際にはVue.extend関数内のmergeOptionsで処理されるので、以下のようになるはずです:
Parent.options.created = [
created: function () {
console.log('parentVal')
}
]
parentVal.concat(childVal)
mergeHook関数の処理後、文中のキー: 、parentVal、childValは配列にマージされます。つまり、最終的な結果は以下のようになります:
[
created: function () {
console.log('parentVal')
},
created: function () {
console.log('childVal')
}
]
また、3番目の三項演算子にも注目してください:
: Array.isArray(childVal)
? childVal
: [childVal]
これは、childValが配列かどうかを判定し、ライフサイクルフックが配列として書けることを示しています。これが冒頭で説明した原則です!
ライフサイクルフックのイベントリスナー
ライフサイクルフックのイベントリスナー」と呼ばれるものをご存知ないでしょうか。と思われるかもしれませんが、実はVueのコンポーネントはこのように書くことができます:
<child
@hook:created="childCreated"
@hook:mounted="childMounted"
/>
callhook(vm, 'created')
初期化では、作成されたライフサイクル関数が関数を使用して実行され、次にcallhook()の実装を見ます:
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
const handlers = vm.$options[hook]
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
try {
handlers[i].call(vm)
} catch (e) {
handleError(e, vm, `${hook} hook`)
}
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
callhook() 関数は2つの引数を取ります:
- インスタンスオブジェクト;
- 呼び出すライフサイクルフックの名前;
まずはライフサイクル機能のキャッシュ:
const handlers = vm.$options[hook]
callHook(vm, created)
もし実行されれば、それは同等です:
const handlers = vm.$options.created
先ほど説明したように、ライフサイクルフックのオプションは最終的にマージされて配列に処理されるので、結果として得られるハンドラはライフサイクルフックの配列になります。次に実行されるのはこのコードです:
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
try {
handlers[i].call(vm)
} catch (e) {
handleError(e, vm, `${hook} hook`)
}
}
}
最後に、callHook関数の最後にあるこのコードに注目してください:
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
vm._hasHookEventはinitEvents関数で定義され、その役割は"ライフサイクル・フック・イベント・リスナー"が存在するかどうかを判断することです。
ライフサイクルフックのイベントリスナーは、冒頭で述べたとおりです:
<child
@hook:created="childCreated"
@hook:mounted="childMounted"
/>
コンポーネントの対応するライフサイクルフックをリッスンするには、hook: にライフサイクルフックの名前を加えてください。
まとめ
1 の場合、サブコンポーネントデータオプション関数はパラメータを持ち、現在のインスタンスオブジェクトです;
2、ライフサイクルフックは、実行するために、配列の形式で記述することができます;
3.ライフサイクルフックのイベントリスナーを使用して、ライフサイクル関数を登録できます。
「ただし、公式文書に書かれていないメソッドは推奨されません」。