ホームページ >ウェブフロントエンド >jsチュートリアル >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 の依存関係追跡メカニズムに基づいており、特定の依存関係データが変更されると、このデータに依存するすべての関連データまたは関数が自動的に変更されるか、呼び出されます。 。
watch
オプションを介してデータ変更に対応するためのより一般的な方法を提供しています。このアプローチは、データ変更時に非同期操作またはコストのかかる操作を実行する必要がある場合に最も役立ちます。 Vue 公式ドキュメントの watch
の説明から、watch
オプションを使用すると、非同期操作 (API へのアクセス) または高パフォーマンスの操作を実行できることがわかります。最終結果に到達する前に、この操作を実行して中間状態を設定する頻度 (計算されたプロパティでは実行できない) を制限します。
以下では、computed
と watch
の違いに関するいくつかの追加ポイントも要約しています:
computed
は新しいプロパティを計算し、そのプロパティを vm (Vue インスタンス) にマウントします。一方、watch
は既存のプロパティを監視し、vm
にマウントします。データなので、watch
を使用して、computed
計算プロパティ (その他には data
、props
が含まれます)
##computed これは本質的に、キャッシュ可能性を備えた遅延評価オブザーバーであり、依存関係が変更された場合にのみ、
computed 属性が初めてアクセスされます。新しい値が計算され、データが変更されたときに
watch が実行関数を呼び出します。
computed が適用されます。 1 つのデータが複数のデータに影響され、
watch が 1 つのデータが複数のデータに影響する場合、
computed について学習しました。もちろん、この 2 つはそれほど明確で厳密ではない場合もありますが、最終的にはさまざまなビジネスを具体的に分析する必要があります。
原理分析
computed
ソース コードを分析する前に、まず Vue の応答システムについて基本的に理解しておく必要があります。Vue ではこれを非侵入型応答システムと呼び、データ モデルは単なる通常の JavaScript オブジェクトです。それらを変更すると、ビューが自動的に更新されます。
通常の JavaScript オブジェクトを Vue インスタンスの data オプションに渡すと、Vue はオブジェクトのすべてのプロパティを走査します。そして、Object.defineProperty
を使用して、これらすべてのプロパティをgetter/setter
に変換します。これらの getter/setter
はユーザーには表示されませんが、内部的には Vue に依存関係を追跡させます。各コンポーネント インスタンスには、対応する watcher
インスタンス オブジェクトがあり、コンポーネントのレンダリング中にプロパティが依存関係として記録され、依存関係が項目の setter## に記録されるときに変更が通知されます。 # が呼び出されると、
watcher に再計算するよう通知し、関連するコンポーネントが更新されます。
Vue 応答システムには 3 つのコア ポイントがあります: observe
、watcher
、dep:
observe
:遍历 data
中的属性,使用 Object.defineProperty 的 get/set
方法对其进行数据劫持;
dep
:每个属性拥有自己的消息订阅器 dep
,用于存放所有订阅了该属性的观察者对象;
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
函数(其前后也分别初始化了 initData
和 initWatch
)并传入两个参数 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) } } } }
从这段代码开始我们观察这几部分:
获取计算属性的定义 userDef
和 getter
求值函数
const userDef = computed[key] const getter = typeof userDef === 'function' ? userDef : userDef.get
定义一个计算属性有两种写法,一种是直接跟一个函数,另一种是添加 set
和 get
方法的对象形式,所以这里首先获取计算属性的定义 userDef
,再根据 userDef
的类型获取相应的 getter
求值函数。
计算属性的观察者 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() } } }
为了简洁突出重点,这里我手动去掉了我们暂时不需要关心的代码片段。
观察 Watcher
的 constructor
,结合刚才讲到的 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>
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
前面的代码可以看到 sharedPropertyDefinition
的 get/set
方法在经过 userDef
和 shouldCache
等多重判断后被重写,当非服务端渲染时,sharedPropertyDefinition
的 get
函数也就是 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()
计算求值。
コンポーネントが初期化されるとき、computed
と data
はそれぞれ独自の応答システムを確立します。 Observer
data
get/set
の各属性設定をトラバースします データ インターセプト
computed は
initComputed 関数
watcher インスタンスを登録します。そして、後続のコレクションの依存関係 (レンダリング関数の
watcher や、計算されたプロパティの変更を監視する他の
watcher など) のために、内部的に
Dep メッセージ サブスクライバーをインスタンス化します。
Object.defineProperty の
get アクセサー関数
watcher.depend() メソッドは、他の属性の
watcher を自身のメッセージ サブスクライバー
の subs
に追加しますdep
watcher の
evaluate メソッドを呼び出します (その後、
watcher の
get メソッドを呼び出します)自分自身を watcher
のメッセージ サブスクライバの他の サブスクライバにするには、まず
watcher を
Dep.target に割り当て、次に
getter 評価を実行します。関数内の評価 When 属性 (
data、
props、またはその他の
computed からなど) にアクセスする場合、その
get アクセサー。関数もトリガーされます。計算されたプロパティの
watcher を、評価関数のプロパティの
watcher のメッセージ サブスクライバー
dep に追加して、最後に閉じます。
これらの操作が完了すると、Dep.target が
null に代入され、評価関数の結果が返されます。
set インターセプト関数をトリガーし、それ自体のメッセージ サブスクライバー
dep# の を呼び出します。 ## Notice
メソッドは、現在の dep
内のすべてのサブスクライバ wathcer
を保持する subs
配列を走査し、watcher
を呼び出します。 ##update メソッドを使用して応答の更新を完了します。
javascript - チケット ブラッシャーの実装原則
thinkphp コントローラーdisplay() ステップ実装の原理
以上がVue における computed の実装原理は何ですか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。