Knowing its helplessness and being content with it — << Zhuangzi >>

preface

This is a Vue source series of articles, recommended from the first article Vue source series (a) : Vue source interpretation of the correct posture to start reading. Article is my personal learning source code of a process, here to share the hope to help you.

This article is the last article: Vue source code series (three) : data responsive principle supplement. The main content is the principle and difference of data responsiveness of Vue2.X and Vue3.X, at the same time, supplement the publish and subscribe mode and the observer mode, and finally develop a min-Vue data responsiveness.

In the last article, we talked about a method for reactive source parsing: defineReactive(), which is Vue’s core code for reactive processing. If you don’t remember, you can click on the Vue source code series (3) : Data Responsiveness Principle review, the explanation of defineReactive() in the article in block 7.

Next, enter the article content πŸ‘‡ πŸ‘‡

At the heart of VueJs is a “responsive system”. “Responsive” means that when data changes, Vue notifies the code that uses the data. For example, if data is used in a view rendering, the view is automatically updated when the data changes. Next, let’s understand the responsivity principle of Vue2.X and Vue3.X from shallow to deep.

Vue2.X response formula principle

First, let’s talk about the response formula principle of Vue2.X. People malicious words not much, directly on the code πŸ˜‚ πŸ˜‚

<! DOCTYPEhtml>
<html lang="cn">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Vue2.X single attribute data response formula</title>
</head>
<body>
  <div id="app"></div>
  <script>
    // Emulate the data option in Vue
    let data = {
      msg: 'hello world'
    }
    // Simulate the Vue instance
    let vm = {}
    // Data hijacking: Perform some post-hijacking operations when accessing or setting members in the VM
    Object.defineProperty(vm, 'msg', {
      // Execute when the value is fetched
      get () {
        console.log('get: ', data.msg)
        return data.msg
      },
      // Execute when set value
      set (newValue) {
        console.log('set: ', newValue)
        if (newValue === data.msg) {
          return
        }
        data.msg = newValue
        // Update the DOM value when the data changes
        document.querySelector('#app').textContent = data.msg
      }
    })

    O (*οΏ£) *)o
    vm.msg = 'Hello VueJs'
    console.log(vm.msg)
  </script>
</body>
</html>

Copy the code

Running the code we can see that changing the value of vm. MSG triggers data hijacking

You can also try to change it and see how exactly does it trigger data hijacking

The above only supports one attribute responsivity, what if you want to support multiple attributes? Go straight to the code.

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Vue2.X multi-attribute data response formula</title>
</head>
<body>
  <div id="app"></div>
  <script>
    // Emulate the data option in Vue
    let data = {
      msg: 'hello vue'.value: 7
    }

    // Simulate the Vue instance
    let vm = {}

    proxyData(data)

    function proxyData(data) {
      // Iterate over all properties in the data object
      Object.keys(data).forEach(key= > {
        // Convert attributes in data to setters for the VM
        Object.defineProperty(vm, key, {
          enumerable: true.configurable: true,
          get () {
            console.log('get: ', key, data[key])
            return data[key]
          },
          set (newValue) {
            console.log('set: ', key, newValue)
            if (newValue === data[key]) {
              return
            }
            data[key] = newValue
            // Data changes to update the DOM value
            document.querySelector('#app').textContent = data[key]
          }
        })
      })
    }

    O (*οΏ£) *)o
    vm.msg = 'Hello Vue'
    console.log(vm.msg)
  </script>
</body>
</html>
Copy the code

Try to see if multiple attribute responsivity is supported

Vue3.X response principle

Vue3.X and Vue2.X are different in their responsive implementation, but Vue3. Some of you may not be familiar with Proxy, so let’s talk about it briefly:

Proxy objects are used to create a Proxy for an object to intercept and customize basic operations (such as property lookup, assignment, enumeration, function calls, and so on)

grammar

const p = new Proxy(target, handler)
Copy the code

