One, foreword

Vue is a popular framework recently, and I have written several small projects with Vue, so I studied the principle of Vue two-way binding in my spare time, and finally formed a blog to impress me more and share some experience with you.

Two, the implementation principle

First let’s talk about how vUE’s bidirectional binding is implemented. In fact, VUE uses the data hijacking + subscribe publish model to achieve bidirectional binding. One of the main functions is Object.definProperty(). If you don’t know how to use this function, check out MDN’s description of this function. In this case, we are hijacking the data through its GET /set. Bidirectional binding is then implemented through the subscription publishing pattern. Let’s take a quick look at how to implement data hijacking:

let demo = {
  name:""
}

let initValue = "init";
Object.defineProperty(demo, "name", {
  configurable:true.enumerable:true.get: function() {
    console.log("The get method");
    return initValue;
  },
  set: function (value) {
    console.log("Set method"); initValue = value; }})console.log(demo.name);
/ / get methods
//init
demo.name = "demo";
/ / set methods
Copy the code

This way we’ll use our two custom methods every time we get and set the demo.name value. So, after hijacking the data, how do we synchronize the data with the view? At this point, we should think from two aspects:

  1. View changes and data synchronization
  2. Data changes, view synchronization

The first one is easy to do with event listening, so for example, when the contents of the input box change, we can set the input event, and the view will trigger this event when it changes, and we’re synchronizing the data with the view.

So the second thing is when the data changes, how do we synchronize to the view. At this point, we should have the following problems to solve:

  1. How does the view know when the data has changed
  2. When the data changes, how do we know which view we should update the data from hereSubscribe to the publishedTo solve this problem, let’s look at the graph below

    Let’s do it one by one.

  • We need a listener Observer to hijack all the properties in the data so that we know when the data has changed,
  • We need a compiler Compile, which determines which data needs to be bidirectional bound when parsing HTML pages, and generates the corresponding subscriber Watcher to be added to the array
  • We store all the subscribers in an array, which is a property of the Dep class. When the data changes, we call the notify method of the Dep class to notify each subscriber that the data has changed
  • The subscriber is notified and calls its own methods to update the view

So, in summary, if we want to implement this functionality, we need to implement a listener Observer, a subscriber Watcher, a Dep class to store subscribers, and a compiler Compile. Let’s implement it one by one, some of which are very simple, but can help us understand the principle of VUE.

Implement a listener Observer

Because definProperty can only listen for a property of an object, we use a recursive implementation to listen for the entire object. This step is relatively simple:

class Observer {
  constructor(data) {
    this.observerAll(data);
  }

  observer(data, key, value) {
    this.observerAll(value);
    Object.defineProperty(data, key, {
      configurable:true.enumerable:true.get: function () {
        console.log("get " + value);
        return value;
      },
      set: function (newVal) {
        console.log("set " + newVal);
        value = newVal;
      }
    })
  }

  observerAll(data) {
    if(Object.prototype.toString.call(data) ! = ='[object Object]') return ;

    Object.keys(data).forEach((key) = > {
      this.observer(data, key, data[key]); }}})let obj = {
  name:"obj".m: {
    name:"m"}}new Observer(obj);

obj.m.name;
// get [object Object]
// get m
obj.name = "nihao";
// set nihao
console.log(obj.name);
// get nihao
// nihao
Copy the code

In the meantime, let’s do a simple test to prove that our code is OK.

Achieve a subscriber

For a subscriber, he should have four attributes: VM, exp, CB, value. Vm is the object to listen to and exp is the property to listen to. With these two properties, we can know which property a subscriber listens to. Cb is a callback function that subscribers should perform when data changes. Value holds the old value, and when a subscriber receives a notification, it should compare the new value with the old value and update the view if the data has changed, otherwise it does not need to update the view. The subscriber also has three methods, of which run and update are the methods to call for updates. Get is the method called at initialization. Let’s focus on the GET method. As we mentioned earlier, subscribers are stored in an array. When we compile the template, we store the subscribers in the array. How do we add the subscribers to the array? Because we use set and GET functions every time we get and change data, and the initial value of the subscriber’s value is the value of the data he listens to, we are bound to get the data through the GET method. So we can put the code to add the watcher instance to the array in a get function, namely an Observer. So again, we’re not just going to call get the first time, we’re going to call get the next time, so how do we avoid adding the same Watcher multiple times? We only need to add the watcher to the array when we initialize the watcher, so we need to cache the subscriber at dep. target while fetching data from the watcher get method. In the Observer get method, we need to check whether the dep. target is cached. Then determine whether to run the add operation. Let’s write the code:

class Watcher {
  constructor(vm, exp, cb) {
    this.vm = vm;
    this.exp = exp;
    this.cb = cb;
    this.value = this.get();
  }

  update() {
    this.run();
  }

  run() {
    let oldValue = this.value;
    if(oldValue ! = =this.vm[this.exp]) {
      this.value = this.vm[this.exp];

      this.cb(this.value);
    }
  }

  get() {
    Dep.target = this;
    let value = this.vm[this.exp];
    Dep.target = null;
    returnvalue; }}Copy the code

At the same time we need to modify the code in the Observer

