Implementing a simple Vue

Keywords: Javascript Vue Attribute github IE

This week, I wrote a simple vue by referring to some blog posts. There are many implementations of this kind on the internet. My realization is nothing new. I should be a self-practice.

This article is also sent to me. github On the blog, welcome star

Specific realization

First of all, we have to have a Vue class. Of course, I wrote a very rough Vue class, so I call it BabyVue:

function BabyVue(options) {
    const { data, root, template, methods } = options;
    this.data = data;
    this.root = root;
    this.template = template;
    this.methods = methods;
    this.observe();
    this.resolveTemplate();
}

The BabtVue constructor accepts an option that contains data, root (the root node specified in html), template template, and methods. We mount these options on this method so that subsequent functions can easily get them. Then observe and resolveTemplate methods are executed

observe method:

BabyVue.prototype.observe = function() {
    Object.keys(this.data).forEach(key => {
        let val = this.data[key];
        const observer = new Observer();
        Object.defineProperty(this.data, key, {
            get: () => {
                if (Observer.target) {
                    observer.subscribe(Observer.target);
                }
                return val;
            },
            set: newValue => {
                if (val === newValue) {
                    return;
                }
                val = newValue;
                observer.publish(newValue);
            }
        });
    });
};

In the observation method, the data in this.data is traversed first, without considering the deeper structure, only the first data is traversed. The closure is used to cache its current value Val and an observer observer, and the Object. DefneProperty method is used to set its get and set attributes. Observer. TA is judged when the value is obtained. If the target exists, add Observer.target to the subscriber (its role will be described in detail later), and finally return val; when setting the value, compare the new value with val; if it is different, update the Val value and notify the subscriber to update it.

Following is Observer's code, which implements a simple observer pattern:

function Observer() {
    this.subscribers = [];
}
Observer.prototype.subscribe = function(subscriber) {
    !~this.subscribers.indexOf(subscriber) && this.subscribers.push(subscriber);
};
Observer.prototype.publish = function(newVal) {
    this.subscribers.forEach(subscriber => {
        const ele = document.querySelector(`[${subscriber}]`);
        ele && (ele.innerHTML = newVal);
    });
};

Subscribers are identified by their special attributes. When updating, they first get the target dom through the attribute selector and then update its value.

Here is the resolveTemplate code, which mainly renders templates, adds element identifiers and mounts events. In Vue, template parsing should be a more advanced method. I'm just going to parse Template Strings briefly.

BabyVue.prototype.resolveTemplate = function() {
    const root = document.createElement("div");
    root.innerHTML = this.template;
    const children = root.children;
    const nodes = [].slice.call(children);
    let index = 0;
    const events = [];
    while (nodes.length !== 0) {
        const node = nodes.shift();
        const _index = index++;
        node.setAttribute(`v-${_index}`, "");
        if (node.children.length > 0) {
            nodes.push(...node.children);
        } else {
            if (/\{\{(.*)\}\}/.test(node.innerHTML)) {
                const key = node.innerHTML.replace(/\{\{(.*)\}\}/, "$1");
                Observer.target = `v-${_index}`;
                node.innerHTML = this.data[key];
                Observer.target = null;
            }

            const method = node.getAttribute("v-on:click");
            if (method) {
                events.push({
                    key: `v-${_index}`,
                    type: "click",
                    method
                });
            }
        }
    }
    this.root.innerHTML = root.innerHTML;
    events.forEach(event => {
        const { key, type, method } = event;
        const ele = document.querySelector(`[${key}]`);
        ele.addEventListener(type, this.methods[method].bind(this));
    });
};

I add a special label to each element in the template, similar to v-xxx, to facilitate the acquisition of real dom according to the label (why not save node directly? You can try using the elements created by createElement to set innerHTML again, and some problems will arise.

Firstly, according to the regular matching {}, if it meets the criteria, the identifier in brackets is obtained, then Object.target is set as the identity of the element, and innerHTML of the element is set as the data in data. Note that at this time, we get this.data[key], which triggers the get attribute set before, and judges Observer. targe. Whether t exists, because we just set up Observer.target is currently the identity of the element, so it is added to the subscriber.

To retrieve its event attributes, we simply retrieve the v-on:click attributes. We save its attribute values and element identifiers in events.

Finally, after waiting for the template to be mounted in the root element, we traverse the events array to mount events

So far, my BabyVue has been basically implemented.

Demo

A simple counter is implemented:

Interested partners can copy the following code to run to see the effect:

<!DOCTYPE html>
<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>BabyVue</title>
</head>

<body>
  <div id="root"></div>
  <script>
    function BabyVue(options) {
      const {
        data,
        root,
        template,
        methods
      } = options;
      this.data = data;
      this.root = root;
      this.template = template;
      this.methods = methods;
      this.observe();
      this.resolveTemplate();
    }
    BabyVue.prototype.observe = function () {
      Object.keys(this.data).forEach(key => {
        let val = this.data[key];
        const observer = new Observer();
        Object.defineProperty(this.data, key, {
          get: () => {
            if (Observer.target) {
              observer.subscribe(Observer.target);
            }
            return val;
          },
          set: newValue => {
            if (val === newValue) {
              return;
            }
            val = newValue;
            observer.publish(newValue);
          }
        });
      });
    };
    BabyVue.prototype.resolveTemplate = function () {
      const root = document.createElement("div");
      root.innerHTML = this.template;
      const children = root.children;
      const nodes = [].slice.call(children);
      let index = 0;
      const events = [];
      while (nodes.length !== 0) {
        const node = nodes.shift();
        const _index = index++;
        node.setAttribute(`v-${_index}`, "");
        if (node.children.length > 0) {
          nodes.push(...node.children);
        } else {
          if (/\{\{(.*)\}\}/.test(node.innerHTML)) {
            const key = node.innerHTML.replace(/\{\{(.*)\}\}/, "$1");
            Observer.target = `v-${_index}`;
            node.innerHTML = this.data[key];
            Observer.target = null;
          }

          const method = node.getAttribute("v-on:click");
          if (method) {
            events.push({
              key: `v-${_index}`,
              type: "click",
              method
            });
          }
        }
      }
      this.root.innerHTML = root.innerHTML;
      events.forEach(event => {
        const {
          key,
          type,
          method
        } = event;
        const ele = document.querySelector(`[${key}]`);
        ele.addEventListener(type, this.methods[method].bind(this));
      });
    };

    function Observer() {
      this.subscribers = [];
    }
    Observer.prototype.subscribe = function(subscriber) {
      !~this.subscribers.indexOf(subscriber) &&
        this.subscribers.push(subscriber);
    };
    Observer.prototype.publish = function (newVal) {
      this.subscribers.forEach(subscriber => {
        const ele = document.querySelector(`[${subscriber}]`);
        ele && (ele.innerHTML = newVal);
      });
    };
    const root = document.getElementById("root");
    const vm = new BabyVue({
      data: {
        value: 0
      },
      root,
      template: `
        <div>
          <p>{{value}}</p>
          <button v-on:click="add">add</button>
          <button v-on:click="reset">reset</button>
        </div>`,
      methods: {
        add: function () {
          this.data.value++;
        },
        reset: function () {
          this.data.value = 0;
        }
      }
    });
  </script>
</body>

</html>

Posted by stephenf33 on Sun, 04 Aug 2019 08:10:46 -0700