小程序里的自定義組件里是有數據監聽器的,可以監聽對應數據的變化來執行callBack,但是頁面Page里沒有對應的api就顯的很生硬,比如某個數據變了(如切換城市)需要重新刷頁面,如果不做監聽,每次都要在數據變化的地方手動去調一次函數。
那么如何像vue那樣在Page里實現 watch 和 computed 呢 ?如果這時候你腦子里能想到 Obejct.defineProperty 或者 Proxy 那么接下來就慢慢實現吧。
先曬出是這樣調用的,請牢記這個調用,后面會反復提到 test2 test3 currentCity:
this.$computed(this, { test2: function() { return this.data.currentCity.cityID + '2222222' }, test3: function() { return this.data.currentCity.cityID + '3333333' } }) this.$watch(this, { currentCity(city) { console.log('回調傳值',city) if (city.cityID) { this.getHotSpotList() } } })
第一步,先定義一個函數來檢測對應屬性的變化,每當setter,getter的時候會觸發。
function defineReactive(data, key, val, fn) { Object.defineProperty(data, key, { configurable: true, enumerable: true, get: function() {
// 2020.01.06補充 針對對象數組等復雜類型的數據,需要做深拷貝處理,deepClone可自行構造
// 不做深拷貝就切不斷關聯,在取this.data.xx的時候會觸發get屬性,
return deepClone(val) }, set: function(newVal) { if (val == newVal) return val = newVal }, }) }
先實現watch ,簡單,把傳入對象的每個屬性都監測屬性變化
function watch(ctx, obj) { Object.keys(obj).forEach(key => { defineReactive(ctx.data, key, ctx.data[key], function(value) { obj[key].call(ctx, value) }) }) }
上面的方法defineReactive需要稍微改造一下,在set改變值的時候,執行回調函數 fn,且set里新舊值的對比要考慮復雜類型的對比,直接引入lodash的isEqual 方法來對比
function defineReactive(data, key, val, fn) { Object.defineProperty(data, key, { configurable: true, enumerable: true, get: function() {
// 2020.01.06補充 針對對象數組等復雜類型的數據,需要做深拷貝處理,deepClone可自行構造
// 不做深拷貝就切不斷關聯,在取this.data.xx的時候會觸發get屬性,
return deepClone(val)
}, set: function(newVal) { if (_.isEqual(val,newVal)) return fn && fn(newVal) val = newVal }, }) }
接下來實現 computed,這個會比較麻煩點,有幾個注意的地方,1:需要把computed初始的時候傳進來的屬性算出值並放在this.data里,跟vue是一樣的原理。2:每個傳進來的屬性值都要進行遍歷監聽變化。
function computed(ctx, obj) { let keys = Object.keys(obj) let dataKeys = Object.keys(ctx.data) dataKeys.forEach(dataKey => { defineReactive(ctx.data, dataKey, ctx.data[dataKey]) }) }
基於上面的,我們要補充實現剛才提到的第一點,算出computed對應屬性的初始值並設在this.data里
function computed(ctx, obj) { let keys = Object.keys(obj) let dataKeys = Object.keys(ctx.data) dataKeys.forEach(dataKey => { defineReactive(ctx.data, dataKey, ctx.data[dataKey]) }) let firstComputedObj = keys.reduce((prev, next) => { prev[next] = obj[next].call(ctx) return prev }, {}) ctx.setData(firstComputedObj) }
但是現在有個問題,test2 test3 的初始值都算出來了,但后續如果this.data.currentCity變化的時候,test2,test3對應的也要計算出新的值的,這樣才是實現了所謂的computed。
那么該如何去處理呢?我們就需要抓住一個時機,當currentCity變化的時候會觸發 set,這個時候應該觸發一些機制去更新test2,test3.
請注意上面的這行代碼:prev[next] = obj[next].call(ctx)
請看obj[next].call(ctx) 調的就是test2,test3對應的function並執行函數,這個時候函數內部的this.data.currentCity 會觸發到 get ,就是這個時機,我們能完美的把所有跟currentCity屬性相關的其他屬性關聯到一起。
這個時候觸發了get,我們何不把對應的函數記下來,在set的時候去調用,這樣就能做到currentCity變化的時候 test2 test3也同步變化。思路大致有了,接下來看代碼:
computed 大致如下:
function computed(ctx, obj) { let keys = Object.keys(obj) let dataKeys = Object.keys(ctx.data) dataKeys.forEach(dataKey => { defineReactive(ctx.data, dataKey, ctx.data[dataKey]) }) let firstComputedObj = keys.reduce((prev, next) => { ctx.data.$target = function() { ctx.setData({ [next]: obj[next].call(ctx) }) } // obj[next].call(ctx) 執行的時候會觸發該函數執行,函數內部的this.data相關屬性的調用會觸發defineReactive.get
prev[next] = obj[next].call(ctx) ctx.data.$target = null
return prev }, {}) ctx.setData(firstComputedObj) }
defineReactive 函數,上面說過在觸發currentCity get的時候要記下 test2 test3對應的函數,到了set的時候再去執行,起到cuerrentCity變化的時候,test2,test3 也能同步變化。
上面的 ctx.data.$target 稍微 funtion 后立馬再經過 prev[next] = obj[next].call(ctx) 這一句之后,又恢復為null,可能會有點疑惑,上面提過的,你需要注意 prev[next] = obj[next].call(ctx) 中 obj[next] 會觸發 test2 test3的 函數,函數里的 this.data.currentCity 會觸發自己的get,這個時候我們來把 test2 test3 和 currentCity 關聯,在currentcity set的時候,去跟新 test2 test3的值。
defineReactive 的代碼需要加個處理,記下test2 test3的處理函數
function defineReactive(data, key, val, fn) { let subs = [] Object.defineProperty(data, key, { configurable: true, enumerable: true, get: function() { if (data.$target) { subs.push(data.$target) } return val }, set: function(newVal) { if (_.isEqual(newVal,val)) return fn && fn(newVal) if (subs.length) { subs.forEach(sub => sub()) } val = newVal }, }) }
這樣處理下來,大致基本實現了,接下來需要處理幾個坑點,如果fn函數里有取this.data,可能currentCity仍舊是舊的值,明明set里的是新的值,這個涉及到了this.setData異步的問題,咱們需要加個處理。
function defineReactive(data, key, val, fn) { let subs = [] Object.defineProperty(data, key, { configurable: true, enumerable: true, get: function() { if (data.$target) { subs.push(data.$target) } return val }, set: function(newVal) { if (_.isEqual(newVal,val)) return
// 經過試驗,這里的觸發要早於setData的回調
// fn && fn(newVal) // 可能setData異步 還沒及時完成,newVal 是新的,但是this.data里還是舊的
//這樣watch 里去調用對應的方法,可能取的this.data就不是新的
// 如果fn取的是函數形參,那么可以不用setTimeout,但如果是函數里取得this.data就需要
setTimeout(() => { // 這時候已經完成了setData,fn里取this.data就是最新的
fn && fn(newVal) }, 0) if (subs.length) { // 用 setTimeout 因為此時 this.data 還沒更新
// 涉及到微任務,宏任務
setTimeout(() => { subs.forEach(sub => sub()) }, 0) // 跟上面那個setTimeout一樣,如果函數里用到了this.data,就需要加setTimeout
} val = newVal }, }) }
解決完異步的問題,還需要再注意一點:我們在Page里先寫了 computed 然后寫了 個 watch ,由於 computed初始化完成之后,如上面的 test2 test3 已經添加到 this.data里了,那么在 watch里咱們可以直接對 test2 test3 進行 監聽,看上去是挺完美的,但是看 defineReactive 的代碼 咱們應該注意,如果由於每次 執行defineReactive subs都是會置空的,那么 computed 就會失效, this.data.currentCity 變化的時候,對應的 test2 test3 的值就得不到更新,因為 subs 都被清空了,currentCity 觸發set的時候,subs是空的,很尷尬。。。
那么如何保證 subs 不被清空呢? 咱們只能找個地方記下來,最好跟屬性名相關聯。
function defineReactive(data, key, val, fn) { let subs = data['$' + key] || [] Object.defineProperty(data, key, { configurable: true, enumerable: true, get: function() { if (data.$target) { subs.push(data.$target) data['$' + key] = subs } //val 形成局部作用域保存在函數內部,set的時候會改變該值,所以一直能返回對應的屬性值
return val }, set: function(newVal) { // === 不適用判斷復雜類型,所以這里引用lodash中的 isEqual 方法
if (_.isEqual(newVal,val)) return
// console.log('觸發set',newVal, new Date().getTime())
// 經過試驗,這里的觸發要早於setData的回調
// fn && fn(newVal) // 可能setData異步 還沒及時完成,newVal 是新的,但是this.data里還是舊的
//這樣watch 里去調用對應的方法,可能取的this.data就不是新的
// 如果fn取的是函數形參,那么可以不用setTimeout,但如果是函數里取得this.data就需要
setTimeout(() => { // 這時候已經完成了setData,fn里取this.data就是最新的
fn && fn(newVal) }, 0) if (subs.length) { // 用 setTimeout 因為此時 this.data 還沒更新
// 涉及到微任務,宏任務
setTimeout(() => { subs.forEach(sub => sub()) }, 0) // 跟上面那個setTimeout一樣,如果函數里用到了this.data,就需要加setTimeout
} val = newVal }, }) }
到這里,我們算是完成了 computed 和 watch 的實現了。最好把這兩個方法綁定到每個page ,這個過程只要進行mixin就好了,大致思路是對小程序的 Page 對象和 mixin 進行 assign
后續有時間會寫一下小程序整個Page的封裝改造!!!