欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

VUE2双向绑定——数据劫持+订阅发布模式

程序员文章站 2024-02-10 20:15:10
...




前言

单向绑定非常简单,就是把Model绑定到View,当我们用JavaScript代码更新Model时,View就会自动更新。

有单向绑定,就有双向绑定。如果用户更新了View,Model的数据也自动被更新了,这种情况就是双向绑定。

什么情况下用户可以更新View呢?填写表单就是一个最直接的例子。当用户填写表单时,View的状态就被更新了,如果此时MVVM框架可以自动更新Model的状态,那就相当于我们把Model和View做了双向绑定。

双向绑定在MVVM模式中发挥着及其重要的作用,它将视图与数据绑定起来,让我们得以关注于前后端交互的数据变动,而不必过度费心于页面的刷新上。

MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。

一些关键解释如下。

模型

  • 模型是指代表真实状态内容的领域模型(面向对象),或指代表内容的数据访问层(以数据为中心)。

视图

  • 就像在MVCMVP模式中一样,视图是用户在屏幕上看到的结构、布局和外观(UI)。

视图模型

  • 视图模型是暴露公共属性和命令的视图的抽象。MVVM没有MVC模式的控制器,也没有MVP模式的presenter,有的是一个绑定器。在视图模型中,绑定器在视图和数据绑定器之间进行通信。

绑定器

  • 声明性数据和命令绑定隐含在MVVM模式中。绑定器使开发人员免于*编写样板式逻辑来同步视图模型和视图。在微软的堆之外实现时,声明性数据绑定技术的出现是实现该模式的一个关键因素。

数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更

数据劫持: vue3.0之前的版本是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调。

接下来,我将基于Object.defineProperty()和观察者模式,一步步实现双向绑定,构建一个简单的MVVM模式的雏形。如果还不了解Object.defineProperty()和观察者模式原理,建议可以先看看我之前的两篇波博文。

  • 双向绑定基础原理——Object.defineProperty()的使用 https://blog.csdn.net/qq_41996454/article/details/107988996

  • 设计模式之观察者模式——Js实现 https://blog.csdn.net/qq_41996454/article/details/108042475

参考资料

《用ES6的class模仿Vue写一个双向绑定》 https://www.jianshu.com/p/78058c7922bf

剖析Vue原理&实现双向绑定MVVM

vue的双向绑定原理及实现

《Vue原理解析(八):一起搞明白令人头疼的diff算法》https://blog.csdn.net/u011199186/article/details/103668263

processon在线画图 https://www.processon.com/diagrams

初级版本



实现publisher

这个publisher实现的时候,要注意一下几点:

①监听data(对data中每个属性都使用Object.defineProperty加上setter和getter)

②对所有订阅者发布data的更新消息(调用订阅者的公共接口receiveTips)

那么监听到变化之后就是怎么通知订阅者了,所以接下来我们需要实现一个消息订阅器,很简单,维护一个数组,用来收集订阅者,数据变动触发notify,再调用订阅者的update方法

下面是封装成一个MVVM形式的样子。



let myMvvm = {
    $data: {
        data01: "test01",
        data02: "test02",
        data03: "test03",
    },

    // 指定元素
    $el: {}
};

// 对data中每个属性都使用Object.defineProperty加上 setter和getter
myMvvm._initPublisher = function (data = this.$data) {
    let that = this;

    Object.keys(data).forEach((key) => {
        let value = data[key];

        // 设置
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function () {
                console.log("这是get()方法");
                return value;
            },
            set: function (newVal) {
                if (value !== newVal) {
                    console.log("拦截到数据变化" + value + "---->" + newVal);
                    value = newVal;
                }
            },
        });
    });
    console.log(that);
};

myMvvm._initPublisher();

myMvvm.$data.data01 = 1;
console.log(myMvvm.$data.data01);

VUE2双向绑定——数据劫持+订阅发布模式



当然,从简单的方式下入手,我们可以先不用封装成一个MVVM对象的样子。

 // 初始化发布者
initPublisher = function (data, subCenter) {
    Object.keys(data).forEach((key) => {
        let value = data[key];

        // 设置
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function () {
                console.log("这是get()方法");
                return value;
            },
            set: function (newVal) {
                if (value !== newVal) {
                    console.log("拦截到数据变化" + value + "---->" + newVal);
                    subCenter.notify(key, value, newVal)
                    value = newVal;     
                }
            },
        });
    });
};
let data = {
        data01: "test01",
        data02: "test02",
        data03: "test03",
      };
      