class Dep {
  constructor() {
    this.subs = [];
  }

  addSub(sub) {
    this.subs.push(sub);
  }

  notify() {
    this.subs.forEach((sub) = >{ sub.update(); }}})class Observer {
  constructor(data) {
    this.dep = new Dep();
    this.observerAll(data);
  }

  observer(data, key, value) {
    this.observerAll(value);
    Object.defineProperty(data, key, {
      configurable:true.enumerable:true.// Second change
      get: (a)= > {
        // The third change
        if(Dep.target) {
          this.dep.addSub(Dep.target);
        }
        return value;
      },
      set: (newVal) = > {
        value = newVal;
        this.dep.notify();
      }
    })
  }

  observerAll(data) {
    if(Object.prototype.toString.call(data) ! = ='[object Object]') return ;

    Object.keys(data).forEach((key) = > {
      this.observer(data, key, data[key]); }}})Copy the code

We made three changes. The first is to add the Dep class, which has an array to store the subscribers and an addSub method to add new subscribers. He also has a way of notifying all the subscribers in the array. As you can see from the code, he notifies all the subscribers when a data change occurs, and the subscribers compare their cached values to the new values to determine whether they should update the view. In the second change, we changed set and get to arrow functions, because we need to use this in functions, and using normal functions would result in this pointing to the wrong value, so we use arrow functions to fix this. The third change is that we add the subscriber code, we determine if there is a cache on the Dep, and if there is, we add it to the array. Now that we have implemented listener and subscriber, we have implemented a simple two-way binding. Let’s see what happens. The test code is as follows:


      
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="Observer.js"></script>
    <script src="Watcher.js"></script>
    <script src="index.js"></script>
</head>
<body>
    <div id="root">
        emmm
    </div>
    <script>
        let data = {
          name:"msg"
        }
        new Observer(data);
        new Watcher(data, "name", (value) => {
          let root = document.getElementById("root");
          console.log(root)
          root.textContent = value;
        })
    </script>
</body>
</html>
Copy the code

The first page displays Hello

We then manually call the Observer and watcher in our code, and pass in a callback function for Watcher that changes what the div node on the page displays when the data changes. All the data has been written down, just to test the effect. When we change the data of data in the console, the page data also changes:

Okay, finally, let’s write a simple compiler that automatically establishes subscribers for elements in a page.

Five, Compile

The parser takes the DOM element in the page and iterates through it, finding where to add a subscriber, replacing template data, and initializing a subscriber. We only implement {{}} in VUE here.

class Compile {
  constructor(vm, el) {
    this.vm = vm;
    this.el = el;
    let root = document.querySelector("#root");
    this.compile(root);
  }

  compile(root) {
    let childNodes = root.childNodes;
    let reg = / \ {\ {(. *) \} \} /;
    [...childNodes].forEach((node) = > {
      let text = node.textContent;
      if(node.nodeType === 3 && reg.test(text)) {
          let exp = reg.exec(text)[1];
          this.addWatcher(exp, node);
          this.updateText(node, this.vm.data[exp]);
      }
      if(node.childNodes && node.childNodes.length) {
        this.compile(node);
      }
    })
  }

  addWatcher(exp, node) {
    new Watcher(this.vm, exp, (value) => this.updateText(node, value)); } updateText(node, value) { node.textContent = value; }}Copy the code

After we get the EL node, we traverse it and judge whether it is a text node for each node, that is, whether nodeType is equal to three. If it is a text node, use regular expressions to determine whether it conforms to the {{XXX}} format. If so, the text is replaced with the corresponding data and a subscriber is initialized. Otherwise you don’t do anything. Finally, determine if this node has any children and continue recursing. This way we can replace all the {{XXX}} class data in the page with the desired value.

Six, integrating

Finally, we integrate these files into index.js. We create a MyVue class that takes an object as an argument. We also call Observer and Compile in the constructor.

class MyVue {
  constructor(option) {
    this.data = option.data;
    this.el = option.el;

    new Observer(this.data);
    new Compile(this.this.el); }}Copy the code

Finally, let’s look at the effect:


      
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="Observer.js"></script>
    <script src="Watcher.js"></script>
    <script src="Compile.js"></script>
    <script src="index.js"></script>
</head>
<body>
    <div id="root">
        <div>
            <div>
                nihao
                <div>
                    {{name}}
                </div>
            </div>
        </div>
    </div>
    <script>
        new MyVue({
          el:"#root",
          data: {
            name: "data"}})</script>
</body>
</html>
Copy the code

It looks like this on the page:

At this point, we have a very simple effect, but I think this simple example will also give us a deeper understanding of how vUE bidirectional binding works.

Seven, conclusion

Finally, vue3 uses a proxy instead of defineProperty, as those of you who care about changes in front-end technology may know. In fact, proxy is just an upgraded version of defineProperty, and the underlying idea is still the same. Of course, since the VUE team decided to use proxy instead of the original approach, the latter must be superior to the former. So what’s the difference between the two? I’ll explain the difference in my next blog post. At the same time, because of my limited level, there must be a lot of mistakes in writing, I hope you can point out in time, we make progress together. In the meantime, the code has been uploaded to github: github.com/klx-buct/my…