面试官:手写一个简单的Vue数据响应式吧

209 阅读3分钟

一、什么是数据的双向绑定(MVVM)

微信图片_20220824103413.jpg

数据的双向绑定主要有以下两个方面:

  1. 改变代码层面的数据,view视图层也同步变化,即Data->view的变化
  2. 改变视图层输入框中的内容,Data数据也同步变化,即view->Data的变化

其一,Data数据层->view视图层的变化我们要通过Object.definePropertyProxy来实现; 其二,view视图层->Data数据层的变化可以通过事件监听来实现,也就是v-model的语法糖。只有两个步骤全都实现才是数据的双向绑定,实现其中一个只是数据的单向绑定!

二、响应式数据效果图

kk 2022-08-24 15-03-49.gif

三、实现数据的响应式

依照vue的模板格式来,首先初始化内容,创建一个MVVM类,html内容都放在#app下。

 <div id="app">
   <h2>通过改变视图=>改变数据</h2>
   <input type="text" placeholder="姓名" v-model="info" />
   <div>
     <p>人物:<span>{{info}}</span></p>
   </div>
 </div>
 <button onclick="ModifyData()">通过改变数据=>改变视图</button>
 <button onclick="getData()">获取当前数据</button>
 function ModifyData() {
   p.setData("info", "武大郎");
 }
 function getData() {
   console.log(p._data);
 }
 
 class MVVM {
   constructor(el, data) {
     this.init(el, data);
   }
   //初始化
   init(el, v) {
     this._data = v.data();
     this.el = document.querySelector(el);
   }
 }
 let p = new MVVM("#app", {
   data() {
     return {
       info: "潘金莲",
     };
   },
 });

以下各个函数的作用:

  • init 负责初始化数据内容
  • observer 对目标对象的属性进行遍历,确保每个属性都被监听到
  • defineReactive 对目标对象属性进行监听
  • matchMustache 匹配html中Mustache的内容,用data中的数据进行替换,需要递归获取文本节点
  • bindInput 获取绑定v-model的输入框节点,改变输入框内容时,数据跟着改变
  • updateView 数据发生变化时,触发页面更新
  • setData 对外暴露的一个修改数据的接口

执行逻辑为:

new一个MVVM ==> 调用 observer(data) 将对象变成响应式 ==> matchMustache函数把{{xxx}}中的内容替换为data的数据 ==> bindInput函数获取v-model的值用data的值进行替换 ==> 输入框改变数据 ==> 触发set更新视图 ==> 点击ModifyData事件,调用setData方法 ==> 触发set更新视图。

Object.defineProperty监听

class MVVM {
   constructor(el, data) {
      this.reg = /\{\{(.*?)\}\}/;
      this.modelValue = {};
      this.init(el, data);
   }
   
   //初始化
   init(el, v) {
     this._data = v.data();
     this.el = document.querySelector(el);
     //监听对象
     this.observer(this._data)
     //初始化data内容到视图层
     this.matchMustache(this.el);
     this.bindInput();
   }
   
  //对目标对象属性进行监听
  defineReactive(object,key, value) {
    let _this = this;
    如果值是个对象则深度监听;
    _this.observer(value);
    Object.defineProperty(object, key, {
      get() {
        return value;
      },
      set(newValue) {
        value = newValue;
        //新增是个对象时也深度监听
        _this.observer(newValue);
        //修改触发更新
        _this.updateView(key, value);
      },
    });
  }
  
  //监听data对象
  observer(target) {
    //如果不是对象则不进行监听
    if (typeof target !== "object" || typeof target === null) {
      return;
    }
    // this.defineReactive(target);
    for (let key in target) {
      this.defineReactive(target, key, target[key]);
    }
  }
  
  //匹配Mustache语法的内容
  matchMustache(el) {
    const childNodes = el.childNodes;
    childNodes.forEach((item) => {
      if (item.nodeType === 3) {
        const _value = item.nodeValue;
        if (_value.trim().length) {
          let _valied = this.reg.test(_value);
          if (_valied) {
            const _match = item.nodeValue.match(this.reg)[1];
            this.modelValue[_match] = item.parentNode;
            item.parentNode.innerText = item.nodeValue.replace(
              this.reg,
              this._data[_match] || undefined
            );
          }
        }
      }
      item.childNodes && this.matchMustache(item);
    });
  }
  
  //绑定输入框的内容
  bindInput(key, value) {
    const _inputs = document.querySelectorAll("input");
    _inputs.forEach((item) => {
      const _model = item.getAttribute("v-model").trim();
      if (key && _model === key) {
        item.value = value;
      } else {
        item.value = this._data[_model];
        if (_model) {
          item.addEventListener("input", (e) => {
            this._data[_model] = e.target.value;
          });
        }
      }
    });
  }
  
  //更新视图
  updateView(key, value) {
    this.modelValue[key].innerText = value;
  }
  
  //改变数据 更新view
  setData(key, value) {
    this._data[key] = value;
    this.bindInput(key, value);
  }
}

Proxy监听

把上面defineReactive函数中Object.defineProperty的监听改为下面Proxy监听

 defineReactive(object) {
   let _this = this;
   this._data = new Proxy(object, {
     get(target, prop) {
       return Reflect.get(target, prop);
     },
     set(target, prop, value, receiver) {
       //修改触发更新
       _this.updateView(prop, value);
       return Reflect.set(...arguments);
     },
   });
 }
 
 observer(target) {
   //如果不是对象则不进行监听
   if (typeof target !== "object" || typeof target === null) {
     return;
   }
   this.defineReactive(target);
 } 

四、Object.defineProperty和Proxy的区别

  • 前者监听的是对象的属性,后者监听的是整个对象
  • 前者不能监听到新增属性和删除属性,后者可以监听
  • 前者不能监听到数组的变化,后者可以监听
  • 前者的兼容性比后者要好,因为ProxyES6提供的一个新的API
  • 前者的性能比后者要好

虽然proxy性能和兼容性差,但是proxy作为新标准将受到浏览器厂商重点持续的性能优化, 性能这块会逐步得到改善

五、参考文献

「Object.defineProperty」 深入浅出

数据双向绑定

Reflect.get

Reflect.set

Proxy