parameter

  • target: to useProxyThe wrapped target object (which can be any type of object, including a native array, a function, or even another proxy).
  • handler: an object that usually has functions as attributes, and the functions in each attribute define the agents that perform the various operationspBehavior.

If you want to know more about MDN, you can explain it in detail.

Specific how to use Proxy Vue3.X response type principle, we or people malicious words not much, directly on the code πŸ˜‚ πŸ˜‚

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Vue3.X data responsiveness</title>
</head>
<body>
  <div id="app"> </div>
  <script>
    // Emulate the data option in Vue
    let data = {
      msg: 'hello vue'.value: 7
    }
    // Simulate a Vue instance
    let vm = new Proxy(data, {
      // The function that performs the proxy behavior is executed when a member accesses the VM
      get (target, key) {
        console.log('get, key: ', key, target[key])
        return target[key]
      },
      // When the vm member is set, it is executed
      set (target, key, newValue) {
        console.log('set, key: ', key, newValue)
        if (target[key] === newValue) {
          return
        }
        target[key] = newValue
        document.querySelector('#app').textContent = target[key]
      }
    })
    O (*οΏ£) *)o
    vm.msg = 'Hello Vue'
    console.log(vm.msg)
  </script>
</body>
</html>
Copy the code

Try it out and see how it works.

πŸ‘Œ 🏻 perfect! So let’s look at the difference, which is pretty clear, but just to summarize.

The responsivity principle of Vue2.X and Vue3.X

The first is Vue 2.x

  • Underlying principle: Object.defineProperty
  • Direct listening properties
  • Browser compatible with IE8 or higher (not compatible with IE8)

The second is Vue3. X

  • Underlying principle: Proxy
  • Listen directly on objects, not properties
  • A method is added in ES6. Internet Explorer is not supported

Publish subscribe mode and observer mode

Moving on to the reactive principles of Vue, we have to talk about publish-subscribe and observer patterns.

Publish and subscribe model

What is a publish and subscribe model? Based on an event center, the object receiving notification is the subscriber, which needs to subscribe to an event first, and the object triggering the event is the publisher, which notifies each subscriber by triggering the event. For example 🌰 : we have subscribed to the public number? For example: Wechat developers ah, strange dance selection ah and so on. There are two roles involved: the public account (the event center) and the people who subscribe to the public account (the subscribers). Then, when the author of the public account published the article, everyone who subscribed to the public account will receive the message, and here involves a role: the author of the public account (publisher).

The event bus in VUE is the publish-subscribe pattern used.

Next, we will simulate the implementation of custom events in Vue, still people malicious words are not much, directly on the code

<! DOCTYPEhtml>
<html lang="cn">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>Publish and subscribe model</title>
</head>
<body>
  <script>
    // Event trigger
    class EventEmitter {
      // Event center
      constructor () {
        // Create an object with a null stereotype attribute
        this.subs = Object.create(null)}// Register events
      $on (eventType, handler) {
        this.subs[eventType] = this.subs[eventType] || []
        this.subs[eventType].push(handler)
      }
      // Trigger the event
      $emit (eventType) {
        if (this.subs[eventType]) {
          this.subs[eventType].forEach(handler= > {
            handler()
          })
        }
      }
    }
    
    O (*οΏ£) *)o
    let em = new EventEmitter()
    // Register events (subscribe messages)
    em.$on('click'.() = > {
      console.log('click1')
    })
    em.$on('click'.() = > {
      console.log('click2')})// Trigger events (publish messages)
    em.$emit('click')
  </script>
</body>
</html>
Copy the code

To verify this, you can also copy the code to try it out:

Is it easy to feel πŸ˜† πŸ˜† πŸ˜†


Observer model

The target object and the observer object are interdependent. The observer observes the state of an object and notifies all observers who depend on the object if the state of the object changes.

The observer model has less of an event center than the publish-subscribe model, and subscribers and publishers are not directly related.

  • Target object[Subject]: own method: [Add/remove/notify] Observer;

  • Observer object[Observer]: Owning method: receiving notification of Subject status change and processing;

