第35题 实现一个简易的MVVM

实现一个简易的MVVM我会分为这么几步来:

  1. 首先我会定义一个类Vue,这个类接收的是一个options,那么其中可能有需要挂载的根元素的id,也就是el属性;然后应该还有一个data属性,表示需要双向绑定的数据
  2. 其次我会定义一个Dep类,这个类产生的实例对象中会定义一个subs数组用来存放所依赖这个属性的依赖,已经添加依赖的方法addSub,删除方法removeSub,还有一个notify方法用来遍历更新它subs中的所有依赖,同时Dep类有一个静态属性target它用来表示当前的观察者,当后续进行依赖收集的时候可以将它添加到dep.subs中。
  3. 然后设计一个observe方法,这个方法接收的是传进来的data,也就是options.data,里面会遍历data中的每一个属性,并使用Object.defineProperty()来重写它的getset,那么这里面呢可以使用new Dep()实例化一个dep对象,在get的时候调用其addSub方法添加当前的观察者Dep.target完成依赖收集,并且在set的时候调用dep.notify方法来通知每一个依赖它的观察者进行更新
  4. 完成这些之后,我们还需要一个compile方法来将HTML模版和数据结合起来。在这个方法中首先传入的是一个node节点,然后遍历它的所有子级,判断是否有firstElmentChild,有的话则进行递归调用compile方法,没有firstElementChild的话且该child.innderHTML用正则匹配满足有/\{\{(.*)\}\}/项的话则表示有需要双向绑定的数据,那么就将用正则new Reg('\\{\\{\\s*' + key + '\\s*\\}\\}', 'gm')替换掉``是其为msg变量。
  5. 完成变量替换的同时,还需要将Dep.target指向当前的这个child,且调用一下this.opt.data[key],也就是为了触发这个数据的get来对当前的child进行依赖收集,这样下次数据变化的时候就能通知child进行视图更新了,不过在最后要记得将Dep.target指为null哦(其实在Vue中是有一个targetStack栈用来存放target的指向的)
  6. 那么最后我们只需要监听documentDOMContentLoaded然后在回调函数中实例化这个Vue对象就可以了

coding :

需要注意的点:

  • childNodes会获取到所有的子节点以及文本节点(包括元素标签中的空白节点)
  • firstElementChild表示获取元素的第一个字元素节点,以此来区分是不是元素节点,如果是的话则调用compile进行递归调用,否则用正则匹配
  • 这里面的正则真的不难,大家可以看一下

完整代码如下:

    <!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>MVVM</title>
      </head>
      <body>
        <div id="app">
          <h3>姓名</h3>
          <p>{{name}}</p>
          <h3>年龄</h3>
          <p>{{age}}</p>
        </div>
      </body>
    </html>
    <script>
      document.addEventListener(
        "DOMContentLoaded",
        function () {
          let opt = { el: "#app", data: { name: "等待修改...", age: 20 } };
          let vm = new Vue(opt);
          setTimeout(() => {
            opt.data.name = "jing";
          }, 2000);
        },
        false
      );
      class Vue {
        constructor(opt) {
          this.opt = opt;
          this.observer(opt.data);
          let root = document.querySelector(opt.el);
          this.compile(root);
        }
        observer(data) {
          Object.keys(data).forEach((key) => {
            let obv = new Dep();
            data["_" + key] = data[key];
    
            Object.defineProperty(data, key, {
              get() {
                Dep.target && obv.addSubNode(Dep.target);
                return data["_" + key];
              },
              set(newVal) {
                obv.update(newVal);
                data["_" + key] = newVal;
              },
            });
          });
        }
        compile(node) {
          [].forEach.call(node.childNodes, (child) => {
            if (!child.firstElementChild && /\{\{(.*)\}\}/.test(child.innerHTML)) {
              let key = RegExp.$1.trim();
              child.innerHTML = child.innerHTML.replace(
                new RegExp("\\{\\{\\s*" + key + "\\s*\\}\\}", "gm"),
                this.opt.data[key]
              );
              Dep.target = child;
              this.opt.data[key];
              Dep.target = null;
            } else if (child.firstElementChild) this.compile(child);
          });
        }
      }
    
      class Dep {
        constructor() {
          this.subNode = [];
        }
        addSubNode(node) {
          this.subNode.push(node);
        }
        update(newVal) {
          this.subNode.forEach((node) => {
            node.innerHTML = newVal;
          });
        }
      }
    </script>
Last Updated:
Contributors: leeguooooo