// --------------初始化发布者-------------
initPublisher(data, mySubCenter)


实现消息订阅中心

接下来就是实现一个消息订阅中心,这里可是的代码要注意全文背诵的嗷!

以生活中的例子来举例,这个消息调度中心就相当于一个出版社。

  • 出版社下有很多挂名作家,这些作家的作品都在这个出版社的监控范围内(每天不停催更催稿)
  • 出版社管理读者订阅退订事务(addSubscriber,removeSubcriber)
  • 出版社负责通知读者最新消息

那么大致的一个思路就出来了,如下图。

VUE2双向绑定——数据劫持+订阅发布模式

// 消息订阅中心
let SubscriptionCenter = {
    // 订阅者Array
    subs: [],

    //  添加订阅者
    addSubscriber: function(sub){
        this.subs.push(sub);
    },

    // 删除订阅者
    removeSubcriber: function(sub){
        index = this.subs.indexOf(sub);
        if(index >= 1){
            this.subs.slice(index, 1);
        }
    },

    // 提示订阅者有新消息,触发receiveTip()这一公共接口
    notify: function(){
        this.subs.forEach(sub=>{
            sub.receiveTip();
        })
    }



实现Subscriber

  • 订阅者Subscriber在初始化的时候需要将自己添加进订阅中心SubscriberCenter中。
  • 订阅者Subscriber内部要有一个公共的接收更新消息的接口。(receiveTip方法)
  • receiveTip方法中要调用一个更新视图的回调方法。(因为这里涉及到解析attr中的“v-model”, {{}}等情况,这个方法通常由一个专门的compile来实现,在这里我只以简单的更换innerHtml或者value为例)
  • 绑定初始化时,要实现添加订阅者Subscriber的操作

那么大致的框架就出来了。

// 绑定时,生成一个订阅者对象
subscriber = {
    // 目标dom元素
    el: el,    
    // 接收通知后如果做 
    receiveTip: function (dataKey, oldVal, newVal) {
        console.log("--------------");
        console.log(this.el);
        console.log("收到更新消息:");
        console.log(dataKey + "(" + oldVal + "----->" + newVal + ")");

        // 视图如何改变,这里只做简单尝试
        callback(this.el, newVal)

        console.log("--------------");
    },
};

实现绑定函数

接下来我们要实现一个绑定方法,要实现以下几个动作:

  • 接收目标dom元素、目标dataObj、订阅的变量名、目标订阅中心、更新视图的回调函数
  • 生成一个订阅者对象,并按照订阅的变量名加入到订阅中心的订阅队列里

// 将el与data绑定起来
function doubleBind(el, data, dataKey, subCenter, callback) {
    if (!data[dataKey]){
        console.log("无法绑定")
        return
    }
    console.log('-------------双向绑定-----------------')
    console.log(el)
    console.log(dataKey)
    console.log('------------------------------')

    // 绑定时,生成一个订阅者对象
    subscriber = {
        // 目标dom元素
        el: el,    
        // 接收通知后如果做 
        receiveTip: function (dataKey, oldVal, newVal) {
            console.log("--------------");
            console.log(this.el);
            console.log("收到更新消息:");
            console.log(dataKey + "(" + oldVal + "----->" + newVal + ")");

            // 视图如何改变,这里只做简单尝试
            callback(this.el, newVal)

            console.log("--------------");
        },
    };
    subCenter.addSubscriber(dataKey, subscriber);
}



到这里,我们已经实现了使用订阅发布模式来实现从el向data的绑定,大致效果如下图。

VUE2双向绑定——数据劫持+订阅发布模式



完整代码

VUE2双向绑定——数据劫持+订阅发布模式

<!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>debounce</title>

    <style>
      .container {
        padding-top: 100px;
        padding-right: 30px;
        padding-left: 30px;
        margin-right: auto;
        margin-left: auto;
      }
    </style>
  </head>

  <body>
    <div class="container" id="app">
      <input type="text" value="xxx" id="id_input_01" />
      <p>当前绑定的data的值:<span id="id_text_01"></span></p>
      <input type="text" value="xxx" id="id_input_02" />
    </div>

    <script>
      // 初始化发布者
      initPublisher = function (data, subCenter) {
        Object.keys(data).forEach((key) => {
          let value = data[key];

          // 设置
          Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function () {
              console.log("这是get()方法");
              return value;
            },
            set: function (newVal) {
              if (value !== newVal) {
                console.log("拦截到数据变化" + value + "---->" + newVal);
                subCenter.notify(key, value, newVal)
                value = newVal;     
              }
            },
          });
        });
      };

      // 订阅中心
      class SubscriptionCenter {
        constructor() {
          // 订阅者Array由SubscriptionCenter来维护
          this.subsList = [];
        }

        //  按订阅的变量名添加订阅者
        addSubscriber(dataKey, sub) {
          if (!this.subsList[dataKey]) {
            this.subsList[dataKey] = [];
          }
          this.subsList[dataKey].push(sub);
        }

        // 删除订阅者
        removeSubcriber(dataKey, sub) {
          let subs = this.subsList[targetType];

          if (!subs) {
            return false;
          }
          if (!sub) {
            subs.length = 0;
          }

          let index = subs.indexOf(sub);
          if (index >= 0) {
            this.subs.splice(index, 1);
          }
        }

        // 提示订阅者有新消息,触发receiveTip()这一公共接口
        notify(dataKey, oldVal, newVal) {
          // 没有订阅就什么都不做
          if(!this.subsList[dataKey]){
            return
          }

          this.subsList[dataKey].forEach((sub) => {
            sub.receiveTip(dataKey, oldVal, newVal);
          });
        }
      }

      // 将el与data双向绑定起来
      function doubleBind(el, data, dataKey, subCenter, callback) {
        if (!data[dataKey]){
          console.log("无法绑定")
          return
        }
        console.log('-------------双向绑定-----------------')
        console.log(el)
        console.log(dataKey)
        console.log('------------------------------')

        // 绑定时,生成一个订阅者对象
        subscriber = {
          // 目标dom元素
          el: el,    
          // 接收通知后如果做 
          receiveTip: function (dataKey, oldVal, newVal) {
            console.log("--------------");
            console.log(this.el);
            console.log("收到更新消息:");
            console.log(dataKey + "(" + oldVal + "----->" + newVal + ")");

            // 视图如何改变,这里只做简单尝试
            callback(this.el, newVal)
            
            console.log("--------------");
          },
        };

        // 加入订阅者队列
        subCenter.addSubscriber(dataKey, subscriber);

        // 从dom向data的绑定
        el.addEventListener("keyup", function(e){
          console.log("------------从dom向data的绑定-------")
          console.log(dataKey + "(" + dataKey[dataKey] +  "----->" + e.target.value + ")")
          data[dataKey] = e.target.value;
          console.log("----------------------")
        })
      }

      // -------------------主程序--------------------------------
      let eInput01 = document.getElementById("id_input_01");
      let eInput02 = document.getElementById("id_input_02");
      let eText01 = document.getElementById("id_text_01");

      let data = {
        data01: "test01",
        data02: "test02",
        data03: "test03",
      };

      // ---------------建立订阅中心-------------
      mySubCenter = new SubscriptionCenter();

      // --------------初始化发布者-------------
      initPublisher(data, mySubCenter)

      // ---------------自定义回调事件--------------------------------
      let input_01_callback = function(el, newVal){
        el.value = newVal;
      }
      let text_01_callback =  function(el, newVal){
        el.innerText = newVal;
      }
      // ------------------将el与data双向绑定-----------------
      doubleBind(eInput01, data, "data01", mySubCenter, input_01_callback);
      doubleBind(eInput02, data, "data01", mySubCenter, input_01_callback);
      doubleBind(eText01, data, "data01", mySubCenter, text_01_callback);
    </script>
  </body>
