Vue 提供了一種通用的方式來觀察和響應 Vue 實例上的數據變動:監聽屬性 watch。
雖然watch的濫用會導致性能不佳,但在一些情況下我們還是需要watch,使得代碼更加簡潔、邏輯更加清晰(其實就是嫌麻煩...)。
接下來我將逐步講解微信小程序中如何實現一個監聽器 watch,若想直接看最終代碼,可直接滑動至底部。
監聽器的原理,將data中需監聽的屬性寫在watch對象中,並給其提供一個方法,當被監聽屬性的值改變時,調用該方法。
所以很顯然,我們需要用到Javascript中的Object.defineProperty()方法,來手動劫持對象的getter/setter,從而實現給對象賦值時(調用setter),執行watch對象中相對應的函數,達到監聽效果。Object.defineProperty()不在這里詳細介紹,還不會使用的童鞋速戳這里-> Object.defineProperty()介紹。
首先,既然是微信小程序自定義watch屬性,我建議直接將代碼寫在app.js內,需要使用的頁面直接在onLoad()內調用getApp().setWatch(...)即可
// app.js /** * 小程序初始化 */ onLaunch(){...} /** * 設置監聽器 */ setWatcher(data, watch) { // 接收index.js傳過來的data對象和watch對象 Object.keys(watch).forEach(v => { // 將watch對象內的key遍歷 this.observe(data, v,watch[v]); // 監聽data內的v屬性,傳入watch內對應函數以調用 }) }, /** * 監聽屬性 並執行監聽函數 */ observe(obj, key,watchFun) { var val = obj[key]; // 給該屬性設默認值 Object.defineProperty(obj, key, { configurable: true, enumerable: true, set: function(value) { val = value; watchFun(value,val); // 賦值(set)時,調用對應函數 }, get: function() { return val; } }) } 此時在index.js中已經可以實現簡單的屬性監聽: //index.js Page({ data: { name:"xuyang" }, onLoad(){ getApp().setWatcher(this.data, this.watch); // 設置監聽器 this.setData({ name:'lxm' }) }, watch:{ name:function(newValue){ console.log(newValue); // name改變時,調用該方法輸出新值。 } } })
我們可以看到,index頁面啟動后,name被賦值成'lxm',同時觸發其setter,調用watch內的name()方法,控制台打印出 lxm。
是不是挺簡單的呢?掌握了這一點,接下來的事情就好辦多了。
我們知道,Vue中 watch對象內不光是能寫name、age這類屬性,還能用引號寫出'my.name'、'my.age'這類屬性,即對my對象下的name、age進行監聽。 我們只需要改進一下setWatcher方法:
/** * 設置監聽器 */ setWatcher(data, watch) { Object.keys(watch).forEach(v => { let key = v.split('.'); // 將watch中的屬性以'.'切分成數組 let nowData = data; // 將data賦值給nowData for (let i = 0; i < key.length - 1; i++) { // 遍歷key數組的元素,除了最后一個! nowData = nowData[key[i]]; // 將nowData指向它的key屬性對象 } let lastKey = key[key.length-1]; // 假設key==='my.name',此時nowData===data['my']===data.my,lastKey==='name' this.observe(nowData, lastKey,watch[v]); // 監聽nowData對象的lastKey }) } 此時在index.js中可以這樣寫: //index.js Page({ data: { my:{ name:"xuyang", age:21 } }, onLoad(){ getApp().setWatcher(this.data, this.watch); this.data.my.name = 'lxm'; this.data.my.age = 2; this.setData({ my: this.data.my }) }, watch:{ 'my.name':function(newValue){ console.log(newValue); }, 'my.age':function(newValue){ console.log(newValue); } } })
控制台會打印出 lxm 和 2
現在我們又離成功更進了一步~ 接下來我們要實現Vue中的深度監聽,即監聽my對象即可監聽它的內部所有屬性以及屬性的屬性以及屬性的屬性的屬性......並且監聽函數的this要指向這個page里的this。
需要深度監聽,Vue中的寫法是
watch:{ my:{ handler(newValue){ // do something... }, deep:true // 深度監聽 } } 我們需要修改一下app.js中的兩個函數: /** * 設置監聽器 */ setWatcher(page) { let data = page.data; let watch = page.watch; Object.keys(watch).forEach(v => { let key = v.split('.'); // 將watch中的屬性以'.'切分成數組 let nowData = data; // 將data賦值給nowData for (let i = 0; i < key.length - 1; i++) { // 遍歷key數組的元素,除了最后一個! nowData = nowData[key[i]]; // 將nowData指向它的key屬性對象 } let lastKey = key[key.length - 1]; // 假設key==='my.name',此時nowData===data['my']===data.my,lastKey==='name' let watchFun = watch[v].handler || watch[v]; // 兼容帶handler和不帶handler的兩種寫法 let deep = watch[v].deep; // 若未設置deep,則為undefine this.observe(nowData, lastKey, watchFun, deep, page); // 監聽nowData對象的lastKey }) }, /** * 監聽屬性 並執行監聽函數 */ observe(obj, key, watchFun, deep, page) { var val = obj[key]; // 判斷deep是true 且 val不能為空 且 typeof val==='object'(數組內數值變化也需要深度監聽) if (deep && val != null && typeof val === 'object') { Object.keys(val).forEach(childKey=>{ // 遍歷val對象下的每一個key this.observe(val,childKey,watchFun,deep,page); // 遞歸調用監聽函數 }) } var that = this; Object.defineProperty(obj, key, { configurable: true, enumerable: true, set: function(value) { // 用page對象調用,改變函數內this指向,以便this.data訪問data內的屬性值 watchFun.call(page,value,val); // value是新值,val是舊值 val = value; if(deep){ // 若是深度監聽,重新監聽該對象,以便監聽其屬性。 that.observe(obj, key, watchFun, deep, page); } }, get: function() { return val; } }) } 其中需要特別注意,若是data中先聲明一個對象userInfo = {},再給userInfo賦值一個對象(比如從服務器獲取用戶數據),則需要重新遍歷監聽才能實現深度監聽,故需加上這段代碼: if(deep){ // 若是深度監聽,重新監聽該對象,以便監聽其屬性。 that.observe(obj, key, watchFun, deep); } 此時index.js中需要深度監聽的屬性,可以這樣寫: //index.js Page({ data: { my: { name: 'xuyang', age: 21, hobby: ['girls', 'games'] }, nameInfo:{} }, onLoad() { getApp().setWatcher(this); this.data.my.hobby[0] = 'study'; this.setData({ nameInfo:{name:'haha',sex:'boy'} }) console.log(this.data) }, watch: { my:{ handler(newValue) { console.log(newValue); }, deep:true } } })