雖然目前的技術堆疊已由Vue 轉到了React,但從之前使用Vue 開發的多個專案實際經歷來看還是非常愉悅的,Vue 文檔清晰規範,api 設計簡潔高效,對前端開發人員友好,上手快,甚至個人認為在很多場景使用Vue 比React 開發效率更高,之前也有斷斷續續研讀過Vue 的源碼,但一直沒有梳理總結,所以在此做一些技術歸納同時也加深自己對Vue 的理解,那麼今天要寫的便是Vue 中最常用到的API 之一computed
的實作原理。
話不多說,一個最基本的例子如下:
<div> <p>{{fullName}}</p> </div>
new Vue({ data: { firstName: 'Xiao', lastName: 'Ming' }, computed: { fullName: function () { return this.firstName + ' ' + this.lastName } } })
Vue 中我們不需要在template 裡面直接計算{{this. firstName ' ' this.lastName}}
,因為在模版中放入太多宣告式的邏輯會讓模板本身過重,尤其當在頁面中使用大量複雜的邏輯表達式處理資料時,會對頁面的可維護性造成很大的影響,而computed
的設計初衷也正是用來解決這類問題。
watch
當然很多時候我們使用computed
時往往會與Vue 中另一個API 也就是偵聽器watch
比較,因為在某些方面它們是一致的,都是以Vue 的依賴追蹤機制為基礎,當某個依賴資料發生變化時,所有依賴這個資料的相關資料或函數都會自動發生變化或調用。
watch
選項提供了一個更通用的方法來回應資料的變化。當需要在資料變更時執行非同步或開銷較大的操作時,這個方式是最有用的。 從Vue 官方文件對watch
的解釋我們可以了解到,使用 watch
選項允許我們執行非同步操作(存取一個API)或高消耗效能的操作,限制我們執行該操作的頻率,並在我們得到最終結果之前,設定中間狀態,而這些都是計算屬性無法做到的。
下面也總結了幾點關於computed
和watch
的差異:
##computed 是計算一個新的屬性,並將該屬性掛載到vm(Vue 實例)上,而
watch 是監聽已經存在且已掛載到
vm 上的數據,所以用
watch 同樣可以監聽
computed 計算屬性的變化(其它還有
data、
props)
computed 基本上是一個惰性求值的觀察者,具有快取性,只有當依賴變更後,第一次存取
computed 屬性,才會計算新的值,而
watch 則是當資料發生變化便會呼叫執行函數
computed 適用一個資料被多個資料影響,而
watch 適用一個資料影響多個資料;
和watch
之間的一些差異和使用場景的區別,當然某些時候兩者並沒有那麼明確嚴格的限制,最後還是要具體到不同的業務進行分析。 原理分析
身上,為了更深層地了解計算屬性的內在機制,接下來就讓我們一步一步探索Vue源碼中關於它的實作原理吧。 在分析
原始碼之前我們先得對Vue 的響應式系統有一個基本的了解,Vue 稱其為非侵入性的響應式系統,資料模型僅僅是普通的JavaScript 對象,而當你修改它們時,視圖就會進行自動更新。
當你把一個普通的JavaScript 物件傳給Vue 實例的data
選項時,Vue 將遍歷此物件所有的屬性,並且使用Object.defineProperty
把這些屬性全部轉為getter/setter
,這些getter/setter
對使用者來說是不可見的,但是在內部它們讓Vue 追蹤依賴,在屬性被存取和修改時通知變化,每個元件實例都有對應的watcher
實例對象,它會在元件渲染的過程中把屬性記錄為依賴,之後當依賴項的setter
被呼叫時,會通知watcher
重新計算,從而致使它關聯的元件得以更新。 Vue 回應系統,其核心有三點:
observe、watcher
、dep
:<ol class=" list-paddingleft-2">
<li><p><code>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 實例,並在內實例化一個
Dep 訊息訂閱器用作後續收集依賴(例如渲染函數的
watcher
呼叫計算屬性時會觸發其
Object.defineProperty的
get存取器函數
watcher.depend()
方法將其他屬性的watcher
的evaluate
方法(進而呼叫watcher
的get
方法)讓自己成為其他 watcher
的訊息訂閱器的訂閱者,首先將watcher
賦給Dep.target
,然後執行getter
求值函數,當存取求值函數裡面的屬性(例如來自data
、
)時,會同樣觸發它們的
get 存取器函數從而將該計算屬性的watcher 加到求值函數中屬性的
watcher
dep 中,當這些操作完成,最後關閉 Dep.target
賦為null
並傳回求值函數結果。 ###############當某個屬性發生變化,觸發###set### 攔截函數,然後呼叫自身訊息訂閱器###dep### 的### notify### 方法,遍歷目前###dep### 中保存著所有訂閱者###wathcer### 的###subs### 數組,並逐一呼叫###watcher### 的 # ##update### 方法,完成回應更新。 ############相關推薦:#########javascript - 刷票器的實作原則###############thinkphp 控制器中display()步驟實作的原理############以上是Vue中computed的實作原理是什麼?的詳細內容。更多資訊請關注PHP中文網其他相關文章!