ホームページ >ウェブフロントエンド >jsチュートリアル >Vue における computed の実装原理は何ですか?

Vue における computed の実装原理は何ですか?

不言
不言オリジナル
2018-09-14 16:10:063920ブラウズ
この記事の内容は、Vue における computed の実装原理についてです。困っている友人は参考にしていただければ幸いです。

現在のテクノロジー スタックは Vue から React に移行しましたが、Vue を使用して複数のプロジェクトを開発する実際の経験は依然として非常に快適です。Vue のドキュメントは明確で標準化されており、API 設計はシンプルで効率的で、フレンドリーです。フロントエンド開発者へ すぐに始めることができ、個人的には React 開発よりも Vue を使用した方が効率的だとさえ思っています。ここでいくつかの技術的な概要を作成し、Vue についての理解を深めたいと思います。そこで、今日書きたいのは、Vue で最もよく使用される API の 1 つである computed の実装原理です。

基本的な紹介

さっそく、基本的な例を以下に示します。

<div>
    <p>{{fullName}}</p>
</div>
new Vue({
    data: {
        firstName: 'Xiao',
        lastName: 'Ming'
    },
    computed: {
        fullName: function () {
            return this.firstName + ' ' + this.lastName
        }
    }
})

Vue では、テンプレート内で直接計算する必要はありません{{ this.firstName ' ' this.lastName}}、テンプレートに宣言ロジックを入れすぎると、特にページ内のデータを処理するために多数の複雑な論理式が使用される場合、テンプレート自体が過大になるためです。ページの保守性に大きな影響を与えるため、computed はそのような問題を解決するように設計されています。

リスナーの比較watch

もちろん、computed を使用するときは、Vue の別の API と比較することがよくあります。 listenerwatch 比較すると、これらはいくつかの点で一貫しているため、どちらも Vue の依存関係追跡メカニズムに基づいており、特定の依存関係データが変更されると、このデータに依存するすべての関連データまたは関数が自動的に変更されるか、呼び出されます。 。

ほとんどの場合、計算されたプロパティの方が適切ですが、カスタム リスナーが必要になる場合もあります。そのため、Vue では watch オプションを介してデータ変更に対応するためのより一般的な方法を提供しています。このアプローチは、データ変更時に非同期操作またはコストのかかる操作を実行する必要がある場合に最も役立ちます。

Vue 公式ドキュメントの watch の説明から、watch オプションを使用すると、非同期操作 (API へのアクセス) または高パフォーマンスの操作を実行できることがわかります。最終結果に到達する前に、この操作を実行して中間状態を設定する頻度 (計算されたプロパティでは実行できない) を制限します。

以下では、computedwatch の違いに関するいくつかの追加ポイントも要約しています:

  1. computed は新しいプロパティを計算し、そのプロパティを vm (Vue インスタンス) にマウントします。一方、watch は既存のプロパティを監視し、vm にマウントします。データなので、watch を使用して、computed 計算プロパティ (その他には dataprops が含まれます)

    # の変更を監視することもできます。
  2. ##computed これは本質的に、キャッシュ可能性を備えた遅延評価オブザーバーであり、依存関係が変更された場合にのみ、 computed 属性が初めてアクセスされます。新しい値が計算され、データが変更されたときに watch が実行関数を呼び出します。

  3. 使用シナリオに関しては、

    computed が適用されます。 1 つのデータが複数のデータに影響され、watch が 1 つのデータが複数のデータに影響する場合、

上記の

computed について学習しました。もちろん、この 2 つはそれほど明確で厳密ではない場合もありますが、最終的にはさまざまなビジネスを具体的に分析する必要があります。 原理分析

記事のトピック

computed

に戻りましょう。計算プロパティの内部メカニズムをより深く理解するために、Vue を調べてみましょう。ステップバイステップ ソースコードでの実装原理について話しましょう。

computed ソース コードを分析する前に、まず Vue の応答システムについて基本的に理解しておく必要があります。Vue ではこれを非侵入型応答システムと呼び、データ モデルは単なる通常の JavaScript オブジェクトです。それらを変更すると、ビューが自動的に更新されます。

通常の JavaScript オブジェクトを Vue インスタンスの dataVue における computed の実装原理は何ですか? オプションに渡すと、Vue はオブジェクトのすべてのプロパティを走査します。そして、Object.defineProperty

