深入理解vue的watch
深入理解vue的watch
vue中的wactch可以監聽到data的變化,執行定義的回撥,在某些場景是很有用的,本文將深入原始碼揭開watch額面紗
- 前言
- watch的使用
- watch的多種使用方式
- 傳值函式
- 傳值陣列
- 傳值字串
- 傳值物件
- 傳值物件的其他作用
- 原始碼分析watch
- 初始watch
- 建立Watcher
- watchWatcher
- 立即執行的watch
- 與computed比較
前言
- version: v2.5.17-beta.0
- 閱讀本文需讀懂vue資料驅動部分
watch的使用
當改變data值,同時會引發副作用時,可以用watch。比如:有一個頁面有三個使用者行為會觸發this.count的改變,當this.count改變,需要重新獲取list值,這時候就可以用watch輕鬆完成
new Vue({ el: '#app', data: { count: 1, list: [] }, watch: { // 不管多少地方改變count,都會執行到這裡去改變list的值 count(val) { ajax(val).then(list => { this.list = list; }) } }, methods: { // 點選+1,count + 1,重新整理列表 handleClick() { this.count += 1; }, // 點選重置,count = 1,重新整理列表 handleReset() { this.count = 1; }, // 點選隨機, count隨機數,重新整理列表 handleRamdon() { this.count = Math.ceil(Math.random() * 10); } } })
這樣的好處就是把所有源頭聚集到了watch中,不需要在多個count改變的地方手動去呼叫方法,減少程式碼冗餘。
watch的多種使用方式
watch的寫法有多種,以上案例是最常見的一種方法,接下來介紹所有寫法。
傳值函式
new Vue({
data: {
count: 1
},
watch: {
count() {
console.log('count改變')
}
}
})
最常見的寫法,count改變時將會觸發傳值的回撥函式
傳值陣列
new Vue({ data: { count: 1 }, watch: { count: [ () => { console.log('count改變') }, () => { console.log('count watch2') } ] } })
傳陣列,count改變後會依次執行陣列內每一個回撥函式
傳值字串
new Vue({
data: {
count: 1
},
watch: {
count: 'handleChange'
},
methods: {
handleChange(val) {
console.log('count改變了')
}
}
})
我們也可以傳值字串handleChange,然後在methods寫handleChange函式的邏輯,同樣可以做到count改變執行handleChange
傳值物件
new Vue({
data: {
count: 1
},
watch: {
count: {
handler() {
console.log('count改變')
}
}
}
})
可以傳值物件,該物件包含一個handler函式,當count改變時,會執行此handler函式,為什麼多此一舉需要包裝一層物件呢?存在即合理,是有其特殊作用的。
傳值物件的其他作用
watch為監聽屬性的變化,呼叫回撥函式,因此,在初始化時,並不會觸發,在初始化後屬性改變才觸發,如果想要初始時也要觸發watch,那就需要傳值物件,如下:
new Vue({
data: {
count: 1
},
watch: {
count: {
immediate: true, // 加此屬性
handler() {
console.log('count改變')
}
}
}
})
傳的物件有immediate屬性為true,則watch會立刻觸發。
原始碼分析watch
本節進行原始碼分析,探索watch的真面貌
初始watch
// 初始化
function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.data) {
initData(vm);
}
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
// watch初始化
function initWatch (vm, watch) {
for (var key in watch) {
var handler = watch[key];
if (Array.isArray(handler)) {
for (var i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
} else {
createWatcher(vm, key, handler);
}
}
}
從vue的執行流程,讀到了initWatch函式,此函式的用法很清晰,將傳入的每一個watch屬性執行createWatcher
處理。如果傳值是陣列,則遍歷去呼叫。
下面看一下createWatcher函式
function createWatcher (
vm,
expOrFn,
handler,
options
) {
// 如果是物件,則處理
if (isPlainObject(handler)) {
// 將物件快取,給$watch函式
options = handler;
handler = handler.handler;
}
if (typeof handler === 'string') {
handler = vm[handler];
}
return vm.$watch(expOrFn, handler, options)
}
createWatcher中做了相容處理:
- 如果handler是個物件,則進行一步轉換;
- 如果handler是字串,則取vue例項的方法(methods裡宣告)
- 最後呼叫例項的$watch方法
建立Watcher
Vue.prototype.$watch = function (
expOrFn,
cb,
options
) {
var vm = this;
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {};
options.user = true;
var watcher = new Watcher(vm, expOrFn, cb, options);
if (options.immediate) {
cb.call(vm, watcher.value);
}
return function unwatchFn () {
watcher.teardown();
}
};
vm.$watch裡是最終實現watch的部分,在這裡仍然做了相容判斷,如果是物件,回撥createWatcher;接下來就最重要的new Watcher。
$watch的功能其實就是new了一個Watcher,那麼,我們在程式碼裡實現的一切響應,都來自於Watcher,下面看一下watch裡的Watcher
watchWatcher
Watcher是vue資料驅動核心部分的一員,他承載著依賴收集與事件的觸發。下面重點解讀一下watch的Watcher實現。
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
// parsePath去解析expOrFn並返回getter函式
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = function () {};
}
}
watch Watcher會執行上面部分,parsePath
原始碼可自行檢視,他會將obj.a
這種寫法相容, 最終是返回需要監聽的屬性的getter函式
if (this.computed) {
this.value = undefined;
this.dep = new Dep();
} else {
// 執行get方法
this.value = this.get();
}
拿到getter後,會執行this.get方法:
Watcher.prototype.get = function get () {
// 加入Dep.target
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm); // 執行
} catch (e) {
} finally {
popTarget();
this.cleanupDeps();
}
return value
};
以上為get方法,內容簡單,但是做的事情舉足輕重,他不僅做了值的獲取,還做了依賴收集。
pushTarget會將當前watchWatcher賦值到Dep.target中,然後執行this.getter函式,要監聽的屬性如count會觸發他的get鉤子,與此同時會進行收集依賴,收集到的依賴就是前面Dep.target也就是當前的watchWatcher
正因為有上面的依賴收集,使count屬性有了此watchWatcher的依賴,當this.count改變時,會觸發set鉤子,進行事件分發,從而執行回撥函式
Watcher.prototype.getAndInvoke = function getAndInvoke (cb) {
var value = this.get();
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
// set new value
var oldValue = this.value;
this.value = value;
this.dirty = false;
if (this.user) {
try {
cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
}
} else {
cb.call(this.vm, value, oldValue);
}
}
};
上面就是this.count改變時,最終呼叫的方法,在這裡會執行this.cb,也就是定義的watch的回撥函式,會把value/oldValue傳遞過去
立即執行的watch
前面說到,watch只會在監聽的屬性改變值後才會觸發回撥,在初始化時不會執行回撥,如果想要一開始初始化就執行回撥,需要傳參物件,並immediate為true,實現原理已經在建立Watcher貼出來了
if (options.immediate) {
cb.call(vm, watcher.value);
}
建立watcher時,如果immediate為真值,會直接執行回撥函式
與computed比較
computed是計算屬性,watch是監聽屬性變化,有些場景計算屬性做的事情,watch也可以做,當然要儘量用computed去做,為什麼?
new Vue({
data: {
num: 1,
sum: 2
},
watch: {
num(val) {
this.sum = val + 1;
}
}
})
watch實現需要宣告2個data屬性num 和 sum,2個都會加入資料驅動,當num改變後,num和sum都觸發了set鉤子。
而computed不會,computed只會觸發num的set鉤子,因為sum根本沒有宣告,num改變後是動態計算出來的