使用Vue逐步實現(xiàn)Watch屬性詳解
watch
對于watch的用法,在Vue文檔 中有詳細描述,它可以讓我們觀察data中屬性的變化。并提供了一個回調函數,可以讓用戶在屬性值變化后做一些事情。
watch對象中的value分別支持函數、數組、字符串、對象,較為常用的是函數的方式,當想要觀察一個對象以及對象中的每一個屬性的變化時,便會用到對象的方式。
下面是官方的一個例子,相信在看完之后就能對watch的幾種用法有大概的了解:
var vm = new Vue({
data: {
a: 1,
b: 2,
c: 3,
d: 4,
e: {
f: {
g: 5
}
}
},
watch: {
a: function (val, oldVal) {
console.log('new: %s, old: %s', val, oldVal)
},
// string method name
b: 'someMethod',
// the callback will be called whenever any of the watched object properties change regardless of their nested depth
c: {
handler: function (val, oldVal) { /* ... */ },
deep: true
},
// the callback will be called immediately after the start of the observation
d: {
handler: 'someMethod',
immediate: true
},
// you can pass array of callbacks, they will be called one-by-one
e: [
'handle1',
function handle2 (val, oldVal) { /* ... */ },
{
handler: function handle3 (val, oldVal) { /* ... */ },
/* ... */
}
],
// watch vm.e.f's value: {g: 5}
'e.f': function (val, oldVal) { /* ... */ }
}
})
vm.a = 2 // => new: 2, old: 1初始化watch
在了解了watch的用法之后,我們開始實現(xiàn)watch。

在初始化狀態(tài)initState時,會判斷用戶在實例化Vue時是否傳入了watch選項,如果用戶傳入了watch,就會進行watch的初始化操作:
// src/state.js
function initState (vm) {
const options = vm.$options;
if (options.watch) {
initWatch(vm);
}
}initWatch中本質上是為每一個watch中的屬性對應的回調函數都創(chuàng)建了一個watcher:
// src/state.js
function initWatch (vm) {
const { watch } = vm.$options;
for (const key in watch) {
if (watch.hasOwnProperty(key)) {
const userDefine = watch[key];
if (Array.isArray(userDefine)) { // userDefine是數組,為數組中的每一項分別創(chuàng)建一個watcher
userDefine.forEach(item => {
createWatcher(vm, key, item);
});
} else {
createWatcher(vm, key, userDefine);
}
}
}
}createWatcher中得到的userDefine可能是函數、對象或者字符串,需要分別進行處理:
function createWatcher (vm, key, userDefine) {
let handler;
if (typeof userDefine === 'string') { // 字符串,從實例上取到對應的method
handler = vm[userDefine];
userDefine = {};
} else if (typeof userDefine === 'function') { // 函數
handler = userDefine;
userDefine = {};
} else { // 對象,userDefine中可能會包含用戶傳入的deep,immediate屬性
handler = userDefine.handler;
delete userDefine.handler;
}
// 用處理好的參數調用vm.$watch
vm.$watch(key, handler, userDefine);
}createWatcher中對參數進行統(tǒng)一處理,之后調用了vm.$watch,在vm.$watch中執(zhí)行了Watcher的實例化操作:
export function stateMixin (Vue) {
// some code ...
Vue.prototype.$watch = function (exprOrFn, cb, options) {
const vm = this;
const watch = new Watcher(vm, exprOrFn, cb, { ...options, user: true });
};
}此時new Watcher時傳入的參數如下:
vm: 組件實例exprOrFn:watch選項對應的keycb:watch選項中key對應的value中提供給用戶處理邏輯的回調函數,接收key在data中的對應屬性的舊值和新值作為參數options:{user: true, immediate: true, deep: true},immediate和deep屬性當key對應的value為對象時,用戶可能會傳入
在Watcher中會判斷options中有沒有user屬性來區(qū)分是否是watch屬性對應的watcher:
class Watcher {
constructor (vm, exprOrFn, cb, options = {}) {
this.user = options.user;
if (typeof exprOrFn === 'function') {
this.getter = this.exprOrFn;
}
if (typeof exprOrFn === 'string') { // 如果exprFn傳入的是字符串,會從實例vm上進行取值
this.getter = function () {
const keys = exprOrFn.split('.');
// 后一次拿到前一次的返回值,然后繼續(xù)進行操作
// 在取值時,會收集當前Dep.target對應的`watcher`,這里對應的是`watch`屬性對應的`watcher`
return keys.reduce((memo, cur) => memo[cur], vm);
};
}
this.value = this.get();
}
get () {
pushTarget(this);
const value = this.getter();
popTarget();
return value;
}
// some code ...
}這里有倆個重要的邏輯:
- 由于傳入的
exprOrFn是字符串,所以this.getter的邏輯就是從vm實例上找到exprOrFn對應的值并返回 - 在
watcher實例化時,會執(zhí)行this.get,此時會通過this.getter方法進行取值。取值就會觸發(fā)對應屬性的get方法,收集當前的watcher作為依賴 - 將
this.get的返回值賦值給this.value,此時拿到的就是舊值
當觀察的屬性值發(fā)生變化后,會執(zhí)行其對應的set方法,進而執(zhí)行收集的watch對應的watcher的update方法:
class Watcher {
// some code ...
update () {
queueWatcher(this);
}
run () {
const value = this.get();
if (this.user) {
this.cb.call(this.vm, value, this.value);
this.value = value;
}
}
}和渲染watcher相同,update方法中會將對應的watch watcher去重后放到異步隊列中執(zhí)行,所以當用戶多次修改watch屬性觀察的值時,并不會不停的觸發(fā)對應watcher 的更新操作,而只是以它最后一次更新的值作為最終值來執(zhí)行this.get進行取值操作。
當我們拿到觀察屬性的最新值之后,執(zhí)行watcher中傳入的回調函數,傳入新值和舊值。
下面畫圖來梳理下這個過程:

