lgwebdream / FE-Interview

🔥🔥🔥 前端面试,独有前端面试题详解,前端面试刷题必备,1000+前端面试真题,Html、Css、JavaScript、Vue、React、Node、TypeScript、Webpack、算法、网络与安全、浏览器
https://lgwebdream.github.io/FE-Interview/
Other
6.76k stars 897 forks source link

Day340:动手实现一个简单的 mvvm 模型 #1170

Open Genzhen opened 2 years ago

Genzhen commented 2 years ago

每日一题会在下午四点在交流群集中讨论,五点小程序中更新答案 欢迎大家在下方发表自己的优质见解

二维码加载失败可点击 小程序二维码

扫描下方二维码,收藏关注,及时获取答案以及详细解析,同时可解锁800+道前端面试题。


简单实现参考

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>JS Bin</title>
  </head>
  <body>
    <div id="app">
      <input v-model="name" v-on:click="hello" type="text" />
      <input v-model="age" type="text" />
      <h3>{{name}}'s age is: {{age}}</h3>
    </div>
  </body>
</html>
// MVVM(Model-View-ViewModel) 单项绑定

function observe(data) {
  if (!data || typeof data !== "object") return;

  for (var key in data) {
    let val = data[key];
    let sub = new Subject();

    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,

      get: function () {
        console.log("get function in observer fun");
        console.log(val);
        if (currentObserver) {
          console.log("currentOb exist" + currentObserver);
          currentObserver.subscribeTo(sub);
        }
        return val;
      },

      set: function (newVal) {
        console.log("set function in observer fun");
        val = newVal;
        sub.notify();
      },
    });

    if (typeof val === "object") {
      observe(val);
    }
  }
}

let currentObserver = null;
let id = 0;

class Subject {
  constructor() {
    this.id = id++;
    this.observers = [];
  }

  addObserver(observer) {
    console.log("addObserver fun in class subject");
    this.observers.push(observer);
  }

  removerObserver(observer) {
    console.log("removeOb fun in class subject");
    var index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }

  notify() {
    console.log("notify fun in class subject");
    this.observers.forEach((observer) => {
      observer.update();
    });
  }
}

class Observer {
  constructor(vm, key, callback) {
    this.subjects = {};
    this.vm = vm;
    this.key = key;
    this.callback = callback;
    this.value = this.getValue();
  }

  getValue() {
    console.log("getValue fun in class observer");
    currentObserver = this;
    //     console.log('bug start, print vm,vm.data,key');
    //     console.log(this);
    //     console.log(this.vm);
    //     console.log(this.vm.$data);
    //     console.log(this.key);
    let value = this.vm.$data[this.key];
    currentObserver = null;
    return value;
  }

  update() {
    console.log("update fun in class observer");
    var oldVal = this.value;
    var newVal = this.getValue();
    if (oldVal !== newVal) {
      this.value = newVal;
      this.callback.bind(this.vm)(newVal, oldVal);
    }
  }

  subscribeTo(subject) {
    console.log("subscribeto fun in class observer");
    if (!this.subjects[subject.id]) {
      console.log("subscribe to... ", subject);
      subject.addObserver(this);
      this.subjects[subject.id] = subject;
    }
  }
}

class mvvm {
  constructor(opts) {
    this.init(opts);
    observe(this.$data);
    new Compile(this);
  }

  init(opts) {
    console.log("init fun in mvvm class");
    this.$ele = document.querySelector(opts.ele);
    this.$data = opts.data || {};
    this.methods = opts.methods || {};

    for (let key in this.methods) {
      this.methods[key] = this.methods[key].bind(this);
    }

    for (let key in this.$data) {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,

        get: () => {
          return this.$data[key];
        },

        set: (newVal) => {
          this.$data[key] = newVal;
        },
      });
    }
  }
}

class Compile {
  constructor(vm) {
    this.vm = vm;
    this.node = vm.$ele;
    this.compile();
  }

  compile() {
    console.log("compile fun in class Compile");
    this.traverse(this.node);
  }

  traverse(node) {
    console.log("traverse fun in class Compile");
    if (node.nodeType === 1) {
      this.compileNode(node);
      node.childNodes.forEach((childNode) => {
        this.traverse(childNode);
      });
    } else if (node.nodeType === 3) {
      this.renderText(node);
    }
  }

  // 处理指令
  compileNode(node) {
    let attrsArr = Array.from(node.attributes);
    attrsArr.forEach((attr) => {
      if (this.isModel(attr.name)) {
        this.bindModel(node, attr);
      } else if (this.isHandle(attr.name)) {
        this.bindHandle(node, attr);
      }
    });
  }

  bindModel(node, attr) {
    let key = attr.value;
    node.value = this.vm.$data[key];
    new Observer(this.vm, key, function (newVal) {
      node.value = newVal;
    });

    node.oninput = (e) => {
      this.vm.$data[key] = e.target.value;
    };
  }

  bindHandle(node, attr) {
    let startIndex = attr.name.indexOf(":") + 1;
    let endIndex = attr.name.length;
    let eventType = attr.name.substring(startIndex, attr.name.length);
    let method = attr.value;
    node.addEventListener(eventType, this.vm.methods[method]);
  }

  // 判断指令
  isModel(attrName) {
    return attrName === "v-model";
  }

  isHandle(attrName) {
    return attrName.indexOf("v-on") > -1;
  }

  // 渲染单变量
  renderText(node) {
    console.log("render fun in class Compile");
    let reg = /{{(.+?)}}/g;
    let match;
    while ((match = reg.exec(node.nodeValue))) {
      let raw = match[0];
      let key = match[1].trim();
      node.nodeValue = node.nodeValue.replace(raw, this.vm.$data[key]);
      new Observer(this.vm, key, function (newVal, oldVal) {
        node.nodeValue = node.nodeValue.replace(oldVal, newVal);
      });
    }
  }
}

let testMvvm = new mvvm({
  ele: "#app",

  data: {
    name: "fejavu",
    age: 23,
  },

  methods: {
    hello: function () {
      console.log("testmvvm start");
      console.log(this);
      console.log(this.vm);
      //       console.log(this)
      alert("hello! " + this.name);
    },
  },
});