一种简化版MVVM的实现

代码功能流程图

在这里插入图片描述

源代码

index.html

<html>
  <head>
    <meta charset="UTF-8" />
  </head>
  <body>
    <div id="app">
      <!-- 双向数据绑定 靠的是表单 -->
      <input type="text" v-model="message.a" />
      <div>{{message.a}}</div>
      {{message.a}} {{b}}
    </div>
    <script src="./src/watcher.js"></script>
    <script src="./src/observer.js"></script>
    <script src="./src/compile.js"></script>
    <script src="./src/MVVM.JS"></script>
    <script>
      // 我们的数据一般都挂载在vm上
      let vm = new MVVM({
        el: "#app",
        data: {
          message: { a: "foo" },
          b: "bar",
        },
      });
    </script>
  </body>
</html>

MVVM.JS

class MVVM {
  constructor(options) {
    this.$el = options.el;
    this.$data = options.data;
    if (this.$el) {
      new Observer(this.$data);
      // 将数据代理到实例上直接操作实例即可,不需要通过vm.$data来进行操作
      this.proxyData(this.$data);
      new Compile(this.$el, this);
    }
  }
  proxyData(data) {
    Object.keys(data).forEach((key) => {
      Object.defineProperty(this, key, {
        get() {
          return data[key];
        },
        set(newValue) {
          data[key] = newValue;
        },
      });
    });
  }
}

compile.js

class Compile {
  constructor(el, vm) {
    // 看看传递的元素是不是DOM,不是DOM我就来获取一下~
    this.el = this.isElementNode(el) ? el : document.querySelector(el);
    this.vm = vm;
    if (this.el) {
      // 如果这个元素能获取到 我们才开始编译
      // 1.先把这些真实的DOM移入到内存中 fragment (性能优化)
      let fragment = this.node2fragment(this.el);
      // 2.编译 => 提取想要的元素节点 v-model 和文本节点 {{}}
      this.compile(fragment);
      // 3.把编译好的fragment在塞回到页面里去
      this.el.appendChild(fragment);
    }
  }
  node2fragment(el) {
    // 需要将el中的内容全部放到内存中
    // 文档碎片 内存中的dom节点
    let fragment = document.createDocumentFragment();
    let firstChild;
    while ((firstChild = el.firstChild)) {
      fragment.appendChild(firstChild);
      // appendChild具有移动性
    }
    return fragment; // 内存中的节点
  }
  compile(fragment) {
    // 需要递归 每次拿子元素
    let childNodes = fragment.childNodes;
    Array.from(childNodes).forEach((node) => {
      if (this.isElementNode(node)) {
        // 是元素节点,还需要继续深入的检查
        // 这里需要编译元素
        this.compileElement(node);
        this.compile(node);
      } else {
        // 文本节点
        // 这里需要编译文本
        this.compileText(node);
      }
    });
  }
  compileElement(node) {
    // 带v-model v-text
    let attrs = node.attributes; // 取出当前节点的属性
    Array.from(attrs).forEach((attr) => {
      // 判断属性名字是不是包含v-model
      let attrName = attr.name;
      if (this.isDirective(attrName)) {
        // 取到对应的值放到节点中
        let expr = attr.value;
        let [, type] = attrName.split("-"); //
        // 调用对应的编译方法 编译哪个节点,用数据替换掉表达式
        CompileUtil[type](node, this.vm, expr);
      }
    });
  }
  compileText(node) {
    let expr = node.textContent; // 取文本中的内容
    let reg = /\{\{([^}]+)\}\}/g; // {{a}} {{b}} {{c}}
    if (reg.test(expr)) {
      // 调用编译文本的方法 编译哪个节点,用数据替换掉表达式
      CompileUtil["text"](node, this.vm, expr);
    }
  }
  /* 专门写一些辅助的方法 */
  isElementNode(node) {
    return node.nodeType === 1;
  }
  /* 核心的方法 */
  /*辅助的方法*/
  // 是不是指令
  isDirective(name) {
    return name.includes("v-");
  }
}

