简单手动实现 Vue2 MVVM 过程

4 篇文章 0 订阅
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>手动实现MVVM</title>
</head>

<body>
  <div id="app">
    <input type="text" v-model="name">
    <input type="text" v-model="age">
    <h1>{{name}}</h1>
    <h2>{{age}}</h2>
  </div>
</body>

</html>

<script>
  // 实现逻辑
  // @1 定义方法 获取参数

  function Vue(option) {
    //  @2 私有属性挂载 $el ,$data
    this.$el = document.querySelector(option.el);
    this.$data = option.data;
    observe(this.$data);// @3 数据劫持
    nodeToFragment(this.$el, this);//  负责模板编译 将Vue 语法变成真实变量 @4
  };

  /** 数据劫持部分*/
  function observe(data) {
    // @3 判断是否是对象 1
    if (({}).toString.call(data) !== '[object Object]') return;
    // 2 获取所有属性名,方便劫持 
    let keys = Object.keys(data);
    keys.forEach(key => {
      defineReactive$$1(data, key, data[key])
    })
  };

  function defineReactive$$1(target, key, val) {
    // 实现数据劫持 @3
    let dep = new Dep(); // @6 每个key 都有自己的订阅器 Dep
    Object.defineProperty(target, key, {
      enumerable: true,//是否可枚举
      get() {
        // console.log('get', val)
        if (Dep.target) {
          // @7 这里加判断是为了保证,只有Watcher 订阅才会触发
          dep.addSub(Dep.target); // 事件池中的订阅器
        }
        return val  // 返回值
      },
      set(newVal) {
        if (newVal !== val) {
          //  console.log(newVal, 'newVal====')
          val = newVal; // 设置的新值
          dep.notify(); // 通知事件执行
        }

      }
    })
  }

  /** 模板编译部分*/
  function nodeToFragment(el, vm) {
    // 文档节点转到 文档碎片上 @4
    let fragment = document.createDocumentFragment(); //创建文档碎片
    let child; // 每一个节点
    while (child = el.firstChild) {
      /** xx.appendChild: 将一个节点附加到指定父节点的子节点列表的末尾处,如果该文本节点已存在则覆盖
       * xx.firstChild : 只读属性返回树中节点的第一个子节点,如果节点无子节点,则返回 null。
      */
      // debugger;
      compile(child, vm); // 实现将每个模板都编译 @5
      fragment.appendChild(child); //将节点 移到文档碎片
    }
    el.appendChild(fragment); // 再将文档碎片的内容 一个一个移到接节点
  }

  function compile(node, vm) {
    const { $data } = vm;
    // 编译node节点 @5  需要判断节点类型
    /**
     *  1.区分文本节点还是元素节点,元素节点需要考虑 行内属性和其子节点
     *  2. 如果是文本节点 直接替换  节点类型 : 1:元素节点(属性nodeType) / 3:文本节点 / 8: 注释节点 /9:根节点
     *  3. node.attributes : 属性返回该元素所有属性节点的一个实时集合,返回值是对象不是数组,无法使用数组方法
     * 4 node.childNodes 返回包含指定节点的子节点的集合
    */
    switch (node.nodeType) {
      case 1:
        // 元素节点
        let attrs = node.attributes; // 获取所有行内属性 type,v-model ...
        // debugger;
        [...attrs].forEach(e => {
          // 获取到 v-xxx 行内属性 以及 v-xxx 对应的 值 xxx
          if (/^v-/.test(e.nodeName)) {
            // v- 属性 ,
            let vName = e.nodeValue;//对应的词汇
            let val = $data[vName]; // 拿到 data对应的 key对应的值
            new Watcher(node, vName, vm); // @6  
            node.value = val;
            node.addEventListener('input', (e) => { // input 事件 绑定 @8
              vm.$data[vName] = e.target.value
            })
          }
        });
        // 如果存在子节点
        [...node.childNodes].forEach(x => {
          compile(x, vm)
        })
        break;
      case 3:
        // 获取对应文本节点 ,将文本里的小胡子 转为 变量对应的 值

        let str = node.textContent;

        // 获取文本 
        if (/\{\{(\w+)\}\}/.test(str)) {
          // 存在 小胡子语法
          str = str.replace(/\{\{(\w+)\}\}/, (a, b) => { // 内容替换
            new Watcher(node, b, vm); // @6  
            return $data[b]
          })
          node.textContent = str;
        };


        break;
      case 8:
        // ... 
        break;
      case 9:
        // ... 
        break;
    }

  }


  // 观察订阅器@6

  /**
   * 1 创造订阅器
  */
  class Dep {
    constructor () {
      this.subs = []; // 事件池
    };
    addSub(sub) {
      this.subs.push(sub); // 添加事件
    }
    notify() {
      // 
      this.subs.forEach(subs => {
        // 负责通知 各个事件 ,subs 是订阅者实例
        subs.updated()
      })
    }
  }
  /**
   * 1 创造订阅者
  */

  class Watcher {
    constructor (node, key, vm) {
      Dep.target = this;
      this.node = node;
      this.key = key;
      this.vm = vm;
      this.getValue(); // 把当前Watcher 放进对应的事件池
      Dep.target = null;
    };
    getValue() {
      this.value = this.vm.$data[this.key]; //触发 get
    };
    updated() {
      // 负责更新DOM 
      this.getValue(); // 获取新的Value值
      switch (this.node.nodeType) {
        case 1:
          // 元素节点 input 的情况下
          this.node.value = this.value;
          break;
        case 3:
          this.node.textContent = this.value;
          break;
        case 8:
          // ... 
          break;
        case 9:
          // ... 
          break;
      }
    }
  }
</script>

<script>
  // 调用

  let vm = new Vue({
    el: '#app',
    data: {
      name: '手写mvvm',
      age: 18
    }
  })

</script>

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值