In simple terms, Vue responsive principle, MVVM implementation process

    I believe that when I first came into contact with Vue response, many students will think of the response principle when learning css, that is, make corresponding adjustments to the style of the content on the page according to different devices, different widths and different characteristics, while the response in Vue is not based on the sense of image style, The response formula in Vue is that once the data changes, we will immediately intercept it through the listener, and then we can add some things we want to do. These operations include but are not limited to sending a network request, modifying the dom and so on.

In Vue, two js schemes are used to realize the data response, namely:

1.Vue (version 2.x) object attribute interception  

Object.defineProperty

2.Vue (version 3.x) object overall proxy

Proxy

Next, we will analyze the principle mainly based on the implementation of Object.defineProperty. The difference between 3.0 and 2.0 is that it solves the unprovoked performance consumption of responsive processing, and adopts the overall method of hijacking objects + lazy processing (a bit similar to lazy loading)

Conventional methods of defining attributes cannot trigger responsive processing, such as our common literal definition

let data = {
      name: 'Eason Chan'
    }

We process the data in the background, but with this definition method, we can't intercept the modification processing or access processing, because we can't intercept the modification and access of attributes through literal definition, so we can't respond to the intercepted operations.

Here, we need the object attribute interception method of js to intercept the access and modification of our attributes, so as to insert the functions we want to implement

		Object.defineProperty(data, 'name', {
			// The get method is called automatically when we access the name attribute of data
			// The return value of the get method is the value you get from data.name
			get() {
				console.log('You visited data of name attribute')
				return 'Eason Chan'
			},
			// The set method is called automatically when we modify the name attribute
			// And the latest value of the property will be passed in as an argument, that is, newValue
			set(newValue) {
				console.log('You modified it data of name The latest value of the property is', newValue)
				// This location will be executed as long as you modify the name attribute
				// So if you want to do something of your own when the name changes
				// You can put it here
				// 1. ajax()
				// 2. Operate a dom region
			}
        })

 

The principle basis of Vue's responsive mechanism is preliminarily realized through Object.defineProperty. Of course, there are still many problems in this writing

For example, get directly returns a fixed value, and set does not operate after getting the new value

  After the modification, the access name attribute still hasn't changed. Of course, what should we write if we want to intercept multiple attributes in an object? Do you want to write such a large section for each attribute to add interception?

We have studied before forEach Method can traverse the key and value of each attribute of the object. Key represents the attribute name of the attribute and value represents the attribute value of the attribute. Through traversal, we can get the property names and property values of multiple properties under an object. The data in the Object.defineProperty method we used above is the object name, and the name corresponds to the traversed key. In the get and set functions, we can modify and access the value of the traversed value. Through this method, we can make a general interception scheme, To automatically traverse and obtain key and value values to add interception to the object's properties. The code is as follows

  // Under normal circumstances, the objects we define will have multiple attributes  
  let data = {
      name: 'Zhang San',
      nickName: 'Outlaw maniac',
    }
  // Traverse the method of adding interception
    Object.keys(data).forEach((key) => {
      console.log(key, data[key])
      // key represents each attribute name of the data object
      // As long as we know the data and key, we can know all the attribute values through the object access method
	  // The method added to the object binding interception in the traversal passes the corresponding property name and property value
      defineReactive(data, key, data[key])
    })
    // The general property interception method calls the following methods multiple times through traversal
    function defineReactive(data, key, value) {
      Object.defineProperty(data, key, {
        get() {
          console.log('You have accessed properties', key)
          return value
        },
        set(newValue) {
          console.log('You modified the properties', key)
          value = newValue
        }
      })
    }

We complete the hijacking of data changes through the above methods. Whenever the data is modified or viewed, we can add methods from it. Then we can realize a process of M = > V, which is the basis of data binding, that is, the change of data causes the change of view. It is not difficult to realize this function with the above methods

Process of implementing v-text component

To realize the change of view caused by the change of data, and identify and render through the identification of v-text, we can complete it in three steps

The first step is to monitor data changes, which we have implemented

