Simple implementation of observer and watcher for Vue

Keywords: Javascript Attribute Vue Google

Non-Ding Blind Cattle Series ~=. =

In daily project development, we pass js objects to the data option in the vue instance as the basis for updating the view. In fact, vue will traverse its attributes and set their get/set with Object.defineProperty, so that the attributes of data can respond to data changes:

 Object.defineProperty(obj, name, {
   // Put the value in the vm's _data attribute object first
   get() {
     // Features displayed when assigning values
   },
   set() {
     // What can we do when the value changes?
   }
 })

Next, we can use it to implement the simplest watcher. Since data attribute and callback attribute are indispensable to bind data to perform callback function, we define a VM object (vm object in vue is the root instance, it is global):

/**
 * @param {Object} _data Used to store data values
 * @param {Object} $data data Raw data object, current value
 * @param {Object} callback callback
 */
var vm = { _data: {}, $data: {}, callback: {} }

When setting a value, if it detects that the current value changes with the corresponding value stored in _data, it updates the value and executes a callback function. With get () & set () in the Object.definedProperty method, we can quickly achieve this function to _________

 vm.$watch = (obj, func) => {
    // callback
    vm.callback[ obj ] = func
    // Set data
    Object.defineProperty(vm.$data, obj, {
      // Put the value in the vm's _data attribute object first
      get() {
        return vm._data[ obj ]
      },
      set(val) {
        // Compare the original values, assign values if they are not equal, and execute callbacks
        if (val !== vm._data[ obj ]) {
          vm._data[ obj ] = val
          const cb = vm.callback[ obj ]
          cb.call(vm)
        }
      }
   })
}
vm.$watch('va', () => {console.log('Has been successfully monitored')})
vm.$data.va = 1

Although this small function has been initially implemented, the problem is that if the obj object is only a simple variable of value type, the above code can be satisfied; but if the obj object is a variable with a one-tier or even multi-tier tree structure, we can only listen to the changes in the outermost layer, that is, the obj itself, and the changes in internal attributes can not be monitored (no pairs are set). Set and get should be set for attributes, because the number of layers of attributes inside the object itself is unknown and can theoretically be infinite (which is not usually done), so let's use recursion to solve it here.

Let's first peel off the Object.defineProperty function, one is decoupling, the other is convenient for us to recursively ~

var defineReactive = (obj, key) => {
  Object.defineProperty(obj, key, {
    get() {
      return vm._data[key]
    },
    set(newVal) {
      if (vm._data[key] === newVal) {
        return
      }
      vm._data[key] = newVal
      const cb = vm.callback[ obj ]
      cb.call(vm)
    }
  })
}

Well, the recursion that we have said is not urgent. It just takes the functions with get and set functions away from it.
Now let's add recursion ~

var Observer = (obj) => {
  // Traverse so that each attribute in the object can be added get set
  Object.keys(obj).forEach((key) =>{
    defineReactive(obj, key)
  })
}

This is just traversal. To achieve recursion, you need to add a judgment when defining Reactive to determine whether the attribute is of object type. If so, you execute Observer itself ~we rewrite the defineReactive function.

// If you decide whether it is an object type, you should continue to execute yourself.
var observe = (value) => {
  // To determine whether it is an object type, continue executing Observer
  if (!value || typeof value !== 'object') {
    return
  }
  return new Observer(value)
}

// Place the observe method in the set of Object.defineProperty in defineReactive to form recursion
var defineReactive = (obj, key) => {
  // To determine whether val is an object, if the object has many layers of attributes, the code on this side will call itself continuously (because observation executes Observer again, while Observer executes defineReactive) until the last layer, starting from the last layer, executing the following code, returning layer by layer (which can be understood as the onion model), until the first layer, adding get/set to all attributes.
  var childObj = observe(vm._data[key])
  Object.defineProperty(obj, key, {
    get() {
      return vm._data[key]
    },
    set(newVal) {
      // Do nothing if the set values are exactly equal
      if (vm._data[key] === newVal) {
         return
      }
      // Unequal assignment
      vm._data[key] = newVal
      // CCCallFuncN
      const cb = vm.callback[ key ]
      cb.call(vm)
      // If the value set comes in is a complex type, recurse it, and add set/get
      childObj = observe(val)
    }
  })
}

Now let's sort it out and evolve the functional prototype we just started to implement.

var vm = { _data: {}, $data: {}, callback: {}}
var defineReactive = (obj, key) => {
  // In the beginning, it was not set value, so we had to do a set of observe s outside.
  // To determine whether val is an object, if the object has many layers of attributes, the code on this side will call itself continuously (because observation executes Observer again, while Observer executes defineReactive) until the last layer, starting from the last layer, executing the following code, returning layer by layer (which can be understood as the onion model), until the first layer, adding get/set to all attributes.
  var childObj = observe(vm._data[key])
  Object.defineProperty(obj, key, {
    get() {
      return vm._data[key]
    },
    set(newVal) {
      if (vm._data[key] === newVal) {
        return
      }
    // If the value changes, do something about it.
    vm._data[key] = newVal
    // CCCallFuncN
    const cb = vm.callback[ key ]
    cb.call(vm)
    // If the value set comes in is a complex type, recurse it, and add set/get
    childObj = observe(newVal)
    }
  })
}
var Observer = (obj) => {
  Object.keys(obj).forEach((key) =>{
    defineReactive(obj, key)
  })
}
var observe = (value) => {
  // To determine whether it is an object type, continue executing Observer
  if (!value || typeof value !== 'object') {
    return
  }
  Observer(value)
}
vm.$watch = (name, func) => {
  // callback
  vm.callback[name] = func
  // Set data
  defineReactive(vm.$data, name)
}
// Bind a, a to perform callback methods if changed
var va = {a:{c: 'c'}, b:{c: 'c'}}
vm._data[va] = {a:{c: 'c'}, b:{c: 'c'}}
vm.$watch('va', () => {console.log('Has been successfully monitored')})
vm.$data.va = 1

Paste the above code in the console of Google Browser, and then return to find that, as expected, VA itself has been monitored. Yes, let's try to see if the internal attributes of VA have been monitored. Change vm.$data.va = 1 to vm.$data.va.a = 1, and the result is wrong.

What the hell?

We carefully examined the code, WTF. It turns out that when we recurse, the key parameter of callback function cb in Object.defineProperty changes all the time. What we hope is that when the attributes inside are changed, the callback function we defined beforehand will be executed. Then we will change the method to pass in the callback that we defined at the beginning as a parameter to ensure that every layer. The callbacks of recursive set are all set in advance.

var vm = { _data: {}, $data: {}, callback: {}}
var defineReactive = (obj, key, cb) => {
  // In the beginning, it was not set value, so we had to do a set of observe s outside.
  var childObj = observe(vm._data[key], cb)
  Object.defineProperty(obj, key, {
    get() {
      return vm._data[key]
    },
    set(newVal) {
      if (vm._data[key] === newVal) {
        return
      }
      // If the value changes, do something about it.
      vm._data[key] = newVal
      // CCCallFuncN
      cb()
      // If the value set comes in is a complex type, recurse it, and add set/get
      childObj = observe(newVal)
    }
  })
}
var Observer = (obj, cb) => {
  Object.keys(obj).forEach((key) =>{
    defineReactive(obj, key, cb)
  })
}
var observe = (value, cb) => {
  // To determine whether it is an object type, continue executing Observer
  if (!value || typeof value !== 'object') {
    return
  }
  Observer(value, cb)
}
vm.$watch = (name, func) => {
  // callback
  vm.callback[name] = func
  // Set data
  defineReactive(vm.$data, name, func)
}
// Bind a, a to perform callback methods if changed
var va = {a:{c: 'c'}, b:{c: 'c'}}
vm._data.va = {a:{c: 'c'}, b:{c: 'c'}}
vm.$watch('va', () => {console.log('Successfully monitored again')})
vm.$data.va.a = 1

More than one more execution of the code found that the internal a attribute was also monitored, and when the value of the attribute changed, we executed our pre-defined callback function ~Hee-hee-hee-hee-hee-hee-hee-hee-hee-hee-hee-hee-hee-hee-

Although we have realized the basic function of $watch, we still have a certain distance from the source code of vue, especially some flattening and modularization ideas need to involve some design patterns. In fact, when we look at the source code, we often go against the author's thinking. Functions from simple to complex often involve the modularization and decoupling of the code, which makes the code very dispersed and read. To obscure and difficult to understand, do it yourself, from a small block of functional code to achieve, and then combined with source code, comparative thinking, slowly enrich, but also as a way to learn source code to

ps: If you read the error of this article or are advised by better optimization, please feel free to contact

Posted by skyer2000 on Fri, 12 Apr 2019 09:18:31 -0700