Notify all observers of a change in the status of the target object [Subject].

In Vue, responsive data changes are observer patterns. As we learned in the previous source code analysis article, each responsive attribute has a DEP, and the DEP stores the watcher that depends on this attribute. If the data changes, The DEP notifies all observers watcher to call the update method. Therefore, the observer needs to be collected by the target object in order to notify all observers that depend on it. Why is there a DEP in Watcher? The reason is that the watcher currently executing needs to know which DEP is notifying it at this point.

Observer (Subscriber) – Watcher

  • Update (): Exactly what to do when an event occurs

Target (publisher) – Dep

  • Subs array: Stores all observers
  • AddSub (): Adds an observer
  • Notify (): Call update() for all observers after an event occurs

No event center

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>Observer model</title>
</head>
<body>
  <script>
    // Target (publisher)
    class Dep {
      constructor () {
        // Record all subscribers
        this.subs = []
      }
      // Add subscribers
      addSub (sub) {
        if (sub && sub.update) {
          this.subs.push(sub)
        }
      }
      // Issue a notification
      notify () {
        this.subs.forEach(sub= > {
          sub.update()
        })
      }
    }
    // Observer (subscriber)
    class Watcher {
      update () {
        console.log('update')}}O (*οΏ£) *)o
    let dep = new Dep()
    let watcher = new Watcher()
    let watcher1 = new Watcher()
    // Add a subscription
    dep.addSub(watcher)
    dep.addSub(watcher1)
    // Enable notifications
    dep.notify()
  </script>
</body>
</html>
Copy the code

It’s pretty obvious, but let’s check it out.

Does it feel like an Epiphany when you guys see this place? “Ha ha… is it a bit 🌬 πŸ‚ 🍺?”

The difference between publish and subscribe and observer

Here are three ways to summarize:

From the structural analysis

  • In the observer mode, there are only two roles: the observer and the target (also known as the observed).
  • In a publish-subscribe model, there are not only publishers and subscribers, but also an event center (also called a control center)

From the relationship analysis

  • The observer and the target are loosely coupled
  • Publishers and subscribers, there is no coupling at all

Analysis from the perspective of use

  • Observer mode, mostly used within a single application (as mentioned above, responsive data changes in Vue are observer mode)
  • The publish-subscribe pattern applies more to cross-application patterns, such as the messaging middleware we commonly use

Let me illustrate it with a picture

Simple VUE response

Now that we’re all set, let’s move on to today’s topic. Still people malicious words are not much, directly on the code.

Use code (examples)

First, use the code. Then according to the functional code and then on the various parts of the code.

<body>
  <div id="app">
    <div>
      <input v-model="msg" />
      <span> {{ msg }} </span>
      <p v-text="msg"></p>
    </div>
    <br>
    <div>
      <input v-model="value" />
      <span> {{ value }} </span>
      <p v-text="value"></p>
    </div>
  </div>
  <script src="./dep.js"></script>
  <script src="./watcher.js"></script>
  <script src="./compiler.js"></script>
  <script src="./observer.js"></script>
  <script src="./vue.js"></script>
  <script>
    let vm = new Vue({
      el: '#app'.data: {
        msg: 'hello vue'.value: 7,}})</script>
</body>
Copy the code

Vue class “vue.js”

