In vUE development, we can use Watch and computed to easily detect changes in data and make changes accordingly, but in a small program, you can only manually trigger this.setData() when data changes. How can we add these two functions to a small program?

We know that Object. DefineProperty is used in Vue to detect changes in data. By injecting all binding operations into setters for this variable, we can drive changes in other data as the variable changes. So can we apply this method to small programs?

In fact, it is easier to implement in applets than in Vue, because for data objects, Vue has to recursively bind each variable in the object to make it reactive. But in wechat applet, no matter the object or the basic type, can only be changed by this.setData(), so we only need to detect the change of the key value in the data, not the key value in the key value.

Start with the test code

<view>{{ test.a }}</view>
<view>{{ test1 }}</view>
<view>{{ test2 }}</view>
<view>{{ test3 }}</view>
<button bindtap="changeTest">change</button>
Copy the code
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)}})},})Copy the code

Now we’re going to implement watch and computed methods so that test1, test2, and test3 change when test changes, and we’re going to add a button that changes when test is clicked.

The watch method is relatively simple. First, we define a function to detect changes:

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
    },
  })
}
Copy the code

It then iterates over the object passed in by the Watch function, calling the method for each key

function watch(ctx, obj) {
  Object.keys(obj).forEach(key= > {
    defineReactive(ctx.data, key, ctx.data[key], function(value) {
      obj[key].call(ctx, value)
    })
  })
}
Copy the code

Here we have fn, which is the value of test in the watch method above, and we’re going to wrap that method in a layer and bind it to the context.

Now for computed, this is a little bit more complicated, because we don’t know which variable in data computed depends on, so we can only iterate through every variable in 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) = > {
    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)
}
Copy the code

To explain this code in detail, start by calling the defineReactive method for each attribute in data. Then calculate the value of each attribute in computed for the first time, which is test2 and test3 in the previous example.

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

Here we call the values of test2 and test3 separately, combine the return value with the corresponding key value into an object, and then call setData(), which evaluates both values for the first time, using the reduce method. But you might notice that these two lines of code, they don’t seem to say what they’re for.

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

Test2 and test3 depend on test, so you must call the corresponding functions of test2 and test3 in setter functions when test changes, and set these variables using setData. To do this, you need to change defineReactive.

function defineReactive(data, key, val, fn) {
  let subs = [] / / new
  Object.defineProperty(data, key, {
    configurable: true.enumerable: true.get: function() {
      / / new
      if (data.$target) {
        subs.push(data.$target)
      }
      return val
    },
    set: function(newVal) {
      if (newVal === val) return
      fn && fn(newVal)
      / / new
      if (subs.length) {
        // Use setTimeout because this.data has not been updated yet
        setTimeout((a)= > {
          subs.forEach(sub= > sub())
        }, 0)
      }
      val = newVal
    },
  })
}
Copy the code

With a few more lines of code than before, we declare a variable to hold all functions that need to be executed when they change, execute each function on set because the value of this.data.test has not changed yet, and use setTimeout to execute it on the next round. Now there is the question of how to add functions to subs. I don’t know if you remember the two lines of code we talked about in Reduce. Because test’s getter method is called when test1 and test2 perform their first computed values, this is a good time to inject the function into subs and declare a $target variable on data, And then you assign the function to that variable, so that in the getter you can determine if there is a target value on the data, so that you can push the subs. You need to set target to null immediately. Of course, that’s exactly how it works in VUE, except it’s a little less complicated.

So far you’ve implemented Watch and computed, but you’re not done. There’s a problem. When both are used together, the key of the Object in Watch also exists in data, so object.defineProperty is called repeatedly on that variable, overwriting the previous one. Because we don’t determine the order of calls in vue, we recommend writing computed first and then watch, so that we can watch the values in computed. So there’s a problem with computed inefficiency because of coverage.

Think about why?

Obviously, because the previous subs has been redeclared as an empty array. At this point, we thought that a simple solution would be to store the previously computed subs in one place and see if the corresponding key already has a subs the next time we call defineReactive, and that would solve the problem. Modify the code.

function defineReactive(data, key, val, fn) {
  let subs = data['$' + key] || [] / / new
  Object.defineProperty(data, key, {
    configurable: true.enumerable: true.get: function() {
      if (data.$target) {
        subs.push(data.$target)
        data['$' + key] = subs / / new
      }
      return val
    },
    set: function(newVal) {
      if (newVal === val) return
      fn && fn(newVal)
      if (subs.length) {
        // Use setTimeout because this.data has not been updated yet
        setTimeout((a)= > {
          subs.forEach(sub= > sub())
        }, 0)
      }
      val = newVal
    },
  })
}
Copy the code

In this way, we have implemented the required functionality step by step. For complete code and examples please click here.

Although some testing has been done, there is no guarantee that there are no other unknown bugs, so questions are welcome.