</html>

运行结果如下。不过这里有个bug,就是从dom向data绑定的这条方向上,会发生按键盘过快导致data丢失而变成undefined;而且每次input.value的值改变而修改value的值时,又会触发一次set()中的发布方法。

VUE2双向绑定——数据劫持+订阅发布模式

VUE2双向绑定——数据劫持+订阅发布模式

进阶

现在我想仿照vue那样的调用形式来调用双向绑定,大概调用方式如下:

  • 能使用{{}}来绑定
  • 能使用v-model来绑定
  • 能使用@click调用方法改变data值
<div id="app">
        <input type="text" v-model="text1"><br>
        <input type="text" v-model="text2"><br>
        <textarea type="text" v-model="text3"></textarea><br>
        <button @click="add">加一</button>
        <h1>输入的是:{{text1}}+{{text2}}+{{text3}}</h1>
</div>
<script>
let app = new TinyVue({
          el: '#app',
          data: {
              text1: 123,
              text2: 456,
              text3: '文本框',
          },
          methods: {
              add() {
                  this.text1 ++
                  this.text2 ++
              }
          }
      })
</script>

那么,整个框架的运作方式大概如下图。

VUE2双向绑定——数据劫持+订阅发布模式

引入compile,并封装成MVVM

compiler要完成的几个关键动作:

  • 解析模板指令,并替换模板数据,初始化视图

  • 将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器