CompileUtil = {
  text(node, vm, expr) {
    // 文本处理
    let updateFn = this.updater["textUpdater"];
    // 文本比较特殊 expr可能是'{{message.a}} {{b}}'
    // 调用getTextVal方法去取到对应的结果
    let value = this.getTextVal(vm, expr);
    expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
      new Watcher(vm, arguments[1], (newValue) => {
        // 如果数据变化了,文本节点需要重新获取依赖的属性更新文本中的内容
        updateFn && updateFn(node, this.getTextVal(vm, expr));
      });
    });

    updateFn && updateFn(node, value);
  },
  getTextVal(vm, expr) {
    // 获取编译文本后的结果
    return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
      // 依次去去数据对应的值
      return this.getVal(vm, arguments[1]);
    });
  },
  getVal(vm, expr) {
    // 获取实例上对应的数据
    expr = expr.split("."); // {{message.a}} [message,a] 实现依次取值
    // vm.$data.message => vm.$data.message.a
    return expr.reduce((prev, next) => {
      return prev[next];
    }, vm.$data);
  },
  setVal(vm, expr, value) {
    expr = expr.split(".");
    return expr.reduce((prev, next, currentIndex) => {
      if (currentIndex === expr.length - 1) {
        return (prev[next] = value);
      }
      return prev[next];
    }, vm.$data);
  },
  model(node, vm, expr) {
    // 输入框处理
    let updateFn = this.updater["modelUpdater"];
    // 这里应该加一个监控 数据变化了 应该调用这个watch的callback
    new Watcher(vm, expr, (newValue) => {
      // 当值变化后会调用cb 将新的值传递过来
      updateFn && updateFn(node, this.getVal(vm, expr));
    });
    node.addEventListener("input", (e) => {
      let newValue = e.target.value;
      // 监听输入事件将输入的内容设置到对应数据上
      this.setVal(vm, expr, newValue);
    });

    updateFn && updateFn(node, this.getVal(vm, expr));
  },
  // ------------------------------------
  updater: {
    // 文本更新
    textUpdater(node, value) {
      node.textContent = value;
    },
    // 输入框更新
    modelUpdater(node, value) {
      node.value = value;
    },
  },
};

observer.js

class Dep {
  constructor() {
    // 订阅的数组
    this.subs = [];
  }
  addSub(watcher) {
    this.subs.push(watcher);
  }
  notify() {
    this.subs.forEach((watcher) => watcher.update());
  }
}

class Observer {
  constructor(data) {
    this.observe(data);
  }
  observe(data) {
    // 要对这个data数据将原有的属性改成set和get的形式
    // defineProperty针对的是对象
    if (!data || typeof data !== "object") {
      return;
    }
    // 要将数据 一一劫持 先获取取到data的key和value
    Object.keys(data).forEach((key) => {
      // 定义响应式变化
      this.defineReactive(data, key, data[key]);
      this.observe(data[key]); // 深度递归劫持
    });
  }
  // 定义响应式
  defineReactive(obj, key, value) {
    let that = this;
    let dep = new Dep(); // 每个变化的数据 都会对应一个数组,这个数组是存放所有更新的操作
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        // 当取值时调用的方法
        Dep.target && dep.addSub(Dep.target);
        return value;
      },
      set(newValue) {
        if (newValue != value) {
          that.observe(newValue);
          value = newValue;
          dep.notify(); // 通知所有人 数据更新了
        }
      },
    });
  }
}

// 更新Observer中的defineReactive

watcher.js

class Watcher {
  // 因为要获取老值 所以需要 "数据" 和 "表达式"
  constructor(vm, expr, cb) {
    this.vm = vm;
    this.expr = expr;
    this.cb = cb;
    // 先获取一下老的值 保留起来
    this.value = this.get();
  }
  // 老套路获取值的方法,这里先不进行封装
  getVal(vm, expr) {
    expr = expr.split(".");
    return expr.reduce((prev, next) => {
      return prev[next];
    }, vm.$data);
  }
  get() {
    Dep.target = this;
    let value = this.getVal(this.vm,this.expr); // 会调用属性对应的get方法
    Dep.target = null;
    return value;
  }
  // 对外暴露的方法,如果值改变就可以调用这个方法来更新
  update() {
    let newValue = this.getVal(this.vm, this.expr);
    let oldValue = this.value;
    this.value = newValue;
    if (newValue != oldValue) {
      this.cb(newValue); // 对应watch的callback
    }
  }
}

参考文章:
看完这篇关于MVVM的文章,面试通过率提升了80%

属于自己的文字,理解,观点,欢迎交流

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值