在微信小程序裏使用 watch 和 computed

在開發 vue 的時候,咱們可使用 watch 和 computed 很方便的檢測數據的變化,從而作出相應的改變,可是在小程序裏,只能在數據改變時手動觸發 this.setData(),那麼如何給小程序也加上這兩個功能呢?html

咱們知道在 vue 裏是經過 Object.defineProperty 來實現數據變化檢測的,給該變量的 setter 裏注入全部的綁定操做,就能夠在該變量變化時帶動其它數據的變化。那麼是否是能夠把這種方法運用在小程序上呢?vue

實際上,在小程序裏實現要比 vue 裏簡單,應爲對於 data 裏對象來講,vue 要遞歸的綁定對象裏的每個變量,使之響應式化。可是在微信小程序裏,無論是對於對象仍是基本類型,只能經過 this.setData() 來改變,這樣咱們只需檢測 data 裏面的 key 值的變化,而不用檢測 key 值裏面的 keygit

先上測試代碼github

<view>{{ test.a }}</view>
<view>{{ test1 }}</view>
<view>{{ test2 }}</view>
<view>{{ test3 }}</view>
<button bindtap="changeTest">change</button>
const { watch, computed } = require('./vuefy.js')
Page({
  data: {
    test: { a: 123 },
    test1: 'test1',
  },
  onLoad() {
    computed(this, {
      test2: function() {
        return this.data.test.a + '2222222'
      },
      test3: function() {
        return this.data.test.a + '3333333'
      }
    })
    watch(this, {
      test: function(newVal) {
        console.log('invoke watch')
        this.setData({ test1: newVal.a + '11111111' })
      }
    })
  },
  changeTest() {
    this.setData({ test: { a: Math.random().toFixed(5) } })
  },
})

如今咱們要實現 watch 和 computed 方法,使得 test 變化時,test一、test二、test3 也變化,爲此,咱們增長了一個按鈕,當點擊這個按鈕時,test 會改變。小程序

watch 方法相對簡單點,首先咱們定義一個函數來檢測變化:微信小程序

function defineReactive(data, key, val, fn) {
  Object.defineProperty(data, key, {
    configurable: true,
    enumerable: true,
    get: function() {
      return val
    },
    set: function(newVal) {
      if (newVal === val) return
      fn && fn(newVal)
      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)
    })
  })
}

這裏有參數是 fn ,即上面 watch 方法裏 test 的值,這裏把該方法包一層,綁定 context。微信

接着來看 computed,這個稍微複雜,由於咱們沒法得知 computed 裏依賴的是 data 裏面的哪一個變量,所以只能遍歷 data 裏的每個變量。dom

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) })
    }
    prev[next] = obj[next].call(ctx)
    ctx.data.$target = null
    return prev
  }, {})
  ctx.setData(firstComputedObj)
}

詳細解釋下這段代碼,首先給 data 裏的每一個屬性調用 defineReactive 方法。接着計算 computed 裏面每一個屬性第一次的值,也就是上例中的 test二、test3。函數

computed(this, {
  test2: function() {
    return this.data.test.a + '2222222'
  },
  test3: function() {
    return this.data.test.a + '3333333'
  }
})

這裏分別調用 test2 和 test3 的值,將返回值與對應的 key 值組合成一個對象,而後再調用 setData() ,這樣就會第一次計算這兩個值,這裏使用了 reduce 方法。可是你可能會發現其中這兩行代碼,它們好像都沒有被提到是幹嗎用的。

ctx.data.$target = function() {
    ctx.setData({ [next]: obj[next].call(ctx) })
  }
  
  ctx.data.$target = null

能夠看到,test2 和 test3 都是依賴 test 的,這樣必須在 test 改變的時候在其的 setter 函數中調用 test2 和 test3 中對應的函數,並經過 setData 來設置這兩個變量。爲此,須要將 defineReactive 改動一下。

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 (newVal === val) return
      fn && fn(newVal)
      // 新增
      if (subs.length) {
        // 用 setTimeout 由於此時 this.data 還沒更新
        setTimeout(() => {
          subs.forEach(sub => sub())
        }, 0)
      }
      val = newVal
    },
  })
}

相較於以前,增長了幾行代碼,咱們聲明瞭一個變量來保存全部在變化時須要執行的函數,在 set 時執行每個函數,由於此時 this.data.test 的值還未改變,使用 setTimeout 在下一輪再執行。如今就有一個問題,怎麼將函數添加到 subs 中。不知道各位仍是否記得上面咱們說到的在 reduce 裏的那兩行代碼。由於在執行計算 test1 和 test2 第一次 computed 值的時候,會調用 test 的 getter 方法,此刻就是一個好機會將函數注入到 subs 中,在 data 上聲明一個 $target 變量,並將須要執行的函數賦值給該變量,這樣在 getter 中就能夠判斷 data 上有無 target 值,從而就能夠 push 進 subs,要注意的是須要立刻將 target 設爲 null,這就是第二句的用途,這樣就達到了一石二鳥的做用。固然,這其實就是 vue 裏的原理,只不過這裏沒那麼複雜。

到此爲止已經實現了 watch 和 computed,可是還沒完,有個問題。當同時使用這二者的時候,watch 裏的對象的鍵也同時存在於 data 中,這樣就會重複在該變量上調用 Object.defineProperty ,後面會覆蓋前面。由於這裏不像 vue 裏能夠決定二者的調用順序,所以咱們推薦先寫 computed 再寫 watch,這樣能夠 watch computed 裏的值。這樣就有一個問題,computed 會因覆蓋而無效。

思考一下爲何?

很明顯,這時由於以前的 subs 被從新聲明爲空數組了。這時,咱們想一個簡單的方法就是把以前 computed 裏的 subs 存在一個地方,下一次調用 defineReactive 的時候看對應的 key 是否已經有了 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 // 新增
      }
      return val
    },
    set: function(newVal) {
      if (newVal === val) return
      fn && fn(newVal)
      if (subs.length) {
        // 用 setTimeout 由於此時 this.data 還沒更新
        setTimeout(() => {
          subs.forEach(sub => sub())
        }, 0)
      }
      val = newVal
    },
  })
}

這樣,咱們就一步一步的實現了所需的功能。完整的代碼和例子請戳

雖然通過了一些測試,但不保證沒有其它未知錯誤,歡迎提出問題。

相關文章
相關標籤/搜索