mvvm原理实践

AmsChan ... 2021-10-06 框架原理
  • Mvvm
大约 4 分钟

# 前言

原理部分不在叙述,网上很多博客都有提,我是在掘金看了别的博主的文章(不好意思!耽误你的十分钟,让 MVVM 原理还给你 (opens new window)),然后按自己的理解模仿着实现了基础的 demo,在此基础上又添加了 methods、v-show 和@click 的实现。

由于自己还没彻底消化,所以叙述会有点烂 😢,当成一个菜鸟的学习记录吧!下面提到的东西可能是有错误的 😓

完整代码:github 传送门 (opens new window)

demo 演示:demo 传送门 (opens new window)

# 具体实现

# 数据代理

这里主要是 data 和 methods 的代理,代理的目的很简单,在 Vue 中,我们可以直接使用 this.xxx 来访问数据,而数据代理就是达到该目的的实现之一。

另外,如果 methods 里面的方法也能使用 this.xxx 来访问数据,那么还需要改变 method 的 this 指向,这里我写了个_bind()方法来实现

class MVVM {
  constructor(options = {}) {
    this.$options = options;
    this._proxy(options.data);
    this._proxy(options.methods);
    this._bind(options.methods);
  }
  // 将数据挂载到实例上,this代理options.data/methods,即可以直接使用this.key访问data的数据/methods的方法
  _proxy(data) {
    if (typeof data === "object") {
      for (let key in data) {
        Object.defineProperty(this, key, {
          enumerable: true, // 可被枚举
          set: function(newVal) {
            data[key] = newVal;
          },
          get: function() {
            return data[key];
          },
        });
      }
    }
  }
  // 改变methods里面的方法this指向
  _bind(methods) {
    for (let key in methods) {
      methods[key] = methods[key].bind(this);
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 数据劫持 + 订阅发布

数据劫持是通过Object.defineProperty()方法来实现,用 ES6 的Proxy来实现也可,有时间再更新。

这个模式好像是观察者+发布订阅的结合使用,不知道对不对,感觉是这样。

关于这两个设计模式可以看一下我的另外两篇文章:手撕观察者模式 (opens new window)手撕发布-订阅模式 (opens new window)

# Dep

通过这个类是发布-订阅的具体实现

class Dep {
  constructor() {
    this.subscribeObj = {};
  }

  subscribe(key, sub) {
    this.subscribeObj[key] = sub;
  }

  notify(key) {
    this.subscribeObj[key].update();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# Observer

这个类的作用主要是作为一个拦截器(数据劫持),订阅数据,发布通知,数据的获取和修改都需要经过这里(不出意外的话

class Observer {
  constructor(data) {
    for (let key in data) {
      let val = data[key];
      let dep = new Dep(); // 发布订阅类实例
      this._traverse(val); // 递归遍历,深度劫持
      Object.defineProperty(data, key, {
        enumerable: true, // 可被枚举
        set: function(newVal) {
          if (val !== newVal) {
            val = newVal;
            dep.notify(key); // 数据更新,通知订阅者
            return newVal;
          }
        },
        get: function() {
          Dep.target && dep.subscribe(key, Dep.target); // 增加订阅者,监听数据
          return val;
        },
      });
    }
  }
  _traverse(data) {
    if (data && typeof data === "object") {
      return new Observer(data);
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# Watcher

监听者,update函数就是用来更新数据的

class Watcher {
  constructor(vm, exp, cb) {
    // 实例本身,模板键值(如v-model="obj.key"的obj.key),回调函数
    this.vm = vm;
    this.exp = exp;
    this.cb = cb;
    Dep.target = this;
    let val = vm;
    exp.split(".").forEach((key) => {
      val = val[key];
    });
  }
  update() {
    let val = this.vm;
    this.exp.split(".").forEach((key) => {
      val = val[key];
    });
    this.vm.vShow.forEach((obj) => {  // 检查vShow数组里面存储的v-show指令绑定值的状态
      obj.node.style.display = this.vm[obj.key] ? "" : "none";
    });
    this.cb(val);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 数据编译

数据的更新啥的都弄好了,下面就得进行最后一步数据渲染了!

下面节点的更新有用到DocumentFragment,这里稍微偏一下题,使用DocumentFragment来来临时存储节点是有性能优化的作用的,比如下面的节点更新,如果一个一个节点的插入到DOM树中,就会有大量的DOM操作,引起多次的重绘和重排,从而影响到渲染的性能,将需要更新的节点存放到DocumentFragment中,最后再一次性更新,只有一次DOM操作,因此这里使用DocumentFragment是有原因滴~

class Compile {
  constructor(el, vm) {
    vm.$el = document.querySelector(el);
    let fragment = document.createDocumentFragment();
    let child;
    while ((child = vm.$el.firstChild)) {
      fragment.appendChild(child);
    }
    this._replace(fragment, vm);
    // 再将文档碎片放入el中
    vm.$el.appendChild(fragment);
  }
  _replace(fragment, vm) {
    Array.from(fragment.childNodes).forEach((node) => {
      let text = node.textContent;
      let reg = /\{\{(.*?)\}\}/g; // 匹配{{}}的内容
      /*
       * nodeType: 1 元素节点,3 文本节点
       */
      if (node.nodeType === 3 && reg.test(text)) {
        function _replaceText() {
          // 替换节点文本
          node.textContent = text.replace(reg, (matched, placeholder) => {
            console.log(matched, placeholder);
            new Watcher(vm, placeholder, _replaceText);
            return placeholder.split(".").reduce((val, key) => {
              return val[key];
            }, vm);
          });
        }
        _replaceText();
      }
      if (node.nodeType === 1) {
        let attrs = node.attributes; // 获取dom节点的属性
        Array.from(attrs).forEach((attr) => {
          console.log(attr);
          let name = attr.name;
          let exp = attr.value;
          if (name.includes("v-model")) {
            // v-model
            node.value = vm[exp];
          } else if (name.includes("@click")) {
            // 绑定点击事件
            node.addEventListener("click", vm[exp]);
          } else if (name.includes("v-show")) {
            // v-show指令处理
            vm.vShow.push({
              node,
              type: "v-show",
              key: exp,
            });
            node.style.display = vm[exp] ? "" : "none";
            console.log(vm);
          }
          new Watcher(vm, exp, function(newVal) {
            node.value = newVal; // 当watcher触发时会自动将内容放进输入框中
          });
          node.addEventListener("input", function(e) {
            // 监听input事件,输入时更新数据
            let newVal = e.target.value;
            vm[exp] = newVal;
          });
        });
      }
      if (node.childNodes && node.childNodes.length) {
        this._replace(node, vm); // 递归遍历节点
      }
    });
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

# 总结

目前还需要一段时间去消化这些知识,这篇就当作学习记录吧!不敢说是技术分享,讲的实在太烂了呜呜呜…