身為一個菜蟲,洗菜的時候抓到很多蟲應該超棒的(欸!只是說,有些蟲實在是很惱人,因為那也不算是蟲,只是個隱藏版的功能。至於為何沒有寫在文件裡?這就只能去問作者了!
注意!最新版本的 Vue 2.6.10 與本文部分原始碼可能會有所出入,但邏輯大部分相同。服用前請先詳閱公開說明書( 哪來的公開說明書 )。
$watch 的實際狀況與問題
文件裡面有特別寫到 $watch
的額外選項,
deep
immediate
這個就不多做解釋,可以去看官方文件
我來聊一下關於 $watch
遇到的問題,
methods: {
...mapActions(['setTick'])
},
computed: {
...mapGetters(['getTick'])
},
created () {
this.$watch(() => {
return this.getTick
}, (tick) {
console.log(tick)
})
setTimeout(() => {
this.setTick(1)
}, 500)
}
這樣你會拿到 1
,這是沒有問題的。接著,我們連續觸發兩次,
created () {
this.$watch(() => {
return this.getTick
}, (tick) {
console.log(tick)
})
setTimeout(() => {
this.setTick(1)
this.setTick(2)
}, 500)
}
這樣你會拿到 2
,看起來也是沒有問題的。但是,第一次呼叫的 1
呢?在連續觸發的情況下,$watch
只會認最後一次變更的結果,換句話說,如果連續觸發多次,只有最後一次的結果會被傳回 $watch
的回呼函示中。
created () {
this.$watch(() => {
return this.getTick
}, (tick) {
console.log(tick)
})
setTimeout(() => {
this.setTick(1)
this.setTick(2)
this.setTick(3)
this.setTick(4)
this.setTick(5)
}, 500)
}
最後,你會拿到 5
這個結果。如果這個 $watch
存在於 setTimeout
或是 rAF (requestAnimationFrame) 當中,則不會有這個問題,
created () {
this.$watch(() => {
return this.getTick
}, (tick) {
console.log(tick)
})
setTimeout(() => {
this.setTick(1)
})
setTimeout(() => {
this.setTick(2)
})
setTimeout(() => {
this.setTick(3)
})
setTimeout(() => {
this.setTick(4)
})
setTimeout(() => {
this.setTick(5)
})
}
這樣你就能拿到 1
, 2
, 3
, 4
, 5
這樣的結果。但,這樣好像挺蠢的。基於抓菜蟲的精神,所以我翻了一下 Vue 的原始碼,當中關於 Watcher
的地方,發現了幾個菜蟲。
$watch 原始碼追蹤
在 Watcher
這個元件中,除了官方提及的 deep
與 immediate
外,他還多了 user
, lazy
與 sync
這三個項目。奇妙的是,immediate
並沒有在 Watcher
的初始化方法中被列出,反而是在 $watch
這個方法才出現(不知道為什麼?
// options
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.lazy = !!options.lazy;
this.sync = !!options.sync;
} else {
this.deep = this.user = this.lazy = this.sync = false;
}
這三個文件當中沒有的設定,lazy
, lazy
與 sync
到底功用為何?我們先來看看他的更新動作,
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
這裡用到了 lazy
與 sync
,那麼 user
在哪裡呢?答案在 run
這個方法裡面,
Watcher.prototype.run = function run () {
// ...中略
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue);
} catch (e) {
/* istanbul ignore else */
if (config.errorHandler) {
config.errorHandler.call(null, e, this.vm);
} else {
process.env.NODE_ENV !== 'production' && warn(
("Error in watcher \"" + (this.expression) + "\""),
this.vm
);
throw e
}
}
} else {
this.cb.call(this.vm, value, oldValue);
}
// ...後略
}
那到底他們各自做了什麼事情?
lazy 設定
lazy
並不會影響外部動作,他僅是將內部的 dirty
給一個預設值,意思就是說,當你設定 lazy: true
的時候,這個 Watcher
在初始化的時候,自身的 dirty
就會被設定為 true
。
這件事情基本上跟 immediate
有點關連,舉例來說,
created () {
this.$watch(() => {
return this.getTick
}, (tick) {
console.log(tick)
}, {
immediate: true,
lazy: true
})
setTimeout(() => {
this.setTick(1)
})
這樣的話,由於你設定了 immediate
,所以在 $watch
初始化的時候會呼叫一次,接著,因為設定了 lazy
,所以第一次呼叫會傳回 undefined
。這件事情在初始化有數值的時候,是不適合這樣做的,舉例來說,
created () {
this.$watch(() => {
return this.tick
}, (tick) {
console.log(tick)
}, {
immediate: true,
lazy: true
})
setTimeout(() => {
this.tick(1)
})
},
data () {
return {
tick: 0
}
}
由於加上了 immediate
,所以在整個 $watch
被初始化的同時,會先回傳一個 0
,但是,因為加上 lazy
的關係,所以並不會是 0
而會回傳 undefined
!而每次的 lazy
並不會被重新設定,而他也只會在 $watch
初始化時發生作用。
user 設定
這個設定挺神奇,感覺上就只是為了避免資料有非同步傳輸的情況,而誕生的一個設定值。他出現在 run
的方法當中,如果將 user
設定為 true
的同時,他並不會直接去呼叫回呼函示將數值傳回,而是包著 try { ... } catch (e) { }
的方式來避免一些例外情況發生(請看上面的原始碼。
也就是說,當你 { user: true }
的時候,他會試著呼叫回呼函示,如果錯誤了,就拋出一個異常,交給 Vue 的異常函示去處理後續動作(預設是 Vue.config.errorHandler
,如果沒有設定,在正式站的環境下,會直接噴一個 throw
出去。
sync 設定
顧名思義,就是同步處理,也就是這次要解決 setTimeout
這個蠢方法的方式。舉例來說,
created () {
this.$watch(() => {
return this.getTick
}, (tick) {
console.log(tick)
}, {
sync: true
})
setTimeout(() => {
this.setTick(1)
this.setTick(2)
this.setTick(3)
this.setTick(4)
this.setTick(5)
}, 500)
}
這樣你就能拿到 1
, 2
, 3
, 4
, 5
這樣的結果,而不需要連續的 setTimeout
來達到目的。主要的方式在於 update
時,當遇到 sync === true
時,直接呼叫 run
方法,把數值輸出,而不會使用預設的 queueWatcher
。
所以,這個方法會在任何的變更時,都會呼叫 $watch
的 run
並且餵給回呼函示,將資料帶出去。當然,我想效能上可能會有低落的可能(畢竟每次都餵出去。
但是,我總覺得每次都餵出去才比較合乎我們凡人的想像,畢竟他的 queueWatcher
有個 很機歪 偷效能的地方。
為何預設方式無法連續觸發?
而 $watch
本身的 run
預設是直接進入 queueWatcher
去將 $watch
的東西做一個排程。所以連續呼叫的狀態下,有人會因為這個排程的執行而被剔除。我們先來看看 queueWatcher
的原始碼,
function queueWatcher (watcher) {
var id = watcher.id;
if (has$1[id] == null) {
has$1[id] = true;
if (!flushing) {
queue.push(watcher);
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
var i = queue.length - 1;
while (i >= 0 && queue[i].id > watcher.id) {
i--;
}
queue.splice(Math.max(i, index) + 1, 0, watcher);
}
// queue the flush
if (!waiting) {
waiting = true;
nextTick(flushSchedulerQueue);
}
}
}
至此的連續呼叫其實都正常,但是,這個東西把連續呼叫給中斷了,
if (has$1[id] == null) {
has$1[id] = true;
// ...中略
}
這個 has$1
在 Watcher
當中是屬於全域變數,要直到 flushSchedulerQueue
執行過了,或是 resetSchedulerState
執行之後才會被重新設定。那,感覺上應該是先執行了 setTick(1)
而 setTick(2)
被忽略才對,為何剛好相反?
喔,因為這個 watcher 並不是不可變的元件 (Immutable) 的關係,所以你知道的(以下省略一萬字。
小結
這年頭菜蟲不好當啊(欸