Vue 2.5データバインド実装ロジック(三)initState

9936 ワード

Vueインスタンスは、確立時に一連の初期化操作を実行しますが、これらの初期化操作では、データバインドに最も関連付けられているのはinitStateです.この中にも言いたいことが多いので、今回の文章は全部書けないかもしれませんが、まずこれを書いてみましょう.
まずはinitStateを見て
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)
  }
}

ここでは主にprops,methods,data,computed,watchを初期化します(これらの属性がまだ分からない場合は、まず公式ドキュメントを見ていくつかの小さな例を書くことをお勧めします).これらのアトリビュートはDomレンダリング時に取得されます.もちろん、データバインドも必要です.
initProps
function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted
  observerState.shouldConvert = isRoot
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      ......
    } else {
      defineReactive(props, key, value)
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  observerState.shouldConvert = true
}

省略したところは開発環境でデバッグ書きやすいように書かれているコードで、Vueソースコードにはかなりの箇所が書かれています.
全体的な論理は次のとおりです.
  • すべてのpropのkeyをoptionsの_propKeys中.
  • 各propについて、そのkeyを_に追加するpropKeysでvalueを取得し、defineReactive関数を実行します.(知らないのは前節を見ることができる)
  • 各propについて、proxy関数を呼び出してVueオブジェクト上にその値の参照を確立する.

  • Propのvalueを取得するときにvalidatePropを呼び出して検証し、検証後の戻り値を取得します.
    export function validateProp (
      key: string,
      propOptions: Object,
      propsData: Object,
      vm?: Component
    ): any {
      const prop = propOptions[key]
      const absent = !hasOwn(propsData, key)
      let value = propsData[key]
      // handle boolean props
      if (isType(Boolean, prop.type)) {
        if (absent && !hasOwn(prop, 'default')) {
          value = false
        } else if (!isType(String, prop.type) && (value === '' || value === hyphenate(key))) {
          value = true
        }
      }
      // check default value
      if (value === undefined) {
        value = getPropDefaultValue(vm, prop, key)
        // since the default value is a fresh copy,
        // make sure to observe it.
        const prevShouldConvert = observerState.shouldConvert
        observerState.shouldConvert = true
        observe(value)
        observerState.shouldConvert = prevShouldConvert
      }
      if (
        process.env.NODE_ENV !== 'production' &&
        // skip validation for weex recycle-list child component props
        !(__WEEX__ && isObject(value) && ('@binding' in value))
      ) {
        assertProp(prop, key, value, vm, absent)
      }
      return value
    }
    

    Prop検証は、開発環境でのみ行われ、レンダリングに影響はなく、警告のみが発行されます.
    ここでの作業は主にpropが値を伝えていないときにpropのデフォルト値(デフォルト値は自分で設定した)を取得し、その値に対してobserveを実行することである.ブールタイプの場合、デフォルト値がない場合はfalseとみなされます.
    開発環境であれば、タイプ検証が行われます.この検証は典型的に構造関数名に基づいてタイプ検証が行われます.この関数名は後で文字列の比較が行われますが、最近は自分で比較的完璧なタイプ検証コンポーネントを書こうと思っていますので、この文章では詳しく説明しないで、問題を逃さないようにしています.
    ここで何度もshouldConvertは値を割り当て、この値のtrue or falseはObserverが確立するかどうかを直接決定します.
    このpropsDataはいつ取得されたのでしょうか.もちろんテンプレートのコンパイル時に取得されます.propについてはまだ言うべきことがたくさんありますが、別の文章を書いて説明するかもしれません.
    initMethod
    methodの初期化は他に比べて簡単です
    function initMethods (vm: Component, methods: Object) {
      const props = vm.$options.props
      for (const key in methods) {
        if (process.env.NODE_ENV !== 'production') {
          if (methods[key] == null) {
            warn(
              `Method "${key}" has an undefined value in the component definition. ` +
              `Did you reference the function correctly?`,
              vm
            )
          }
          if (props && hasOwn(props, key)) {
            warn(
              `Method "${key}" has already been defined as a prop.`,
              vm
            )
          }
          if ((key in vm) && isReserved(key)) {
            warn(
              `Method "${key}" conflicts with an existing Vue instance method. ` +
              `Avoid defining component methods that start with _ or $.`
            )
          }
        }
        vm[key] = methods[key] == null ? noop : bind(methods[key], vm)
      }
    }
    

    主に開発環境で検査する.
  • メソッド名が空であるかどうか
  • メソッド名がpropと競合するかどうか
  • メソッド名が既存のVueインスタンスメソッドと競合するかどうか
  • さらにbindを使用してメソッドの役割ドメインをVueインスタンスオブジェクトにバインドし、Vueインスタンスオブジェクトへの参照を作成します(これは重要です).
    export function bind (fn: Function, ctx: Object): Function {
      function boundFn (a) {
        const l: number = arguments.length
        return l
          ? l > 1
            ? fn.apply(ctx, arguments)
            : fn.call(ctx, a)
          : fn.call(ctx)
      }
      // record original fn length
      boundFn._length = fn.length
      return boundFn
    }
    

    このbindはapplyとcallで書き換えられたbindで、原生のbindよりも速いと言われていますが、実は才学が浅く、なぜか分かりません.
    initData
    前の文章で述べた内容をよく知っていれば、ここは難しくないはずです.
    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:
    ' + '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が取得され、dataが関数であればgetDataが呼び出されて関数の戻り値が取得されます.この中にはまだいくつかの重名の問題を検出しているので、詳しく話したくありません.
    ここで最も重要なのはdataに対してobserve関数を実行してObserverとフック関数を創立することです
    initComputed
    ここでは面倒ですが、属性の計算は値ではなく関数であり、戻り値はいくつかの値に関係します.また、キャッシュの問題にも関連しているので、特別な方法で処理する必要があります.文章が長すぎるのを避けるために、次の記事に載せます.
    initWatch
    ここまで言うと、Watcherについて前に話していなかった問題を補足しなければなりません.まずコードを見て、一歩一歩下に言います.
    function initWatch (vm: Component, watch: Object) {
      for (const key in watch) {
        const handler = watch[key]
        if (Array.isArray(handler)) {
          for (let i = 0; i < handler.length; i++) {
            createWatcher(vm, key, handler[i])
          }
        } else {
          createWatcher(vm, key, handler)
        }
      }
    }
    

    まず、watchプロパティごとにcreateWatcherを実行します(Watcherオブジェクトを作成することも考えてみてください).
    function createWatcher (
      vm: Component,
      keyOrFn: string | Function,
      handler: any,
      options?: Object
    ) {
      if (isPlainObject(handler)) {
        options = handler
        handler = handler.handler
      }
      if (typeof handler === 'string') {
        handler = vm[handler]
      }
      return vm.$watch(keyOrFn, handler, options)
    }
    

    ここでは主に2つのステップの前処理を行い、コード上よく理解され、主にいくつかの解釈を行います.
    第1ステップでは、ユーザが設定したwatchがoptionsオブジェクトである可能性があると理解でき、そうであればoptionsのhandlerをコールバック関数としてとる.(optionsを次のvm.$watchに転送)
    第2のステップでは、watchが以前に定義したmethodである可能性がある場合、この方法をhandlerとして取得する.
    次に$watchメソッドを見てみましょう.このメソッドはstateMixinで定義されています.
    Vue.prototype.$watch = function (
        expOrFn: string | Function,
        cb: any,
        options?: Object
      ): Function {
        const vm: Component = this
        if (isPlainObject(cb)) {
          return createWatcher(vm, expOrFn, cb, options)
        }
        options = options || {}
        options.user = true
        const watcher = new Watcher(vm, expOrFn, cb, options)
        if (options.immediate) {
          cb.call(vm, watcher.value)
        }
        return function unwatchFn () {
          watcher.teardown()
        }
      }
    

    ここでの論理は、cb(前のhandler)がオブジェクトである場合、createWatcherをもう一度実行して処理し、Watcherオブジェクトを確立してリスニングし、optionsのimmediateがtrueである場合、直ちにコールバック関数を実行し、最後にリスニングを停止するための関数数を返す.
    次に、このコールバック関数がいつ実行されたかを見てみましょう.
    run () {
        if (this.active) {
          const value = this.get()
          if (
            value !== this.value ||
            // Deep watchers and watchers on Object/Arrays should fire even
            // when the value is the same, because the value may
            // have mutated.
            isObject(value) ||
            this.deep
          ) {
            // set new value
            const oldValue = this.value
            this.value = value
            if (this.user) {
              try {
                this.cb.call(this.vm, value, oldValue)
              } catch (e) {
                handleError(e, this.vm, `callback for watcher "${this.expression}"`)
              }
            } else {
              this.cb.call(this.vm, value, oldValue)
            }
          }
        }
      }
    
    

    Watcherのrunメソッドを再び見て、この中でuserがtrueであればcb関数を実行すると判断して、この関数はその前に伝わるhandlerコールバック関数で、userはvm.$watchにはtrueが割り当てられ、他の場所で確立されたWatcherは基本的にfalseであり、他のlasyなどのパラメータもoptionsを通じて伝達されている.ここでは詳しくは言わないが、具体的にはコードや公式APIドキュメントを自分で見ることができる.
    締めくくり
    ここまで(計算属性の初期化はさておき)データバインディングの論理的基本分析が終わりましたが、この文章を読んだ後はWatcherオブジェクトの設計に重点を置きます.このモニタの設計はかなり巧みで、くだらないことは多くありません.何か見解や分析が間違っていることを指摘してください.