雖然目前的技術棧已由Vue轉到了React,但從以前使用Vue開發的多個項目實際經從來看仍是很是愉悅的,Vue文檔清晰規範,api設計簡潔高效,對前端開發人員友好,上手快,甚至我的認爲在不少場景使用Vue比React開發效率更高,以前也有斷斷續續研讀過Vue的源碼,但一直沒有梳理總結,因此在此作一些技術概括同時也加深本身對Vue的理解,那麼今天要寫的即是Vue中最經常使用到的API之一computed
的實現原理。前端
話很少說,一個最基本的例子以下:vue
<div id="app"> <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
的設計初衷也正是用於解決此類問題。api
watch
固然不少時候咱們使用computed
時每每會與Vue中另外一個API也就是偵聽器watch
相比較,由於在某些方面它們是一致的,都是以Vue的依賴追蹤機制爲基礎,當某個依賴數據發生變化時,全部依賴這個數據的相關數據或函數都會自動發生變化或調用。數組
雖然計算屬性在大多數狀況下更合適,但有時也須要一個自定義的偵聽器。這就是爲何 Vue 經過
watch
選項提供了一個更通用的方法來響應數據的變化。當須要在數據變化時執行異步或開銷較大的操做時,這個方式是最有用的。
從vue官方文檔對watch
的解釋咱們能夠了解到,使用 watch
選項容許咱們執行異步操做 (訪問一個API)或高消耗性能的操做,限制咱們執行該操做的頻率,並在咱們獲得最終結果前,設置中間狀態,而這些都是計算屬性沒法作到的。緩存
computed
和watch
的差別:computed
是計算一個新的屬性,並將該屬性掛載到vm(Vue實例)上,而watch
是監聽已經存在且已掛載到vm
上的數據,因此用watch
一樣能夠監聽computed
計算屬性的變化(其它還有data
、props
)computed
本質是一個惰性求值的觀察者,具備緩存性,只有當依賴變化後,第一次訪問 computed
屬性,纔會計算新的值,而watch
則是當數據發生變化便會調用執行函數computed
適用一個數據被多個數據影響,而watch
適用一個數據影響多個數據;以上咱們瞭解了computed
和watch
之間的一些差別和使用場景的區別,固然某些時候二者並無那麼明確嚴格的限制,最後仍是要具體到不一樣的業務進行分析。app
言歸正傳,回到文章的主題computed
身上,爲了更深層次地瞭解計算屬性的內在機制,接下來就讓咱們一步步探索Vue源碼中關於它的實現原理吧。異步
在分析computed
源碼以前咱們先得對Vue的響應式系統有一個基本的瞭解,Vue稱其爲非侵入性的響應式系統,數據模型僅僅是普通的JavaScript對象,而當你修改它們時,視圖便會進行自動更新。函數
當你把一個普通的 JavaScript 對象傳給 Vue 實例的data
選項時,Vue 將遍歷此對象全部的屬性,並使用Object.defineProperty
把這些屬性所有轉爲getter/setter
,這些getter/setter
對用戶來講是不可見的,可是在內部它們讓 Vue 追蹤依賴,在屬性被訪問和修改時通知變化,每一個組件實例都有相應的watcher
實例對象,它會在組件渲染的過程當中把屬性記錄爲依賴,以後當依賴項的setter
被調用時,會通知watcher
從新計算,從而導致它關聯的組件得以更新。
Vue響應系統,其核心有三點:observe
、watcher
、dep
:oop
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 < l; i++) { subs[i].update() } } } Dep.target = null
dep
一樣精簡了部分代碼,咱們觀察Watcher
和dep
的關係,用一句話總結
watcher
中實例化了dep
並向dep.subs
中添加了訂閱者,dep
經過notify
遍歷了dep.subs
通知每一個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
。
分析完以上步驟,咱們再來梳理下整個流程:
computed
和data
會分別創建各自的響應系統,Observer
遍歷data
中每一個屬性設置get/set
數據攔截初始化computed
會調用initComputed
函數
watcher
實例,並在內實例化一個Dep
消息訂閱器用做後續收集依賴(好比渲染函數的watcher
或者其餘觀察該計算屬性變化的watcher
)Object.defineProperty
的get
訪問器函數watcher.depend()
方法向自身的消息訂閱器dep
的subs
中添加其餘屬性的watcher
watcher
的evaluate
方法(進而調用watcher
的get
方法)讓自身成爲其餘watcher
的消息訂閱器的訂閱者,首先將watcher
賦給Dep.target
,而後執行getter
求值函數,當訪問求值函數裏面的屬性(好比來自data
、props
或其餘computed
)時,會一樣觸發它們的get
訪問器函數從而將該計算屬性的watcher
添加到求值函數中屬性的watcher
的消息訂閱器dep
中,當這些操做完成,最後關閉Dep.target
賦爲null
並返回求值函數結果。set
攔截函數,而後調用自身消息訂閱器dep
的notify
方法,遍歷當前dep
中保存着全部訂閱者wathcer
的subs
數組,並逐個調用watcher
的 update
方法,完成響應更新。