在開發過程當中,咱們對這兩個屬性已經很是熟悉了:computed
和watch
。可是究其實現原理,或者說兩者到底有何區別,以及何時使用計算屬性,何時使用偵聽屬性,相信有很多朋友們仍存在疑惑。下面就一塊兒來探討一下:react
關於計算屬性的使用,見以下代碼。express
export default {
data() {
return {
msg : {a : 1},
count : 1
}
}
methods : {
changeA() {
this.count++;
}
}
computed: {
newMsg() {
if (this.count < 3) return this.count;
return 5;
}
}
}
複製代碼
在初始化時(initState
),判斷若是opts.computed
存在(用戶定義了computed
屬性),執行initComputed
。數組
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 (!isSSR) {
// create internal watcher for the computed property.
// self-notes : it won't evaluate value immediately
watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
}
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else {
// throw a warn(The computed property id already defined in data / prop
}
}
}
複製代碼
在initComputed
中,首先獲取用戶定義的computed
(能夠是函數,也能夠是對象)。若是不是服務端渲染,就調用new Watcher()
實例化一個 Watcher(咱們稱之爲computed Watcher
),實例化的具體過程咱們稍後討論。緊接着,判斷若是當前 vm 對象上找不到computed
對象的 key 值,調用defineComputed
,而該函數就完成了將 key 值變成了 vm 的屬性。函數
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
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
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
複製代碼
除此以外,在該函數中還作了另外一件更重要的事情,設置了computed
的 getter 和 setter,將createComputedGetter
函數的返回值做爲 getter,而 setter 並不經常使用,能夠暫且不關注。oop
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
}
}
}
複製代碼
該函數返回一個函數,函數中進行了computed
值的計算和依賴收集。那何時會觸發這個 getter 呢?一樣是在執行render
時,會訪問到咱們定義的相關數據。以上就是計算屬性初始化的大體過程。ui
可是,若是順着以上思路走,在調試源碼時會發現一個問題,在走到if(!(key in vm))
時,條件是不成立的,這樣就沒法執行defineComputed
,還談何初始化。那這是爲何呢?其實,查找源碼會發現,在Vue.extend
(也就是處理子組件的構造器時)中已經對計算屬性作了一些處理。下面一塊兒來看一下。this
Vue.extend = function (extendOptions: Object): Function {
if (Sub.options.computed) {
initComputed(Sub)
}
}
function initComputed (Comp) {
const computed = Comp.options.computed
for (const key in computed) {
defineComputed(Comp.prototype, key, computed[key])
}
}
複製代碼
所以總體邏輯是這樣的。在最外層組件的initState
時,判斷opts.computed
爲undefined
,繼續走到Vue.extend
(生成子組件的構造器),此時Sub.options.computed
是存在的,調用initComputed
(函數內調用defineComputed
),設置計算屬性的 getter 爲createComputedGetter
函數的返回值(是一個函數)。lua
當再次走到子組件的initState
時,此時opts.computed
是存在的,調用initComputed()
(實例化computed Watcher
,值爲undefined
),此時if(!(key in vm))
條件不成立,計算屬性完成的初始化。spa
因爲設置了 getter,在render
過程當中訪問到計算屬性時,就會調用其 getter,在該函數中調用watcher.evaluate()
計算出計算屬性的值【調用watcher.get()
-> watcher.getter()
(即用戶定義的計算屬性的 getter,在過程當中會訪問到計算屬性依賴的其餘數據(即執行數據的 getter,同時進行依賴收集),將訂閱該數據變化的computed Watcher
存入new Dep().subs
中)】,最後調用watcher.depend()
(再次依賴收集,將render Watcher
存入new Dep().subs
中)。此時就完成了計算屬性的初次計算以及依賴收集。當咱們更改數據時,就進入了派發更新的過程。prototype
set: function reactiveSetter(newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
// #7981: for accessor properties without setter
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
複製代碼
當咱們更改計算屬性依賴的數據時,會觸發它的 setter,若是值發生變化,會調用dep.notify()
。在該函數中,會遍歷依賴數組分別執行update
(subs
中有兩個 Watcher(computed Watcher
和render Watcher
)),當執行到render Watcher
的update
時,調用queueWatcher
(在過程當中從新計算計算屬性的值,從新渲染頁面)。
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
Watcher.prototype.update = function update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
}
複製代碼
以上就是關於計算屬性的內容,接下來看偵聽屬性。
關於偵聽屬性的使用,見以下代碼。
export default {
data() {
return {
msg: { a: 1 },
count: 1,
nested: {
a: {
b: 1
}
}
};
},
methods: {
change() {
this.count++;
this.nested.a.b++;
}
},
computed: {
newMsg() {
if (this.count < 4) {
return this.count;
}
return 5;
}
},
watch: {
newMsg(newVal) {
console.log("newMsg : " + newVal);
},
count: {
immediate: true,
handler(newVal) {
console.log("count : " + newVal);
}
},
nested: {
deep: true,
sync: true,
handler(newVal) {
console.log("nested : " + newVal.a.b);
}
}
}
}
複製代碼
同計算屬性同樣,在initState
中,判斷若是opts.watch
存在,則調用initWatch
對偵聽屬性進行初始化。
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)
}
}
}
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)
}
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()
}
}
複製代碼
初始的流程也很是簡單,大體通過了initWatch
-> createWatcher
-> vm.$watch
,在vm.$watch
中,建立了user Watcher
,且該函數返回一個函數,能夠用來銷燬user Watcher
。接下來一塊兒探討user Watcher
的建立過程。
// new Watcher(vm, expOrFn, cb, options)
export default class Watcher {
constructor(vm, exporFn, cb, options){
this.deep = !!options.deep
this.user = !!options.user
this.sync = !!options.sync
this.getter = parsePath(exporFn)
this.value = this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} finally {
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
}
export function parsePath (path: string): any {
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
obj = obj[segments[i]]
}
return obj
}
}
複製代碼
在user Watcher
的建立過程當中,因爲傳入的getter
是一個字符串,須要通過parsePath
函數處理,該函數返回一個函數,賦值給watcher.getter
。與計算屬性不一樣,偵聽屬性會當即調用watcher.get()
進行求值(過程當中執行watcher.getter
會訪問到相關數據,觸發數據的 getter,進行依賴收集)。user Watcher
建立完畢後,判斷options.immediate
若是爲 true,則當即執行用戶定義的回調函數。 當數據發生更改時,調用dep.notify()
進行派發更新,總體更新過程仍是相似以前,此處就再也不進行詳細解釋。 總結一下,計算屬性的本質是computed Watcher
,偵聽屬性的本質是user Watcher
,那究竟什麼時候使用計算屬性,什麼時候使用偵聽屬性呢?通常來講,計算屬性適合使用在模板渲染中,某個值是依賴了某些響應式對象而計算得來的;而偵聽屬性適用於觀測某個值的變化去完成一段邏輯。