Vue双向数据绑定的手动实现

class Asarua {

  // 传入一个对象,也就是实例化的时候的参数

  constructor(options) {

    this.$data = options.data;

    // 获取挂载的dom

    this.$el = document.querySelector(options.el);

    this.$methods = options.methods;

    this.$options = options;

    // 定义一个容器,来存放所有的订阅者

    this.observable = {};

    this.initObservable();

    this.addObservable();

    this.observe();

  }

  // 初始化容器

  initObservable() {

    for (let i in this.$data) {

      this.observable[i] = [];

    }

  }

  // 添加订阅

  addObservable() {

    // 获取所有当前节点下面的一级子节点

    const nodes = this.$el.children;

    // 使用for循环,如果是for in会遍历出一些奇怪的东西;

    for (let i = 0; i < nodes.length; i++) {

      // 判断是否存在自定义的a-model属性

      if (nodes[i].hasAttribute("a-model")) {

        // 获取存在a-model属性的元素

        const node = nodes[i];

        // 获取此元素的a-model的值,也就是订阅哪个属性

        const prop = node.getAttribute("a-model");

        // 在初始化后的容器中找到此属性,将一个实例化的观察者添加进容器,同时添加进去的还有一个刷新的方法

        this.observable[prop].push(new Watcher(node, "value", this, prop));

        // 开始监听,如果发生了input事件,则改变this.$data中此属性的值,同时触发拦截器,在拦截器中调用update方法进行更新

        node.addEventListener("input", () => {

          this.$data[prop] = node.value;

        })

      };

      if (nodes[i].hasAttribute("a-text")) {

        const node = nodes[i];

        const prop = node.getAttribute("a-text");

        this.observable[prop].push(new Watcher(node, "innerText", this, prop));

      };

      if (nodes[i].hasAttribute("a-html")) {

        const node = nodes[i];

        const prop = node.getAttribute("a-html");

        this.observable[prop].push(new Watcher(node, "innerHTML", this, prop));

      };

      // 如果有@click,那么为当前元素添加一个事件监听,在点击时调用this.$methods上的方法,同时把作用域指向this.$data,使其可以通过this.来访问$data中的属性

      if (nodes[i].hasAttribute("@click")) {

        const node = nodes[i];

        const prop = node.getAttribute("@click");

        node.addEventListener("click",()=>{

          this.$methods[prop].call(this.$data);

        })

      };

    }

  }

  observe() {

    const _this = this;

    // 遍历容器,给每个属性添加拦截器

    for (let i in this.$data) {

      let _value = this.$data[i];

      // 拦截this.$data中的每一个属性,如果是取值则直接返回,如果是设置,首先判断是否和之前相同,如果不同,将旧值替换为新值,并且调用update方法实时刷新

      Object.defineProperty(this.$data, i, {

        get() {

          return _value;

        },

        set(v) {

          if (_value !== v) {

            _value = v;

            _this.observable[i].forEach(v => v.update())

          }

        }

      })

    }

  }

}

class Watcher {

  // 接收四个参数,当前要更新的元素,要更新的属性,上面类的实例,监听的属性

  constructor(el, prop, any, attr) {

    this.el = el;

    this.prop = prop;

    this.any = any;

    this.attr = attr;

    this.update();

  }

  // update函数,调用时,使当前元素的当前属性的值变为data中的当前属性的值

  update() {

    this.el[this.prop] = this.any.$data[this.attr];

  }

}

// 首先获取所有实例化得到的属性,然后为data选项中的每一项都创建订阅者。

// 其次判断是否存在双向数据绑定的自定义属性,如果存在,则将其添加进其属性值所

// 对应的订阅者容器中。同时将其绑定的dom,其要更改的属性,以及更改后的值,还有一个同步当前dom值和data中的当前属性的值的方法传递进去,以便在调用时可以直接更改dom中的值。

// 然后为其添加一个事件监听,观测动向,如果更改了dom中的值则改变data中的值

// 为data中的所有属性添加拦截器,如果是取值则返回当前data中的值,如果是设置,在将传入的值设置为data的值后,调用容器中的当前属性的所有订阅者的update方法,

// 将更改后的data的值,赋值给所有使用了这个数据的值的dom,更改其属性

这么多东西说起来就两步

第一步,添加将input值变为自身值的订阅者

第二步,添加一个自身值改变之后将所有当前属性订阅者的值进行修改的拦截器