deep、immdediate屬性
當用戶傳入immediate屬性后,會在watch初始化時便立即執(zhí)行對應的回調函數。其具體的執(zhí)行位置是在Watcher實例化之后:
Vue.prototype.$watch = function (exprOrFn, cb, options) {
const vm = this;
const watcher = new Watcher(vm, exprOrFn, cb, { ...options, user: true });
if (options.immediate) { // 在初始化后立即執(zhí)行watch
cb.call(vm, watcher.value);
}
};此時watcher.value是被觀察的屬性當前的值,由于此時屬性還沒有更新,所以老值為undefined。
如果watch觀察的屬性為對象,那么默認對象內的屬性更新,并不會觸發(fā)對應的回調函數。此時,用戶可以傳入deep選項,來讓對象內部屬性更新也調用對應的回調函數:
class Watcher {
// some code ...
get () {
pushTarget(this);
const value = this.getter();
if (this.deep) { // 繼續(xù)遍歷value中的每一項,觸發(fā)它的get方法,收集當前的watcher
traverse(value);
}
popTarget();
return value;
}
}當用戶傳入deep屬性后,get方法中會執(zhí)行traverse方法來遍歷value中的每一個值,這樣便可以繼續(xù)觸發(fā)value中屬性對應的get方法,為其收集當前的watcher作為依賴。這樣在value 內部屬性更新時,也會通知其收集的watch watcher進行更新操作。
traverse的邏輯只是遞歸遍歷傳入數據的每一個屬性,當遇到簡單數據類型時便停止遞歸:
// traverse.js
// 創(chuàng)建一個Set,遍歷之后就會將其放入,當遇到環(huán)引用的時候不會行成死循環(huán)
const seenObjects = new Set();
export function traverse (value) {
_traverse(value, seenObjects);
// 遍歷完成后,清空Set
seenObjects.clear();
}
function _traverse (value, seen) {
const isArr = Array.isArray(value);
const ob = value.__ob__;
// 不是對象并且沒有被觀測過的話,終止調用
if (!isObject(value) || !ob) {
return;
}
if (ob) {
// 每個屬性只會有一個在Observer中定義的dep
const id = ob.dep.id;
if (seen.has(id)) { // 遍歷過的對象和數組不再遍歷,防止環(huán)結構造成死循環(huán)
return;
}
seen.add(id);
}
if (isArr) {
value.forEach(item => {
// 繼續(xù)遍歷數組中的每一項,如果為對象的話,會繼續(xù)遍歷數組的每一個屬性,即對對象屬性執(zhí)行取值操作,收集watch watcher
_traverse(item, seen);
});
} else {
const keys = Object.keys(value);
for (let i = 0; i < keys.length; i++) {
// 繼續(xù)執(zhí)行_traverse,這里會對 對象 中的屬性進行取值
_traverse(value[keys[i]], seen);
}
}
}需要注意的是,這里利用Set來存儲每個屬性對應的dep的id。這樣當出現(xiàn)環(huán)時,Set中已經存儲過了其對應dep的id,便會終止遞歸。
結語
本文一步步實現(xiàn)了Vue的watch屬性,并對內部的實現(xiàn)邏輯提供了筆者相應的理解 。到此這篇關于使用Vue逐步實現(xiàn)Watch屬性詳解的文章就介紹到這了,更多相關Vue Watch屬性內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
vue2+element-ui使用vue-i18n進行國際化的多語言/國際化詳細教程
這篇文章主要給大家介紹了關于vue2+element-ui使用vue-i18n進行國際化的多語言/國際化的相關資料,I18n是Vue.js的國際化插件,項目里面的中英文等多語言切換會使用到這個東西,需要的朋友可以參考下2023-12-12
VUE?html5-qrcode實現(xiàn)H5掃一掃功能實例
這篇文章主要給大家介紹了關于VUE?html5-qrcode實現(xiàn)H5掃一掃功能的相關資料,html5-qrcode是輕量級和跨平臺的QR碼和條形碼掃碼的JS庫,集成二維碼、條形碼和其他一些類型的代碼掃描功能,需要的朋友可以參考下2023-08-08