を使用して、これらすべてのプロパティを getter/setter に変換します。これらの getter/setter はユーザーには表示されませんが、内部的には Vue に依存関係を追跡させます。各コンポーネント インスタンスには、対応する watcher インスタンス オブジェクトがあり、コンポーネントのレンダリング中にプロパティが依存関係として記録され、依存関係が項目の setter## に記録されるときに変更が通知されます。 # が呼び出されると、watcher に再計算するよう通知し、関連するコンポーネントが更新されます。 Vue 応答システムには 3 つのコア ポイントがあります: observewatcher

dep:

  1. observe:遍历 data 中的属性,使用 Object.defineProperty 的 get/set 方法对其进行数据劫持;

  2. dep:每个属性拥有自己的消息订阅器 dep,用于存放所有订阅了该属性的观察者对象;

  3. watcher:观察者(对象),通过 dep 实现对响应属性的监听,监听到结果后,主动触发自己的回调进行响应。

对响应式系统有一个初步了解后,我们再来分析计算属性。
首先我们找到计算属性的初始化是在 src/core/instance/state.js 文件中的 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 */)
  }
  // computed初始化
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

调用了 initComputed 函数(其前后也分别初始化了 initDatainitWatch )并传入两个参数 vm 实例和 opt.computed 开发者定义的 computed 选项,转到 initComputed 函数:

const computedWatcherOptions = { computed: true }

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        'Getter is missing for computed property "${key}".',
        vm
      )
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn('The computed property "${key}" is already defined in data.', vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn('The computed property "${key}" is already defined as a prop.', vm)
      }
    }
  }
}

