LoginSignup
20
7

More than 3 years have passed since last update.

new Vue() に渡されたオプションオブジェクトの行方を探るべく、我々は vue/src/core の奥地へと向かった

Last updated at Posted at 2019-12-15

qnote Advent Calendar 2019 の16日目です。

はじめに

こんにちは。今日も元気に npm run してますか?
Vue.js 、いいですよね、ドキュメントも豊富で簡単でとっても便利。
しかしフレームワークとして簡単に使えてしまうあまり、 Vue の中身を気にすることはあまりないのではないでしょうか。
今日はそんな Vue の中身を覗いて、その謎を少しだけ解明してみることにしましょう。
取り上げるのは、 Vue インスタンスに渡されるオプションオブジェクトの行方です。

オプションオブジェクトの行方

オプションオブジェクトは大まかに、下記の流れで各オプションとして機能するように定義されていきます。

  1. new Vue() に渡される
  2. initMixin()vm.$options が定義される
  3. init...() メソッドでリアクティブシステムへの追加などが行われる
  4. 我々の手に届く

ではオプションオブジェクトの長く険しい道のりを、一緒に追っていきましょう。

vm.$options が定義されるまで

new Vue()

全ての始まり、コンストラクタ関数 Vue()
ここにオプションオブジェクトを渡すことで、 Vue インスタンスが生成されます。
これが定義されている箇所は vue/src/core/instance/index.js です。

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 で定義されています。

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 */
イスタンブール? :flag_tr: :thinking: :question:
調べてみたら、テストのカバレッジを調べてくれるツールのようでした
イスタンブールといえば、飛んでイスタンブールしか思い浮かばなかったのですが、新たな知識を得ることができました。

話がそれましたが、オプションは mergeOptions() に渡されているようですね。

mergeOptions()

mergeOptions()vue/src/core/util/options.js で定義されています。

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 がもつ extendmixin を考慮した処理が行われています。
mixin の数だけ mergeOptions を繰り返し、定義していることがわかります。

if (child.mixins) {
  for (let i = 0, l = child.mixins.length; i < l; i++) {
    parent = mergeOptions(parent, child.mixins[i], vm)
  }
}

その後オプションは一旦、空のオブジェクトとして定義され、 mergeField()parentchild のオプションがマージされ、プロパティ毎の結果がオプションオブジェクトに格納されていきます。

最後にマージされたオプションオブジェクトが return され、 vm.$options に入るわけですね。

data オプションがリアクティブシステムに追加されるまで

全部のオプションの行方を追うのは大変なので、今回は data がリアクティブシステムに追加されるまでに焦点を当ててみましょう。
再び initMixin() に戻ります。
vm.$options が定義されたのち、 様々な init...() を経て、 initState(vm) にインスタンスが渡っています。

vue/src/core/instance/init.js
    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 に定義されています。

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 に定義されています。

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 で回して、 propsmethods ですでに定義されている名前でないかをチェックしています。
キーの名前は methodsprops が優先ということですね。

そして isReserved(key) でキー名が _ または $ から始まっていないことをチェックして、 proxy() に渡しています。

vue/src/core/instance/state.js
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 に定義されています。

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 インスタンスを作成しています。

vue/src/core/observer/index.js
/**
 * 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()

vue/src/core/observer/index.js
/**
 * 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

この仕組みを利用して、リアクティブシステムを可能にしているわけですね。
この 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

20
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
7