基本介紹
話不多說,一個最基本的例子如下:
<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的設計初衷也正是用于解決此類問題。
對比偵聽器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之間的一些差異和使用場景的區別,當然某些時候兩者并沒有那么明確嚴格的限制,最后還是要具體到不同的業務進行分析。
原理分析
言歸正傳,回到文章的主題computed身上,為了更深層次地了解計算屬性的內在機制,接下來就讓我們一步步探索Vue源碼中關于它的實現原理吧。
在分析computed源碼之前我們先得對Vue的響應式系統有一個基本的了解,Vue稱其為非侵入性的響應式系統,數據模型僅僅是普通的JavaScript對象,而當你修改它們時,視圖便會進行自動更新。當你把一個普通的 JavaScript 對象傳給 Vue 實例的 data 選項時,Vue 將遍歷此對象所有的屬性,并使用 Object.defineProperty 把這些屬性全部轉為 getter/setter,這些 getter/setter 對用戶來說是不可見的,但是在內部它們讓 Vue 追蹤依賴,在屬性被訪問和修改時通知變化,每個組件實例都有相應的 watcher 實例對象,它會在組件渲染的過程中把屬性記錄為依賴,之后當依賴項的 setter 被調用時,會通知 watcher 重新計算,從而致使它關聯的組件得以更新。
Vue響應系統,其核心有三點: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 < l; i++) {
subs[i].update()
}
}
}
// 當首次計算 computed 屬性的值時,Dep 將會在計算期間對依賴進行收集
Dep.target = null
const targetStack = []
export function pushTarget (_target: Watcher) {
// 在一次依賴收集期間,如果有其他依賴收集任務開始(比如:當前 computed 計算屬性嵌套其他 computed 計算屬性),
// 那么將會把當前 target 暫存到 targetStack,先進行其他 target 的依賴收集,
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
// 當嵌套的依賴收集任務完成后,將 target 恢復為上一層的 Watcher,并繼續做依賴收集
Dep.target = targetStack.pop()
}
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
方法,完成響應更新。
總結
1.初始化 data 和 computed,分別代理其 set 和 get 方法,對 data 中的所有屬性生成唯一的 dep 實例
2.對 computed 中的 屬性生成唯一的 watcher,并保存在 vm._computedWatchers 中
3.訪問計算屬性時,設置 Dep.target 指向 計算屬性的 watcher,調用該屬性具體方法
4.方法中訪問 data 的屬性,即會調用 data 屬性的 get 方法,將 data 屬性的 dep 加入到 計算屬性的 watcher , 同時該 dep 中的 subs 添加這個 watcher
5.設置 data 的這個屬性時,調用該屬性代理的 set 方法,觸發 dep 的 notify 方法
6.因為時 computed 屬性,只是將 watcher 中的 dirty 設置為 true
7.最后,訪問計算屬性的 get 方法時,得知該屬性的 watcher.dirty 為 true,則調用 watcher.evaluate() 方法獲取新的值
綜合以上:也可以解釋了為什么有些時候當computed沒有被訪問(或者沒有被模板依賴),當修改了this.data值后,通過vue-tools發現其computed中的值沒有變化的原因,因為沒有觸發到其get方法。
參考文章:
https://segmentfault.com/a/1190000010408657
https://segmentfault.com/a/1190000016368913?utm_source=tag-newest