/** * vue.js ** Properties * - $el: mounted DOM object * - $data: data * - $options: passed property ** methods: * - _proxyData converts data to getter/setter form ** /

class Vue {
  constructor(options) {
    // The object passed in defaults to an empty object
    this.$options = options || {}
    // Get el (#app)
    this.$el =
      typeof options.el === 'string'
        ? document.querySelector(options.el)
        : options.el
    // Get data which defaults to an empty object
    this.$data = options.data || {}
    // Call _proxyData to process attributes in data
    this._proxyData(this.$data)
    // Use Obsever to convert data into responsive and monitor data changes, render the view
    new Observer(this.$data)
    // Compile the template render view
    new Compiler(this)}// Register attributes in data to Vue
  _proxyData(data) {
    // Execute data hijacking by traversing all attributes of the data object
    Object.keys(data).forEach((key) = > {
      // Convert attributes in data to getters/setters for the VM
      Object.defineProperty(this, key, {
        // enumerable (traversable)
        enumerable: true.// configurable (can be deleted using delete, can be redefined via defineProperty)
        configurable: true.// get the value
        get() {
          return data[key]
        },
        // When the value is set
        set(newValue) {
          // Return if the new value equals the old value
          if (newValue === data[key]) {
            return
          }
          // If the new value does not equal the old value, it is assigned
          data[key] = newValue
        },
      })
    })
  }
}

Copy the code

The Observer “Observer. Js”


/** * observer.js ** function * - Convert a property of $data to a responsive data * - If a property of $data is also an object, convert that property to a responsive data * - Send a notification when the data changes ** method: * -walk (data) - Walk through data attributes, Call defineReactive to convert data to getter/setter * - defineReactive(data, key, value) - Convert data to getter/setter * */
class Observer {
  constructor(data) {
    this.walk(data)
  }
  // Iterate over data to be responsive
  walk(data) {
     // If data is empty or data is not an object
     if(! data ||typeofdata ! = ="object") {
      return;
    }
    // Iterate over data to be responsive
    Object.keys(data).forEach((key) = > {
      this.defineReactive(data, key, data[key])
    })
  }
  // Convert a property in data to a getter/setter
  defineReactive(data, key, value) {
    // Check if the attribute value is an object. If so, continue to convert the object to reactive
    this.walk(value)
    // δΏε­˜δΈ€δΈ‹ this
    const that = this;
    // Create a Dep object to add an observer to each data
    let dep = new Dep();

    Object.defineProperty(data, key, {
      // enumerable (traversable)
      enumerable: true.// configurable (can be deleted using delete, can be redefined via defineProperty)
      configurable: true.// get the value
      get() {
        // Add the observer object dep. target to represent the observer
        Dep.target && dep.addSub(Dep.target)
        return value
      },
       // When the value is set
      set(newValue) {
        // Return if the new value equals the old value
        if (newValue == value) {
          return;
        }
        // The new value does not equal the old value
        value = newValue;
        // After the assignment, check if the attribute is an object, and if so, convert the attribute to reactive
        that.walk(newValue);
        // Send notification after data change, trigger watcher pudate methoddep.notify(); }})}}Copy the code

The Compiler Compiler. Js”

/** * Compiler.js ** Features * - Compile templates, parse instructions/interpolating expressions * - Responsible for the first rendering of the page * - Re-render views after data changes ** Attributes * - el-app elements * - VM-vue instances ** Methods: * -compile (el) - compile entry * -compileElement (node) - compileElement(instruction) * -compiletext (node) compileText(interpolation) * -isDirective (attrName) - (check whether it is an instruction) * -istextNode (node) - (check whether it is a text node) * -iselementNode (node) - (check whether it is an element node) */

class Compiler {
  constructor(vm) {
    / / for the vm
    this.vm = vm
    / / for el
    this.el = vm.$el
    // Compile the template render view
    this.compile(this.el)
  }
  // Compile the template render view
  compile(el) {
    // Return if it does not exist
    if(! el)return;
    // Get the child node
    const nodes = el.childNodes;
    / / collection
    Array.from(nodes).forEach((node) = > {
      // Compilation of text type nodes
      if (this.isTextNode(node)) {
        // Compiles the text node
        this.compileText(node)
      } else if (this.isElementNode(node)) {
        // Compile the element node
        this.compileElement(node)
      }
      // Determine whether there are still child nodes
      if (node.childNodes && node.childNodes.length) {
        this.compile(node); }}); }// Add instruction methods and execute
  update(node, value, attrName, key) {
    For example, textUpdater is added to handle V-text
    const updateFn = this[`${attrName}Updater`];
    // call if it exists
    updateFn && updateFn.call(this, node, value, key);
  }
  // Used to process v-text
  textUpdater(node, value, key) {
    node.textContent = value;
  }
  // To process the V-model
  modelUpdater(node, value, key) {
    node.value = value;
    // To implement two-way data binding
    node.addEventListener("input".(e) = > {
      this.vm[key] = node.value;
    });
  }
  // Compile the element node
  compileElement(node) {
    // Get all the attributes above the element node
    Array.from(node.attributes).forEach((attr) = > {
      // Get the attribute name
      let _attrName = attr.name
      // Check if it starts with a V -
      if (this.isDirective(_attrName)) {
        / / delete v -
        const attrName = _attrName.substr(2);
        // Get the attribute value and assign it to key
        const key = attr.value;
        const value = this.vm[key];
        // Add instruction methods
        this.update(node, value, attrName, key);
        // After the data is updated, update the view through wather
        new Watcher(this.vm, key, (newValue) = > {
          this.update(node, newValue, attrName, key); }); }}); }// Compiles the text node
  compileText(node) {
    //. Indicates any single character, excluding the newline character. + indicates that multiple previous characters are matched. Indicates non-greedy mode, ending the search as early as possible
    const reg = / \ {\ {(. +?) \} \} /; 
    // Get the text content of the node
    var param = node.textContent;
    // Check whether there is {{}}
    if (reg.test(param)) {
      $1 = $1; $1 = $1
      // Remove Spaces before and after {{}}
      const key = RegExp.$1.trim();
      // Assign to node
      node.textContent = param.replace(reg, this.vm[key]);
      // When compiling the template, create an instance of Watcher and mount it internally to Dep
      new Watcher(this.vm, key, (newValue) = > {
        // Update the view via the callback functionnode.textContent = newValue; }); }}// Determine if the attribute of the element is a vue directive
  isDirective(attrName) {
    return attrName && attrName.startsWith("v-");
  }
  // Determine if it is a text node
  isTextNode(node) {
    return node && node.nodeType === 3;
  }
  // Check if it is an element node
  isElementNode(node) {
    return node && node.nodeType === 1; }}Copy the code

Dep “Dep. Js”

/** * dep.js ** Functions * - Collect observer * - Trigger observer ** Properties * - subs: Array * - target: Watcher ** Method: * -addSub (sub): Add observer * -notify (): Triggers an observer update * */
 
 class Dep {
  constructor() {
    // Store the observer
    this.subs = []
  }
  // Add an observer
  addSub(sub) {
    // Check whether the observer exists, has an update and typeof is function
    if (sub && sub.update && typeof sub.update === "function") {
      this.subs.push(sub); }}// Send notifications
  notify() {
    // Trigger the update method for each observer
    this.subs.forEach((sub) = > {
      sub.update()
    })
  }
}

Copy the code

Watcher “Watcher. Js”

/** ** Watcher.js ** functionality * - Generates observer update view * - Mounts observer instance to Dep class * - Calls callback function to update view ** properties * -vm: vue instance * -key: Observe the key * -cb: callback function * * method: * -update () * */

class Watcher {
  constructor(vm, key, cb) {
    / / for the vm
    this.vm = vm
    // Get the attributes in data
    this.key = key
    // The callback function (how to update the view)
    this.cb = cb
    // Mount the watcher instance to Dep
    Dep.target = this
     // Cache old values
    this.oldValue = vm[key]
    // After the get value, the instance in the Dep is cleared
    Dep.target = null
  }
  // The observer method is used to update the view
  update() {
    // When you call update, get the new value
    let newValue = this.vm[this.key]
    // If the new value is the same as the old value, it is not updated
    if (newValue === this.oldValue) return
    // Call the specific update method
    this.cb(newValue)
  }
}

Copy the code

Let’s verify that.

That’s it. Feel can welcome to like, collect and follow πŸ™ πŸ™.