从这段代码开始我们观察这几部分:

  1. 获取计算属性的定义 userDefgetter 求值函数

    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get

    定义一个计算属性有两种写法,一种是直接跟一个函数,另一种是添加 setget 方法的对象形式,所以这里首先获取计算属性的定义 userDef,再根据 userDef 的类型获取相应的 getter 求值函数。

  2. 计算属性的观察者 watcher 和消息订阅器 dep

    watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
    )

    这里的 watchers 也就是 vm._computedWatchers 对象的引用,存放了每个计算属性的观察者 watcher 实例(注:后文中提到的“计算属性的观察者”、“订阅者”和 watcher 均指代同一个意思但注意和 Watcher 构造函数区分),Watcher 构造函数在实例化时传入了 4 个参数:vm 实例、getter 求值函数、noop 空函数、computedWatcherOptions 常量对象(在这里提供给 Watcher 一个标识 {computed:true} 项,表明这是一个计算属性而不是非计算属性的观察者,我们来到 Watcher 构造函数的定义:

    class Watcher {
      constructor (
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
        options?: ?Object,
        isRenderWatcher?: boolean
      ) {
        if (options) {
          this.computed = !!options.computed
        } 
    
        if (this.computed) {
          this.value = undefined
          this.dep = new Dep()
        } else {
          this.value = this.get()
        }
      }
      
      get () {
        pushTarget(this)
        let value
        const vm = this.vm
        try {
          value = this.getter.call(vm, vm)
        } catch (e) {
          
        } finally {
          popTarget()
        }
        return value
      }
      
      update () {
        if (this.computed) {
          if (this.dep.subs.length === 0) {
            this.dirty = true
          } else {
            this.getAndInvoke(() => {
              this.dep.notify()
            })
          }
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)
        }
      }
    
      evaluate () {
        if (this.dirty) {
          this.value = this.get()
          this.dirty = false
        }
        return this.value
      }
    
      depend () {
        if (this.dep && Dep.target) {
          this.dep.depend()
        }
      }
    }

    为了简洁突出重点,这里我手动去掉了我们暂时不需要关心的代码片段。
    观察 Watcherconstructor ,结合刚才讲到的 new Watcher 传入的第四个参数 {computed:true} 知道,对于计算属性而言 watcher 会执行 if 条件成立的代码 this.dep = new Dep(),而 dep 也就是创建了该属性的消息订阅器。

    export default class Dep {
      static target: ?Watcher;
      subs: Array<watcher>;
    
      constructor () {
        this.id = uid++
        this.subs = []
      }
    
      addSub (sub: Watcher) {
        this.subs.push(sub)
      }
    
      depend () {
        if (Dep.target) {
          Dep.target.addDep(this)
        }
      }
    
      notify () {
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i <p><code>Dep</code> 同样精简了部分代码,我们观察 <code>Watcher</code> 和 <code>Dep</code> 的关系,用一句话总结</p>
    <blockquote>
    <code>watcher</code> 中实例化了 <code>dep</code> 并向 <code>dep.subs</code> 中添加了订阅者,<code>dep</code> 通过 <code>notify</code> 遍历了 <code>dep.subs</code> 通知每个 <code>watcher</code> 更新。</blockquote></watcher>
  3. defineComputed 定义计算属性

    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn('The computed property "${key}" is already defined in data.', vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn('The computed property "${key}" is already defined as a prop.', vm)
      }
    }

    因为 computed 属性是直接挂载到实例对象中的,所以在定义之前需要判断对象中是否已经存在重名的属性,defineComputed 传入了三个参数:vm 实例、计算属性的 key 以及 userDef 计算属性的定义(对象或函数)。
    然后继续找到 defineComputed 定义处:

    export function defineComputed (
      target: any,
      key: string,
      userDef: Object | Function
    ) {
      const shouldCache = !isServerRendering()
      if (typeof userDef === 'function') {
        sharedPropertyDefinition.get = shouldCache
          ? createComputedGetter(key)
          : userDef
        sharedPropertyDefinition.set = noop
      } else {
        sharedPropertyDefinition.get = userDef.get
          ? shouldCache && userDef.cache !== false
            ? createComputedGetter(key)
            : userDef.get
          : noop
        sharedPropertyDefinition.set = userDef.set
          ? userDef.set
          : noop
      }
      if (process.env.NODE_ENV !== 'production' &&
          sharedPropertyDefinition.set === noop) {
        sharedPropertyDefinition.set = function () {
          warn(
            'Computed property "${key}" was assigned to but it has no setter.',
            this
          )
        }
      }
      Object.defineProperty(target, key, sharedPropertyDefinition)
    }

    在这段代码的最后调用了原生 Object.defineProperty 方法,其中传入的第三个参数是属性描述符sharedPropertyDefinition,初始化为:

    const sharedPropertyDefinition = {
      enumerable: true,
      configurable: true,
      get: noop,
      set: noop
    }

    随后根据 Object.defineProperty 前面的代码可以看到 sharedPropertyDefinitionget/set 方法在经过 userDef 和  shouldCache 等多重判断后被重写,当非服务端渲染时,sharedPropertyDefinitionget 函数也就是 createComputedGetter(key) 的结果,我们找到 createComputedGetter 函数调用结果并最终改写  sharedPropertyDefinition 大致呈现如下:

    sharedPropertyDefinition = {
        enumerable: true,
        configurable: true,
        get: function computedGetter () {
            const watcher = this._computedWatchers && this._computedWatchers[key]
            if (watcher) {
                watcher.depend()
                return watcher.evaluate()
            }
        },
        set: userDef.set || noop
    }

    当计算属性被调用时便会执行 get 访问函数,从而关联上观察者对象 watcher 然后执行 wather.depend() 收集依赖和 watcher.evaluate() 计算求值。

すべてのステップを分析した後、プロセス全体を要約しましょう:

  1. コンポーネントが初期化されるとき、computed data はそれぞれ独自の応答システムを確立します。 Observer dataget/set の各属性設定をトラバースします データ インターセプト

  2. #Initialization

    computedinitComputed 関数

    1. を呼び出して

      watcher インスタンスを登録します。そして、後続のコレクションの依存関係 (レンダリング関数の watcher や、計算されたプロパティの変更を監視する他の watcher など) のために、内部的に Dep メッセージ サブスクライバーをインスタンス化します。

    2. 計算プロパティが呼び出されると、その

      Object.definePropertyget アクセサー関数

    3. トリガーされる

      watcher.depend() メソッドは、他の属性の watcher を自身のメッセージ サブスクライバー subs に追加しますdep

    4. watcherevaluate メソッドを呼び出します (その後、watcherget メソッドを呼び出します)自分自身を watcher のメッセージ サブスクライバの他の サブスクライバにするには、まず watcherDep.target に割り当て、次に getter 評価を実行します。関数内の評価 When 属性 (dataprops、またはその他の computed からなど) にアクセスする場合、その get アクセサー。関数もトリガーされます。計算されたプロパティの watcher を、評価関数のプロパティの watcher のメッセージ サブスクライバー dep に追加して、最後に閉じます。 これらの操作が完了すると、Dep.targetnull に代入され、評価関数の結果が返されます。

  3. 特定の属性が変更されると、

    set インターセプト関数をトリガーし、それ自体のメッセージ サブスクライバー dep# の を呼び出します。 ## Notice メソッドは、現在の dep 内のすべてのサブスクライバ wathcer を保持する subs 配列を走査し、watcher を呼び出します。 ##update メソッドを使用して応答の更新を完了します。

    関連する推奨事項:

javascript - チケット ブラッシャーの実装原則


thinkphp コントローラーdisplay() ステップ実装の原理

以上がVue における computed の実装原理は何ですか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。