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

文章目录

  • 前言
  • 参考资料
  • 初级版本
    • 实现publisher
    • 实现消息订阅中心
    • 实现Subscriber
    • 实现绑定函数
    • 完整代码
  • 进阶
    • 引入compile,并封装成MVVM
    • ES6 class语法版本
  • 结语




前言

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

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

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

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

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

一些关键解释如下。

模型

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

视图

  • 就像在MVC和MVP模式中一样,视图是用户在屏幕上看到的结构、布局和外观(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双向绑定——数据劫持+订阅发布模式_第1张图片



当然,从简单的方式下入手,我们可以先不用封装成一个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双向绑定——数据劫持+订阅发布模式_第2张图片

// 消息订阅中心
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双向绑定——数据劫持+订阅发布模式_第3张图片



完整代码

VUE2双向绑定——数据劫持+订阅发布模式_第4张图片


<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>debouncetitle>

    <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双向绑定——数据劫持+订阅发布模式_第5张图片

VUE2双向绑定——数据劫持+订阅发布模式_第6张图片

进阶

现在我想仿照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双向绑定——数据劫持+订阅发布模式_第7张图片

引入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的介绍笔记。

你可能感兴趣的:(前端)