为了解析模板,首先需要获取到dom元素,然后对含有dom元素上含有指令的节点进行处理,因此这个环节需要对dom操作比较频繁,所有可以先建一个fragment片段,将需要解析的dom节点存入fragment片段里再进行处理。

<!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>debounce</title>

    <style>
      .container {
        padding-top: 100px;
        padding-right: 30px;
        padding-left: 30px;
        margin-right: auto;
        margin-left: auto;
      }
    </style>
  </head>

  <body>
    <div id="app">
      <h2>{{title}}</h2>
      <input v-model="name" />
      <h1>{{name}}</h1>
      <button v-on:click="clickMe">click me!</button>
    </div>
    <script>
      // -------------------------------订阅中心 Start --------------------------------
      function SubCenter() {
        this.subs = [];
      }
      SubCenter.prototype = {
        addSub: function (sub) {
          this.subs.push(sub);
        },
        notify: function () {
          this.subs.forEach(function (sub) {
            console.log("收到更新")
            sub.update();
          });
        },
      };
      SubCenter.target = null;
      // -------------------------------订阅中心 End --------------------------------

      // ----------------------发布者 Start-----------------------------
      function Publisher(data) {
        this.data = data;
        this.work(data);
      }

      Publisher.prototype = {
        work: function (data) {
          let self = this;
          Object.keys(data).forEach(function (key) {
            self.defineReactive(data, key, data[key]);
          });
        },
        // 劫持data中每个属性  set get
        defineReactive: function (data, key, val) {
          let subCenter = new SubCenter();
          let childObj = initPublisher(val);
          Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function getter() {
              if (subCenter.target) {
                subCenter.addSub(subCenter.target);
              }
              return val;
            },
            set: function setter(newVal) {
              if (newVal === val) {
                return;
              }
              val = newVal;

              // 通知订阅中心, 有新消息了
              subCenter.notify();
            },
          });
        },
      };

      function initPublisher(value, vm) {
        if (!value || typeof value !== "object") {
          return;
        }
        return new Publisher(value);
      }
      // -----------------------------发布者 End-----------------------------

      // ---------------------------订阅者 Start--------------------------------
      function Subscriber(vm, exp, cb) {
        this.cb = cb; // 收到新通知后执行的callback方法
        this.vm = vm; // mvvm obj
        this.exp = exp; // 期望获得的健值 exp
        this.value = this.get(); // 将自己添加到订阅器的操作
      }

      Subscriber.prototype = {
        update: function () {
          this.run();
        },
        run: function () {
          let value = this.vm.data[this.exp];
          let oldVal = this.value;
          if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
          }
        },
        get: function () {
          SubCenter.target = this; // 缓存自己
          let value = this.vm.data[this.exp]; // 强制执行监听器里的get函数
          SubCenter.target = null; // 释放自己
          return value;
        },
      };

      // ---------------------------订阅者 End--------------------------------

      // ---------------------------编译者 Start--------------------------------
      function Compiler(el, vm) {
        this.vm = vm;
        this.el = document.querySelector(el);
        this.fragment = null;
        this.init();
      }

      Compiler.prototype = {
        // 初始化视图
        init: function () {
          if (this.el) {
            this.fragment = this.nodeToFragment(this.el);
            this.compileElement(this.fragment);
            this.el.appendChild(this.fragment);
          } else {
            console.log("Dom元素不存在");
          }
        },
        //
        nodeToFragment: function (el) {
          let fragment = document.createDocumentFragment();
          let child = el.firstChild;
          while (child) {
            // 将Dom元素移入fragment中
            fragment.appendChild(child);
            child = el.firstChild;
          }
          return fragment;
        },
        compileElement: function (el) {
          let childNodes = el.childNodes;
          let self = this;
          [].slice.call(childNodes).forEach(function (node) {
            let reg = /\{\{(.*)\}\}/;
            let text = node.textContent;

            if (self.isElementNode(node)) {
              self.Compiler(node);
            } else if (self.isTextNode(node) && reg.test(text)) {
              self.compileText(node, reg.exec(text)[1]);
            }

            if (node.childNodes && node.childNodes.length) {
              self.compileElement(node);
            }
          });
        },
        Compiler: function (node) {
          let nodeAttrs = node.attributes;
          let self = this;
          Array.prototype.forEach.call(nodeAttrs, function (attr) {
            let attrName = attr.name;
            if (self.isDirective(attrName)) {
              let exp = attr.value;
              let dir = attrName.substring(2);
              if (self.isEventDirective(dir)) {
                // 事件指令
                self.compileEvent(node, self.vm, exp, dir);
              } else {
                // v-model 指令
                self.compileModel(node, self.vm, exp, dir);
              }
              node.removeAttribute(attrName);
            }
          });
        },
        compileText: function (node, exp) {
          let self = this;
          let initText = this.vm[exp];
          this.updateText(node, initText);
          new Subscriber(this.vm, exp, function (value) {
            self.updateText(node, value);
          });
        },
        compileEvent: function (node, vm, exp, dir) {
          let eventType = dir.split(":")[1];
          let cb = vm.methods && vm.methods[exp];

          if (eventType && cb) {
            node.addEventListener(eventType, cb.bind(vm), false);
          }
        },
        compileModel: function (node, vm, exp, dir) {
          let self = this;
          let val = this.vm[exp];
          this.modelUpdater(node, val);
          new Subscriber(this.vm, exp, function (value) {
            self.modelUpdater(node, value);
          });

          node.addEventListener("input", function (e) {
            let newValue = e.target.value;
            if (val === newValue) {
              return;
            }
            self.vm[exp] = newValue;
            val = newValue;
          });
        },
        updateText: function (node, value) {
          node.textContent = typeof value == "undefined" ? "" : value;
        },
        modelUpdater: function (node, value, oldValue) {
          node.value = typeof value == "undefined" ? "" : value;
        },
        isDirective: function (attr) {
          return attr.indexOf("v-") == 0;
        },
        isEventDirective: function (dir) {
          return dir.indexOf("on:") === 0;
        },
        isElementNode: function (node) {
          return node.nodeType == 1;
        },
        isTextNode: function (node) {
          return node.nodeType == 3;
        },
      };
      // ---------------------------编译者 End--------------------------------

      // ---------------------------MVVM Start--------------------------------
      function MyMVVM(options) {
        let self = this;
        this.data = options.data;
        this.methods = options.methods;

        // 设置别名
        Object.keys(this.data).forEach(function (key) {
          self.proxyKeys(key);
        });

        initPublisher(this.data);
        new Compiler(options.el, this);
        options.mounted.call(this); // 所有事情处理好后执行mounted函数
      }

      MyMVVM.prototype = {
        proxyKeys: function (key) {
          let self = this;
          Object.defineProperty(this, key, {
            enumerable: false,
            configurable: true,
            get: function getter() {
              return self.data[key];
            },
            set: function setter(newVal) {
              self.data[key] = newVal;
            },
          });
        },
      };
      // ---------------------------MVVM End--------------------------------
      testMvvm = new MyMVVM({
        el: "#app",
        data: {
          title: "hello world",
          name: "xxxxxxxxx",
        },
        methods: {
          clickMe: function () {
            this.title = "hello world";
          },
        },
        mounted: function () {
          window.setTimeout(() => {
            this.title = "你好";
          }, 1000);
        },
      });
    </script>
  </body>
