不少時候,咱們都不清楚該何時使用 Vue 的 computed 計算屬性,什麼時候該使用 watch 監聽屬性。如今讓咱們嘗試從源碼的角度來看看,它們二者的異同吧。vue
計算屬性的初始化過程,發生在 Vue 實例初始化階段的 initState()
函數中,其中有一個 initComputed
函數。該函數的定義在 src/core/instance/state.js
中:算法
const computedWatcherOptions = { lazy: 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)
}
}
}
}
複製代碼
首先建立一個空對象,接着遍歷 computed
屬性中的每個 key
, 爲每個 key
都建立一個 Watcher
。這個 Watcher
與普通的 Watcher
不同的地方在於:它是 lazy Watcher
。關於 lazy Watcher
與普通 Watcher
的區別,咱們待會展開。而後對判斷若是 key
不是實例 vm
中的屬性,調用defineComputed(vm, key, userDef)
,不然報相應的警告。express
接下來重點看defineComputed(vm, key, userDef)
的實現:數組
export function defineComputed ( target: any, key: string, userDef: Object | Function ) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.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
給計算屬性對應的key
值添加 getter
和 setter
。咱們重點來關注一下 getter
的狀況,緩存的配置也先忽略,最終 getter
對應的是 createdComputedGetter(key)
的返回值,咱們來看它的定義:緩存
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
複製代碼
createdComputedGetter(key)
返回一個函數computedGetter
,它就是計算屬性對應的 getter
。異步
至此,整個計算屬性的初始化過程到此結束。咱們知道計算屬性對應的 Watcher
是一個 lazy Watcher
,它和普通的 Watcher
有什麼區別呢?由一個例子來分析 lazy Watcher
的實現:函數
var vm = new Vue({
data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
複製代碼
當初始化整個 lazy Watcher
實例的時候,構造函數的邏輯有稍微的不一樣:oop
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
//...
this.value = this.lazy
? undefined
: this.get()
}
複製代碼
能夠發現 lazy Watcher
並不會馬上求值,而是返回的是 undefined
。組件化
而後當咱們的 render
函數執行訪問到 this.fullname
的時候,就出發了計算屬性的 getter
:post
function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
複製代碼
這簡短的幾行代碼是核心邏輯。咱們先來看:此時的 Watcher.dirty
屬性爲 true。會執行 Watcher.evaluate()
:
/** * Evaluate the value of the watcher. * This only gets called for lazy watchers. */
evaluate () {
this.value = this.get()
this.dirty = false
}
複製代碼
這裏,會經過調用 this.get()
方法,執行對應屬性的 get
函數。在咱們的例子中,就是執行:
function () {
return this.firstName + ' ' + this.lastName
}
複製代碼
這個時候,會觸發對應變量firstName
和lastName
的獲取,觸發對應的響應式過程。獲得了最新的值以後,將 this.dirty
屬性設置爲false
。
更加關鍵的代碼在這裏:
if (Dep.target) {
watcher.depend()
}
複製代碼
Vue
實例存在一個 Watcher
,它會調用計算屬性。計算屬性中有 lazy Watcher
,它會調用響應式屬性。每個 Watcher
的 get()
方法中,都有pushTarget(this)
和popTarget()
的操做。
在上面的代碼中,此時的 Dep.target
是 Vue
的實例 Watcher
,此時的 watcher
變量是計算屬性的 lazy Watcher
,經過執行代碼watcher.depend()
,將計算屬性的 lazy Watcher
關聯的 dep
都與 Dep.target
發生關聯。
在咱們的例子中,即把this.firstName
、this.lastName
與實例 Watcher
關聯起來。這樣就能夠實現:
this.firstName
、this.lastName
發生變化的時候,實例 Watcher
就會收到更新通知,此時的計算屬性也會觸發 get 函數,從而更新。this.firstName
、this.lastName
未發生變化的時候,實例 Watcher
調用計算屬性,由於 lazy Watcher
對應的 dirty
屬性爲false
,那麼就會直接返回緩存的 value
值。由此能夠看出:計算屬性中的 lazy Watcher
有如下做用:
偵聽屬性的初始化過程,與計算屬性相似,都發生在 Vue
實例初始化階段的 initState()
函數中,其中有一個 initWatch
函數。該函數的定義在 src/core/instance/state.js
中:
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
複製代碼
這裏就是對 watch
對象作遍歷,拿到每個 handler
,由於 Vue 是支持 watch
的同一個 key
對應多個 handler
,因此若是 handler
是一個數組,則遍歷這個數組,調用createWatcher
:
function createWatcher ( vm: Component, expOrFn: string | Function, handler: any, options?: Object ) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
複製代碼
這裏的邏輯也很簡單,首先對 hanlder
的類型作判斷,拿到它最終的回調函數,最後調用 vm.$watch(keyOrFn, handler, options)
函數,$watch
是 Vue 原型上的方法,它是在執行 stateMixin
的時候定義的:
Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}
複製代碼
也就是說,偵聽屬性 watch
最終會調用 $watch
方法,這個方法首先判斷 cb
若是是一個對象,則調用 createWatcher
方法,這是由於 $watch
方法是用戶能夠直接調用的,它能夠傳遞一個對象,也能夠傳遞函數。
最終都會執行const watcher = new Watcher(vm, expOrFn, cb, options)
實例化一個 Watcher
。這裏須要注意的一點是這是一個 user Watcher
,由於 options.user = true
。經過實例化 Watcher
的方式,一旦咱們 watch 的數據發生了變化,它最終會執行 Watcher
的 run
方法,執行回調函數 cb
。
經過 vm.$watch
建立的 watcher
是一個 user watcher
,其實它的功能很簡單,在對 watcher
求值以及在執行回調函數的時候,會處理一下錯誤,好比:
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
複製代碼
關於 Vue 內部的錯誤處理,有新文章作對應的討論,可戳這裏。
就應用場景而言,計算屬性適合用在模板渲染中,某個值是依賴了其它的響應式對象甚至是計算屬性計算而來;而偵聽屬性適用於觀測某個值的變化去完成一段複雜的業務邏輯。
vue源碼解讀文章目錄:
Vue 更多系列: