1. The nature of responsiveness

When I talk about Vue responsiveness, I usually mean that the view updates as the data changes. The development benefit is that when a view rendering needs to be updated, you only need to modify the data needed for the view rendering, rather than manually manipulating the DOM. In terms of implementation, it can be divided into two parts:

  • Listen for data changes
  • Update the view

We are familiar with listening for user events such as mouse clicks and keyboard inputs, but it is rare to listen directly for a data change event. Although there is no such thing as a data change event, it is possible to listen for data changes and, from a programming point of view, is not fundamentally different from binding a callback function to the event.

To compare the difference between listening for normal events and listening for data changes, we first use the event approach to implement “reactive” view updates.

In the following code, we define the data variable data and the view update function update. When updating a view, the update function reads the text property of data as the text content of the view node. It then listens for an input event on an input element, replaces the current value of data.text with the value entered by the user, and calls the update function to tell the view to update.

If the input does not take effect, please click here

<input id='text' />
<div id='app'></div>
<script>
  /* Define render data and view update functions */
  var data = {
    text: 'hello'
  }
  function update() {
    document.getElementById('app').textContent = data.text
  }
  update()
  /* Bind the input event to update the view after modifying the data */
  var textElm = document.getElementById('text')
  textElm.value = data.text
  textElm.addEventListener('input'.function() {
    data.text = this.value
    update()
  })
</script>
Copy the code

With input events, we are indirectly “reactive,” but it only acts as a link and does not directly respond to changes in data.

2. Monitor data changes

2.1 Object.defineProperty

Object.defineproperty (obj, prop, Descriptor) can add or modify existing attributes to objects. The function takes three arguments:

  • Obj: Object for which attributes are to be defined
  • Prop: The name of the property to be defined or modifiedStringorSymboltype
  • Descriptor: Property descriptor to define or modify, which must beObjecttype

The important thing to know here is the property descriptor object descriptor. Descriptor supports the following fields:

  • configurable: BooleanfortrueTo change the property descriptor and delete the property
  • enumerable: BooleanfortrueWhen, you can passfor ... inObject.keysMethods the enumeration
  • value: Indicates the value of the property. Can be any valid JavaScript value
  • writable: Booleanfortrue, the property value, i.evalueCan be changed by the assignment operator
  • get: The getter function for the property that is called when the property is accessed
  • set: setter function for the property, which is called when the property value is modified

Value and writable can only appear in data descriptors. Get and set can only appear in access descriptors. A property descriptor can only be one or the other, so when you define value or writable, you can’t define get or set, Cannot both specify accessors and a value or writable attribute. And vice versa.

Since we need to be notified when an object property changes, I need to use the access descriptor to define object properties, that is, to define a set in response to a property value change, and to define a GET in response to a property access.

In the data example above, we want to update the view when the value of the property of the object is changed by data.text = XXX, so we need to redefine the descriptor of the property of text and call the view update function in the set function. We also need to define get, because not only do I need to respond when the property value changes, but in the update function we also need to read the value of data.text, which would be undefined if WE didn’t define get.

var data = {
  text: 'hello'
}

var text = data.text

Object.defineProperty(data, 'text', {
  get: function() {
    return text
  },
  set: function(newValue) {
    if(text ! == newValue) { text = newValue update() } } })Copy the code

With this definition, we can directly modify the data.text value to update the view. The reader can save the following complete code to an HTML file and view the changes in the view in the browser console by assigning data.text = ‘world’.

<div id='app'></div>
<script>
  /* Define render data and view update functions */
  var data = {
    text: 'hello'
  }
  function update(a) {
    document.getElementById('app').textContent = data.text
  }
  update()
  /* Use object.defineProperty for reactive view updates */
  var text = data.text
  Object.defineProperty(data, 'text', {
    get: function(a) {
      return text
    },
    set: function(newValue) {
      if(text ! == newValue) { text = newValue update() } } })</script>
Copy the code

Here we just define the response for the text property of data. To make the code more generic and applicable to any object, you can write a function defineReactive(obj, key, update)(the function name refers to the definition of Vue2 and readers can search for this function in the Vue2 source code).

function defineReactive(obj, key, update) {
  var value = obj[key]
  Object.defineProperty(obj, key, {
    get: function() {
      return value
    },
    set: function(newValue) {
      if(value ! == newValue) { value = newValue update() } } })return obj
}
Copy the code

So the above code can be rewritten as:

var data = {
  text: 'hello'
}
function update() {
  document.getElementById('app').textContent = data.text
}
update()

defineReactive(data, 'text', update)
Copy the code

2.2 Proxy

In response to Object attribute changes, in addition to Object.definProperty, browsers also support another global Proxy constructor, which is used to customize the basic operations of objects, such as property lookup, assignment, enumeration, function calls, etc. In contrast, the former can only customize access and assignment of object attributes.

Use Proxy as follows:

const proxy = new Proxy(target, handler)
Copy the code
  • Target: The target object to be proxied. This can be any type of object, including a native array, a function, or even another proxy
  • Handler: An object with functions as attributes. The functions in the attribute define the custom behavior for performing various operations on the proxy instance

The methods that handelr objects support (often called traps) are:

  • Get: called when the property value is read
  • Set: called when an attribute is assigned
  • From:inOperator
  • Use deleteProperty:deleteOperator
  • Use ownKeys:Object.getOwnPropertyNamesMethods andObject.getOwnPropertySymbolsMethod time call
  • Apply: called when a function calls an operation
  • The construct:newOperator
  • Use defineProperty:Object.definePropertyMethod time call
  • Use getOwnPropertyDescriptor:Object.getOwnPropertyDescriptorMethod time call
  • Use getPrototypeOf:Object.getPrototypeOfMethod time call
  • Use setPrototypeOf:Object.setPrototypeOfMethod time call
  • Use isExtensible:Object.isExtensibleMethod time call
  • Use preventExtensions:Object.preventExtensionsMethod time call

You can see that Proxy controls the custom behavior of objects more comprehensively than Object.defineProperty. Here, we focus on the same parts as the latter, namely get and set. Although the names are get and set, the methods pass different parameters. Object.defineproperty defines GET and set for an attribute of an Object, while Proxy defines the entire Object. And the Proxy constructor returns a Proxy instance, not the original object. Therefore, get and set parameters in Proxy have two more arguments than object.defineProperty:

  • Obj: Target object to be proxied, that istarget
  • Key: Property accessed or set by the proxy object

As an example of the previous data object, we define the get and set methods as follows:

const dataProxy = new Proxy(data, {
  get(obj, key) {
    return obj[key]
  },
  set(obj, key, newValue) {
    obj[key] = newValue
    // Indicates success
    return true}})Copy the code

The other big difference between this and Object.defineProperty is that its responsiveness applies to the newly returned proxy Object, while the set and GET callbacks will not be triggered if the original Object attributes are accessed and modified. Therefore, if you use Proxy to rewrite the previous reactive view update, you need to use dataProxy when reading and setting object properties. The complete code is as follows:

<div id='app'></div>
<script>
  function reactive(target, update) {
    var targetProxy = new Proxy(target, {
      get(obj, key) {
        return obj[key]
      },
      set(obj, key, newValue) {
        obj[key] = newValue
        update()
        // Indicates success
        return true}})return targetProxy
  }

  var data = {
    text: 'hello'
  }
  var dataProxy = reactive(data, update)
  function update(a) {
    document.getElementById('app').textContent = dataProxy.text
  }
  update()
</script>
Copy the code

If we were also modifying data in the browser console, we would use dataproxy. text = ‘XXX’ instead of data.text = ‘XXXX’.

3. View update based on virtual DOM

In Handwriting Vue (I), we implemented view mount based on virtual DOM. We now implement reactive updates of the virtual DOM to the real DOM in combination with reactive updates.

The complete code is as follows:

function Vue(options) {
  var vm = this
  function update () {
    vm.update()
  }
  var data = options.data
  var keys = Object.keys(data)
  for (var i = 0, l = keys.length; i < l; i++) {
    var key = keys[i]
    this[key] = data[key]
    defineReactive(this, key, update)
  }
  this.$options = options
}

Vue.prototype.render = function() {
  var render = this.$options.render
  return render.call(this, createVNode)
}

Vue.prototype.update = function() {
  var vnode = this.render()
  this.$el = createElm(vnode, this.$el.parentNode, this.$el)
}

Vue.prototype.$mount = function (id) {
  this.$el = document.querySelector(id)
  this.update()
  return this
}

function createVNode(tag, data, children) {
  var vnode = { tag: tag, data: undefined.children: undefined.text: undefined }
  if (typeof data === 'string') {
    vnode.text = data
  } else {
    vnode.data = data
    if (Array.isArray(children)) {
      vnode.children = children
    } else {
      vnode.children = [ children ]
    }
  }
  return vnode
}

function createElm(vnode, parentElm, refElm) {
  var elm
  // Create a real DOM node
  if (vnode.tag) {
    elm = document.createElement(vnode.tag)
  } else if (vnode.text) {
    elm = document.createTextNode(vnode.text)
  }
  // Insert the actual DOM node into the document
  if (refElm) {
    parentElm.insertBefore(elm, refElm)
    parentElm.removeChild(refElm)
  } else {
    parentElm.appendChild(elm)
  }

  // Create child nodes recursively
  if (Array.isArray(vnode.children)) {
    for (var i = 0, l = vnode.children.length; i < l; i++) {
      var childVNode = vnode.children[i]
      createElm(childVNode, elm)
    }
  } else if (vnode.text) {
    elm.textContent = vnode.text
  }

  return elm
}

function defineReactive(obj, key, update) {
  var value = obj[key]
  Object.defineProperty(obj, key, {
    get: function() {
      return value
    },
    set: function(newValue) {
      if(value ! == newValue) { value = newValue update() } } })return obj
}
Copy the code

Save the above code to myvue_2.js, create a new HTML file myvue_2.html, and replace the following:

<div id="app"></div>
<script src="myvue_2.js"></script>
<script>
var vm = new Vue(
  {
    data: {
      text: 'hello world! '
    },
    render(h) {
      return h('div'.this.text)
    }
  }
).$mount('#app')
</script>
Copy the code

Try typing in the browser console:

vm.text = 'anything you like!!! '
Copy the code

If you see that the content is updated for you in real time, congratulations, you have successfully achieved the same responsive view update as Vue.

summary

We successfully implemented a responsive view update using set interception, but it was not perfect because any assignment to a property in a data object performed a view update, regardless of whether the property was used in the update. This means that if data has many attributes, but not all of them are used to render the view, we will do some superfluous view update operations, which is obviously a meaningless performance overhead. To automatically update the view based on the properties actually used in the update, only the properties used, involves collecting dependencies. We will continue to explore the implementation of the dependency collection in the next article.