</html>

ES6 class语法版本

<!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>debounce</title>

    <style>
      .container {
        padding-top: 100px;
        padding-right: 30px;
        padding-left: 30px;
        margin-right: auto;
        margin-left: auto;
      }
    </style>
  </head>

  <body>
    <div class="container">
      <div id="app">
        <input type="text" v-model="text1"><br>
        <input type="text" v-model="text2"><br>
        <textarea type="text" v-model="text3"></textarea><br>
        <button @click="add">加一</button>
        <h1>您输入的是:{{text1}}+{{text2}}+{{text3}}</h1>
        <select v-model="select">
            <option value="volvo">Volvo</option>
            <option value="saab">Saab</option>
        </select>
        <select v-model="select">
            <option value="volvo">Volvo</option>
            <option value="saab">Saab</option>
        </select>
        <h1>选择了:{{select}}</h1>
    </div>
    
    <script>
      class TinyVue{
            constructor({el, data, methods}){
                this.$data = data
                this.$el = document.querySelector(el)
                this.$methods = methods
                this._compiler()
                this._updater()
                this._initPublisher()
            }
            _initPublisher(data = this.$data) {
                let that = this
                Object.keys(data).forEach(i => {
                    let value = data[i]
                    Object.defineProperty(data, i, {
                        enumerable: true,
                        configurable: true,
                        get: function () {
                            return value;
                        },
                        set: function (newVal) {
                            if (value !== newVal) {
                                value = newVal;
                                that._updater()
                            }
                        }
                    })
                })
            }
            _initEvents(el, attr, callBack) {
                this.$el.querySelectorAll(el).forEach(i => {
                    if(i.hasAttribute(attr)) {
                        let key = i.getAttribute(attr)
                        callBack(i, key)
                    }
                })
            }
            _initView(el, attr, callBack) {
                this.$el.querySelectorAll(el, attr, callBack).forEach(i => {
                    if(i.hasAttribute(attr)) {
                        let key = i.getAttribute(attr),
                            data = this.$data[key]
                        callBack(i, key, data)
                    }
                })
            }
            _updater() {
                this._initView('input, textarea', 'v-model', (i, key, data) => {
                    i.value = data
                })
                this._initView('select', 'v-model', (i, key, data) => {
                    i.querySelectorAll('option').forEach(v => {
                        if(v.value == data) v.setAttribute('selected', true)
                        else v.removeAttribute('selected')
                    })
                })
                let regExpInner = /\{{ *([\w_\-]+) *\}}/g
                this.$el.querySelectorAll("*").forEach(i => {
                    let replaceList = i.innerHTML.match(regExpInner) || (i.hasAttribute('vueID') && i.getAttribute('vueID').match(regExpInner))
                    if(replaceList) {
                        if(!i.hasAttribute('vueID')) {
                            i.setAttribute('vueID', i.innerHTML)
                        }
                        i.innerHTML = i.getAttribute('vueID')
                        replaceList.forEach(v => {
                            let key = v.slice(2, v.length - 2)
                            i.innerHTML = i.innerHTML.replace(v, this.$data[key])
                        })
                    }
                })
            }
            _compiler() {
                this._initEvents('*', '@click', (i, key) => {
                    i.addEventListener('click', () => this.$methods[key].bind(this.$data)())
                })
                this._initEvents('input, textarea', 'v-model', (i, key) => {
                    i.addEventListener('input', () => {
                        Object.assign(this.$data, {[key]: i.value})
                    })
                })
            }
        }
      let app = new TinyVue({
          el: '#app',
          data: {
              text1: 123,
              text2: 456,
              text3: '文本框',
              select: 'saab'
          },
          methods: {
              add() {
                  this.text1 ++
                  this.text2 ++
              }
          }
      })
    </script>
  </body>
