qnote Advent Calendar 2019 の16日目です。
はじめに
こんにちは。今日も元気に npm run
してますか?
Vue.js
、いいですよね、ドキュメントも豊富で簡単でとっても便利。
しかしフレームワークとして簡単に使えてしまうあまり、 Vue
の中身を気にすることはあまりないのではないでしょうか。
今日はそんな Vue
の中身を覗いて、その謎を少しだけ解明してみることにしましょう。
取り上げるのは、 Vue
インスタンスに渡されるオプションオブジェクトの行方です。
オプションオブジェクトの行方
オプションオブジェクトは大まかに、下記の流れで各オプションとして機能するように定義されていきます。
-
new Vue()
に渡される -
initMixin()
でvm.$options
が定義される - 各
init...()
メソッドでリアクティブシステムへの追加などが行われる - 我々の手に届く
ではオプションオブジェクトの長く険しい道のりを、一緒に追っていきましょう。
vm.$options が定義されるまで
new Vue()
全ての始まり、コンストラクタ関数 Vue()
。
ここにオプションオブジェクトを渡すことで、 Vue
インスタンスが生成されます。
これが定義されている箇所は vue/src/core/instance/index.js です。
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)
}
this._init()
にオプションを渡していますね。
この中身を追ってみましょう。
initMixin()
this._init()
は vue/src/core/instance/init.js で定義されています。
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
...
ここで気になるコメントがありました。
/* istanbul ignore if */
イスタンブール?
調べてみたら、テストのカバレッジを調べてくれるツールのようでした。
イスタンブールといえば、飛んでイスタンブールしか思い浮かばなかったのですが、新たな知識を得ることができました。
話がそれましたが、オプションは mergeOptions()
に渡されているようですね。
mergeOptions()
mergeOptions()
は vue/src/core/util/options.js で定義されています。
/**
* Merge two option objects into a new one.
* Core utility used in both instantiation and inheritance.
*/
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
mergeOptions()
には3つの引数が渡されています。
parent
として渡される resolveConstructorOptions(vm.constructor)
はよくわからなかったので説明を省略いたします。
オプションオブジェクトは child
として第2引数に渡されていますね。
第3引数には自身である Vue
インスタンスが vm
として渡されています。
checkComponents(child)
ここでは components
オプションの値をチェックし、変な名前が使用されていないか、などをチェックしています。
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
この3つの関数はそれぞれ props
, inject
, directives
オプションの内容の解析を行なっています。
次に、child
がもつ extend
や mixin
を考慮した処理が行われています。
mixin
の数だけ mergeOptions
を繰り返し、定義していることがわかります。
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
その後オプションは一旦、空のオブジェクトとして定義され、 mergeField()
で parent
と child
のオプションがマージされ、プロパティ毎の結果がオプションオブジェクトに格納されていきます。
最後にマージされたオプションオブジェクトが return
され、 vm.$options
に入るわけですね。
data
オプションがリアクティブシステムに追加されるまで
全部のオプションの行方を追うのは大変なので、今回は data
がリアクティブシステムに追加されるまでに焦点を当ててみましょう。
再び initMixin()
に戻ります。
vm.$options
が定義されたのち、 様々な init...()
を経て、 initState(vm)
にインスタンスが渡っています。
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
...
initState()
initState()
は vue/src/core/instance/state.js に定義されています。
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
メソッドの名前から、 initProps()
では props
を、 initMethods()
では methods
を定義していることがわかります。
読みやすいですね。
では、 initData()
の中身を見ていきましょう。
initData()
initData()
は initState()
と同じ vue/src/core/instance/state.js に定義されています。
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
data
オブジェクトの key
の数だけ while
で回して、 props
や methods
ですでに定義されている名前でないかをチェックしています。
キーの名前は methods
や props
が優先ということですね。
そして isReserved(key)
でキー名が _
または $
から始まっていないことをチェックして、 proxy()
に渡しています。
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
これで data
のプロパティに、 vm
インスタンスから代理アクセスできるようになります。
_
または $
から始まるプロパティには、公式リファレンスにもある通り、 vm.$data.{_または$から始まる名前}
としてのみアクセスできます。
インスタンスからの代理アクセスができないのはこういうわけだったんですね。
さていよいよ大詰めです。 observe()
の中身を見ていきましょう。
observe()
observe()
は vue/src/core/observer/index.js に定義されています。
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
new Observer()
で Observer
インスタンスを作成しています。
/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
...
data
はオブジェクトなので walk()
に渡り、 defineReactive(obj, keys[i])
に渡されています。
defineReactive()
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
ここで Object.defineProperty()
を使用しています。
この仕組みを利用して、リアクティブシステムを可能にしているわけですね。
この Object.defineProperty
が使用できない関係上、 Vue
は IE8 以下をサポートしていないらしいです。
さいごに
以上がオプションオブジェクト、というか data
がリアクティブシステムに登録されるまでの流れでした。
お疲れ様でした。
Vue
の中身ってそういえば気にしたことなかったな、と思い読んでみたのですが、なんとなく使っていたリアクティブシステムの仕組みを知ることができてよかったです。
頭のいい人たちが書いたコードだけあって、とても読みやすくて勉強になりました。
ただ読んだ本人(私)があまり頭がよくないので、間違って理解して書いている可能性もあります。
もし間違っている箇所があればご指摘くださると大変ありがたいです。
最後になりましたが、ここまでお読みいただきありがとうございました!
参考にさせていただいたページ
https://itnext.io/a-deep-dive-in-the-vue-js-source-code-4601a3f5584
https://github.com/ohhoney1/Vue.js-Source-Code-line-by-line