Vue2.x MVVM简单实现

226 阅读2分钟

前言

笔者理解的Vue2.x的核心原理:

  • 基于数据劫持订阅发布模式实现数据绑定(M ==> V)
  • 虚拟dom(本文不涉及)

MVVM模式

MVVM模式实现了Model到View的数据绑定,让开发者只用关心Model的变化。而传统的MVC模式中的Controller需要开发者自己实现根据Model变化引起的View变化(jquery只解决了dom操作的复杂度)。

下表是三大前端框架对于数据绑定的实现

三大前端框架 实现方式
React setState+虚拟dom
Vue 数据劫持+虚拟dom
Angular 特定事件的脏值检查

下图是Vue实现数据绑定的逻辑图

简易Vue实现

Vue初始化分为数据劫持模版编译两部分

数据劫持中会遍历data属性,每次遍历中会实例化一个新的依赖收集者(订阅者)类,也就是说每个属性对应一个dep。get方法中会收集监听者Watcher实例。set方法会通知dep实例执行更新

代码只是简单实现了文本节点的数据绑定,未实现元素节点的指令解析。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue</title>
</head>

<body>
  <div id="root">
    <p>{{name}}</p>
    <p>{{age}}</p>
  </div>
</body>
  document.addEventListener('DOMContentLoaded', function() {
    const app = new Vue({
      el: '#root',
      data: {
        name: 'Neil',
        age: 18
      }
    })
    setTimeout(() => {
      app.$data.age = 20;
    }, 1000)
  })
  // Vue类
  class Vue{
    constructor(options) {
      this.$el = document.querySelector(options.el);
      this.$data = options.data;
      // 数据劫持
      this.observe(this.$data);
      // 模版编译
      this.compile(this.$el)
    }
		// 数据劫持
    observe(data) {
      Object.keys(data).map(key => {
        // 创建Dep实例
        const dep = new Dep();
        data['_' + key] = data[key];
        // 利用闭包在每一个属性中保存dep
        Object.defineProperty(data, key, {
          get() {
            // 初始化编译时,会将使用了该属性的文本节点赋值给dep.target,并添加到订阅数组。
            Dep.target && dep.addSubNode(Dep.target);
            return data['_' + key]
          },
          set(newValue) {
            if (newValue === data['_' + key]) {
              return
            }
            data['_' + key] = newValue;
            dep.notify();
          }
        })
      })
    }
		// 模版编译
    compile(el) {
      const nodeList = Array.from(el.childNodes);
      nodeList.forEach(node => {
        if (node.nodeType === 1) {
          console.log('处理元素节点');
          // 递归解析
          this.compile(node);
        } else if (node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)) {
          console.log('处理文本节点');
          // 初始化文本节点
          node.textContent = this.$data[RegExp.$1];
          // 依赖收集
          new Watcher(this, RegExp.$1, function(newValue) {
            node.textContent = newValue;
          });          
        }
      })
    }
  }
	// 依赖收集类(订阅者,订阅属性变化)
  class Dep {
    constructor() {
      this.subNodeList = []
    }

    addSubNode(subNode) {
      this.subNodeList.push(subNode)
    }

    notify() {
      this.subNodeList.map(subNode => {
        subNode.update()
      })
    }
  }
	// 监听者类(new构造后自动被添加到对应属性的订阅队列)
  class Watcher {
    constructor(vm, key, callback) {
      this.vm = vm;
      this.key = key;
      this.callback = callback;
			// 将当前Watcher对象添加到订阅队列
      Dep.target = this;
      this.vm.$data[key];
      Dep.target = null;
    }

    update() {
      const newVal = this.vm.$data[this.key];
      this.callback(newVal);
    }
  }
</script>

</html>