The second step is to respond the data changes to the view by operating Dom after the data changes

  The above set method will be called automatically every time the data is modified. To realize the above steps, we only need to add a method to operate Dom in the set method and render the obtained new value to Dom in real time

 document.querySelector('Here's what to do dom element').innerText = newValue

  The third step is to identify the specified v-text ID and render the rendering result into the element with the specified ID

·   In this step, we need to use a Dom attribute childNodes   This attribute can obtain all the child nodes under the element. Each child node has a nodeType attribute to distinguish what the child node is. When it is an element node, it is 1, the attribute node is 2, and the text node is 3. Attributes can obtain all the attributes of the node, that is, it can identify the v-text ID, Through this method, we can identify the v-text ID to obtain the element nodes that need to be rendered. Here, we can encapsulate a new method to find and obtain the element nodes under the specified ID and identify the ID

	<div id="app">
		<p v-text="name"></p>
		<div v-text="nickName"></div>
	</div>
    //Suppose we want to get the Dom node above and render the v-text in it
<script>
       let data = {
          name: 'Zhang San',
          nickName: 'Outlaw maniac',
        }
        Object.keys(data).forEach((key) => {
			defineReactive(data, key, data[key])
		})
		function defineReactive(data, key, value) {
			// Perform conversion operation
			Object.defineProperty(data, key, {
				get() {
					console.log('You have accessed properties', key)
					return value
				},
				set(newValue) {
					// The execution of the set function does not automatically determine whether the two modified values are equal
					// Obviously, if equal, the logic of change should not be performed
					if (newValue === value) {
						return
					}
					console.log('You modified the properties', key)
					value = newValue
					// Here we reflect the latest values in the view, which is the key position
					// Core: operating dom is to set the latest value through operating dom api
					findIdentification()
				}
			})
		}
		function findIdentification() {
			let app = document.getElementById('app')
			// Get all child nodes
			const childNodes = app.childNodes
			childNodes.forEach(node => {
				if (node.nodeType === 1) {
					const attrs = node.attributes
					console.log(attrs)
					Array.from(attrs).forEach(attr => {
						const nodeName = attr.nodeName
						const nodeValue = attr.nodeValue
						console.log(nodeName, nodeValue)
						// nodeName = "v-text" is the ID we need to find
						// nodeValue = "name" is the value of the identification of the corresponding data in data
						// Put the data in the data on the dom that meets the identification
						if (nodeName === 'v-text') {
							console.log('Set value', node)
							node.innerText = data[nodeValue]
						}
					})
				}
			})
		}
findIdentification()
</script>

And in the previous data interception access to invoke the method to achieve the preparation of v-text, you can achieve M = "V" process view binding data changes, every time the modified data view will respond to changes in the data, and the same way v-html is the same method, innerText can not be changed to innerHtml can be achieved.

The most special and commonly used Vue also belongs to v-model, that is, two-way binding is also the basic principle of MVVM programming idea. We can already realize M = "V". We only need to implement another v = "m, and then the two-way binding of data can be realized. It doesn't seem difficult to realize it on the basis of v-text. As long as we realize the change monitoring and interception of Dom elements and reflect it to the data at the same time

Implementation of V-model

We create an input field in html and add a v-model ID to it

<div id="app">
  <input v-model="name" />
</div>
		function findIdentification() {
			let app = document.getElementById('app')
			// Get all nodes
			const childNodes = app.childNodes // All types of nodes include text nodes and label nodes
			childNodes.forEach(node => {
				if (node.nodeType === 1) {
					const attrs = node.attributes
					Array.from(attrs).forEach(attr => {
						const nodeName = attr.nodeName
						const nodeValue = attr.nodeValue
						// Implement v-text
						if (nodeName === 'v-text') {
							console.log(`You have currently modified the properties ${nodeValue}`)
							node.innerText = data[nodeValue]
						}
						// Implement v-model
						if (nodeName === 'v-model') {
							// The value of input is equivalent to the innerText of labels such as p. this step is to bind the data value to the view, that is, M = V
							node.value = data[nodeValue]
							// Listen to the input event and get the latest input value assigned to the bound property in the event callback
							node.addEventListener('input', (e) => {
								let newValue = e.target.value
								// Reverse assignment
								data[nodeValue] = newValue
							})
						}
					})
				}
			})
		}

This does realize the two-way binding of data, but there is a problem of wasting memory, because no matter which attribute we modify, other attributes will be updated. The normal logic should be that we update which attribute when we modify it, There is still room for optimization, which is similar to lazy loading to save memory

optimization

Let's think about what code we actually need to execute after each data update?

node.innerText = data[dataProp]

In order to cache node and data[dataProp], we need to cache the data during each execution through a closure function. Before, it will be re created every time it is used. After the event, the cached local variables will be cleared. This is like the steering wheel when driving. The steering wheel should be a global variable when it is straight, Instead of pulling the steering wheel down in a straight line and up in a corner, we should cache the traversal update method in the form of closure. The code is as follows:

() => {
  node.innerText = data[dataProp]
}

We can close the update method. When our data changes, we will call the closed method to update. However, a responsive data may have multiple view parts to rely on, that is, after the responsive data changes, multiple views may need to be changed, as shown in the following figure. A name attribute has two div s using it, Therefore, an attribute usually corresponds to multiple update functions

<div id="app">
   <div v-text="name"></div>
   <div v-text="name"></div>
   <p v-text="nickName"></p>
   <p v-text="nickName"></p>
</div>

Here, we need to understand a new method concept of this one to many form, that is, the publish subscribe mode. This mode is actually an object one to many dependency. When the object state changes, all objects that depend on it will receive messages

We can understand this concept through browser events. A normal click event can only be bound to a time handler to execute

    btn.onclick = function () {
      console.log('btn It was clicked')
    }
	Only one callback function can be bound in this way
    btn.onclick = function(){
      console.log('I was clicked again')
    }

Only the callback of one of the methods will be executed. When we need one to many, we need this method

 btn.addEventListener('click', () => {
      console.log('I was clicked')
    })
    btn.addEventListener('click', () => {
      console.log('I was clicked again')
    })

 Using addEventListener, we can call multiple callback functions for one trigger mode to process. We can realize our optimization effect by making good use of this mode, that is, modifying the attribute field only modifies the view effect of binding the child elements represented by the attribute field

The complete code is as follows

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id="app">
    <p v-text="name"></p>
    <p v-text="name"></p>
    <span v-text="nickName"></span>
    <input type="text" v-model="nickName">
  </div>

  <script>
    // Introduce publish subscribe mode
    const Dep = {
      map: {},
      // Method of collecting events
      collect(eventName, fn) {
        // If the current map has been initialized, click: []  
        // push directly into it. If it is not initialized, initialize it for the first time
        if (!this.map[eventName]) {
          this.map[eventName] = []
        }
        this.map[eventName].push(fn)
      },
      // Method of triggering event
      trigger(eventName) {
        this.map[eventName].forEach(fn => fn())
      }
    }

    let data = {
      name: 'Zhang San',
      nickName: 'Outlaw maniac'
    }
    // Turn the attributes in data into responsive
    Object.keys(data).forEach((key) => {
      defineReactive(data, key, data[key])
    })
    function defineReactive(data, key, value) {
      // Perform conversion operation
      Object.defineProperty(data, key, {
        get() {

          return value
        },
        set(newValue) {
          // The execution of the set function does not automatically determine whether the two modified values are equal
          // Obviously, if equal, the logic of change should not be performed
          if (newValue === value) {
            return
          }
          value = newValue
          // Here we reflect the latest values in the view, which is the key position
          // Core: operating dom is to set the latest value through operating dom api
          // Perform accurate update here - > find the corresponding update function through the attribute name in data and execute it in turn
          Dep.trigger(key)
        }
      })
    }
    // 1. Put the data on the corresponding dom and display it through identification search
    function findIdentification() {
      let app = document.getElementById('app')
      // Get all nodes
      const childNodes = app.childNodes // All types of nodes include text nodes and label nodes
      childNodes.forEach(node => {
        if (node.nodeType === 1) {
          const attrs = node.attributes
          Array.from(attrs).forEach(attr => {
            const nodeName = attr.nodeName
            const nodeValue = attr.nodeValue
            // Implement v-text
            if (nodeName === 'v-text') {
              node.innerText = data[nodeValue]
              // Collect update function
              Dep.collect(nodeValue, () => {
                console.log(`You have currently modified the properties ${nodeValue}`)
                node.innerText = data[nodeValue]
              })
            }
            // Implement v-model
            if (nodeName === 'v-model') {
              // Call the dom operation to bind data to the input tag
              node.value = data[nodeValue]
              // Collect update function
              Dep.collect(nodeValue,()=>{
                node.value = data[nodeValue]
              })
              // Listen to the input event and get the latest input value assigned to the bound property in the event callback
              node.addEventListener('input', (e) => {
                let newValue = e.target.value
                // Reverse assignment
                data[nodeValue] = newValue
              })
            }
          })
        }
      })
    }
    findIdentification()
    console.log(Dep)
  </script>

summary

  1. In the Vue2.x version, the data placed in data will be processed responsively whether we use it or not, so the data defined in data should be as concise as possible

  2. Whether it is interpolation expression or identification, it is essentially a mark that needs to render the data change response to the Dom, and then return the corresponding data change to the corresponding Dom through the mark

  3. The essence of publishing and subscribing is to solve the one to many problem. You can accurately determine the corresponding update function through the name similar to key, so as to reduce unnecessary update operations

  4.MVVM is actually split into m (model) V (view) M = "V view responds to model data change v =" m data responds to view data change
  The author's ability is limited. If there is anything wrong, please correct it. Your praise comments are my biggest update power

Keywords: Javascript Vue.js css

Added by czukoman20 on Fri, 17 Sep 2021 17:50:24 +0300