</html>



结语

我在写这篇博文的时候,得知VUE3即将正式发布了,我这才了解到vue3中的双向绑定改用Proxy来实现了。

前后对比如下:

VUE3之前:

对象:会递归得去循环vue得每一个属性,(这也是浪费性能的地方)会给每个属性增加getter和setter,当属性发生变化的时候会更新视图。

数组:重写了数组的方法,当调用数组方法时会触发更新,也会对数组中的每一项进行监控。

缺点:对象只监控自带的属性,新增的属性不监控,也就不生效。若是后续需要这个自带属性,就要再初始化的时候给它一个undefined值,后续再改这个值

​ 数组的索引发生变化或者数组的长度发生变化不会触发实体更新。可以监控引用数组中引用类型值,若是一个普通值并不会监控,例如:[1, 2, {a: 3}] ,只能监控a

VUE3:

Proxy消除了之前 Vue2.x 中基于 Object.defineProperty 的实现所存在的这些限制:无法监听 属性的添加和删除数组索引和长度的变更,并可以支持 MapSetWeakMapWeakSet

let obj = {
    name:{name:'hhh'},
    arr: ['吃','喝','玩']
}
//proxy兼容性差 可以代理13种方法 get set
//defineProperty 只对特定 的属性进行拦截 

let handler = {
    get (target,key) { //target就是obj key就是要取obj里面的哪个属性
        console.log('收集依赖')
        return target[key]
    },
    set (target,key,value) {
        console.log('触发更新')
        target[key] = value
    }
}

let proxy = new Proxy(obj,handler)
//通过代理后的对象取值和设置值
proxy.arr
proxy.name = '123'

哈哈…看来又得学起来了,不得不说前端的东西真是又多又杂,太难了QAQ

下次我会争取写一篇有关proxy的介绍笔记。