理解VUE实现MVVM架构的方式

MVVM架构

MVVM架构是Model-View-ViewModel的简写,是一种软件设计模式,将应用程序分成3个层次:

  • Model (模型):可以在Model中定义数据修改和操作的业务逻辑
  • View (视图):代表UI 组件,可以理解为DOM
  • ViewModel (视图模型) :是同步View 和 Model的对象

MVVM架构要实现的是数据和视图的分离,并通过视图模型作为桥梁,来同步数据和视图,开发者只需关注业务逻辑,不需要手动进行繁琐的DOM操作。
更简单的说就是通过中间层视图模型来实现数据和视图的自动同步。

Vue.js

Vue是一套构建用户界面的渐进式框架,是目前很火的一个JavaScript MVVM库。
Vue中ViewModel就是一个Vue实例,它是Vue.js的核心。

在这里插入图片描述

理解VUE实现MVVM的方式

VUE将数据保存成纯js对象(类似JSON格式),观察到数据的变化,就对视图对应的内容进行更新。

数据来源可以是通过后台api请求到的,websocket等方式接收到的,可以是浏览器存储的(如:localStorage),也可以是前端自己定义的。

VUE通过模板来实现虚拟视图,监听到视图的变化,就通知数据发生变化。

模板看起来很像DOM,是 HTML、表达式、指令组合起来的特殊语法的字符串,模板被Vue转换为一个JS函数(render渲染函数),生成虚拟DOM,最终转换为html渲染页面。

原理

VUE用数据劫持结合发布者-订阅者模式的方式实现MVVM模式的。通过Object.defineProperty()来劫持数据对象各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

当执行 new Vue() 时,Vue 就进入了初始化阶段,实例化后有3个部分来实现实现数据与视图的同步:

  1. Observer 数据监听器/观察者
    劫持并监听数据 :负责遍历data选项中的属性,并用 Object.defineProperty 将它们转为 getter/setter,实现数据变化监听功能。

  2. Compile 解析器
    对元素节点进行遍历,对指令和插值等进行解析,初始化视图,并订阅数据更新视图。

  3. Watcher 订阅者
    Watcher将数据监听器和指令解析器连接起来,数据的属性变动时,执行指令绑定的相应回调函数,更新视图。

数据的订阅者可能不止一个,所以需要有一个消息订阅器Dep来收集订阅者,Dep负责添加订阅者和遍历订阅者列表通知订阅者更新视图。
具体说当数据发生变化时,Observer 中的 set 方法被触发,在set 方法中调用Dep.notify(),Dep开始遍历所有的订阅者,并调用订阅者的 update方法更新视图。
在这里插入图片描述
现在开始模拟实现VUE的数据绑定,首先定义一个视图,按照Vue模板的语法写这么一段html代码

<div id="app">
  <input type='text' v-model='title'>
  <p id="title">{{title}}</p>
  <p id="title2">{{title}}</p>
</div>

在这里插入图片描述
在这里我们期望的是在js中定义title的值,视图中不需通过dom就能够显直接示这个值,即视图中有两个title的订阅者,input定义了一个属性v-model将用来实现双向数据绑定(在input中输入自动同步title),后面我们将用Compile解析器来解析指令v-model和用{{}}包裹的数据。

1、模拟创建一个Vue构造函数,生成一个实例
  <script>
  function Vue(options){
    this.data = options.data;
    this.id = options.el;
    //在这里调用observer劫持数据...
    //在这里渲染视图中的数据...
  };
  let myVue=new Vue({
    el:'app',
    data:{
      title:'hello world',
      content:`Content of the *******`
    }
  });
  console.log(myVue);
  </script>
  <div id="app">
    <input type='text' v-model='title'>
    <p id="title">{{title}}</p>
    <p id="title2">{{title}}</p>
  </div>
2、劫持并监听数据
function Vue(options){
	...
	//劫持数据
    observer(this.data,this);
	...
};
function observer(data, vm){
  Object.keys(data).forEach(key=>{
    Object.defineProperty(vm,key,{
      get(){
        //初始化时在这里添加订阅...
        return data[key];
      },
      set(newVal){
        if(data[key] === newVal){ return };
        data[key] = newVal;
        console.log(`数据${key}被修改了`);
        //在这里通知变化...
      }
    })
  })
}

在数据劫持后,myVue的变化如下:
在这里插入图片描述
在数据劫持后,获取数据和修改数据时将触发get和set方法。
在这里插入图片描述

3、解析指令,初始化视图
//修改Vue构造函数
function Vue(options){
  ...
  //B 渲染视图中的数据
  let childNodes=document.getElementById(this.id).childNodes;
  Object.keys(childNodes).forEach((i)=>compile(childNodes[i], this));
  ...
};
//解析指令,初始化视图
function compile (node, vm){
  if (node.nodeType === 1) {
    // 1、遍历并解析节点属性,被解析的属性成为指令
    let attr=node.attributes;
    Object.keys(attr).forEach(i=>{
      //解析v-model指令
      if(attr[i].nodeName === 'v-model'){
        let attrName=attr[i].nodeValue;
        node.value = vm.data[attrName];  // 给节点的value属性赋值为v-model指定的数据的值
        node.removeAttribute('v-model');  //移除v-model
        // 在这里监听input事件...
      }
      //解析其他指令...
    });
    //2、遍历元素中的text节点,替换掉节点中的{{}}
    Object.keys(node.childNodes).forEach(i=>{
      let childNode=node.childNodes[i];
      if(childNode.nodeType === 3) {
        if(/\{\{(.*)\}\}/.test(childNode.nodeValue)) {
          let dataKey = RegExp.$1;
          childNode.nodeValue = vm.data[dataKey]; //给节点赋值,替换掉{{}}
          //在这里新建一个订阅者...
        }
      }
    })
  }
};

在这里插入图片描述
到这里数据就渲染出来了,但是通过input修改数据时不会有响应,下面来实现双向数据绑定

4、结合调度员和订阅者实现双向数据绑定

首先修改函数compile,①监听input事件 ②根据订阅了数据的dom新建订阅者

// 容纳订阅者的消息订阅器 Dep
  class Dep {
    constructor() {
      this.subs = [];
      this.target = null;
    };
    addSub(sub) {
      this.subs.push(sub);
      console.log('被添加的订阅者是:', sub);
    };
    notify() {
      this.subs.forEach(sub=>sub.update());
      console.log('要通知的订阅者:', this.subs);
    }
  };

  // 订阅者
  class Watcher {
    constructor(vm, node, name) {
      Dep.target = this;
      this.name = name;
      this.node = node;
      this.vm = vm;
      this.update();
      Dep.target = null;
    };
    update() {
      this.value = this.vm[this.name];  //给Watcher实例value属性赋值,同时将触发改数据的get方法
      this.node.nodeValue = this.value;  //更新视图
    };
  };
  function compile (node, vm){
    ...
    // 监听input事件
    node.addEventListener('input', function(e){
      vm[attrName] = e.target.value;  // 给相应的data属性赋值,触发绑定的set方法
    });
    ...
    //新建一个订阅者
    new Watcher(vm, childNode, dataKey);
    ...
}

接着修改observer函数

  function observer(data, vm) {
    let dep = new Dep();
    Object.keys(data).forEach(key => {
      Object.defineProperty(vm, key, {
        get() {
          //初始化时添加订阅
          if (Dep.target) {
            dep.addSub(Dep.target); //视图初始化时,向消息订阅器中添加一个订阅者(Dep.target)
          }
          return data[key];
        },
        set(newVal) {
          if (data[key] === newVal) {return}
          data[key] = newVal;
          //通知订阅者发生变化...
          dep.notify();
        }
      })
    })
  }

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值