vue MVVM双向绑定实例详解(数据劫持+发布者-订阅者模式)

Vue MVVM双向绑定实例详解(数据劫持+发布者-订阅者模式)

一、MVVM模式

MVVM是Model-View-ViewModel的缩写。在前端开发中,MVVM是一种设计模式,它将数据(Model)、业务逻辑(ViewModel)和页面(View)分离开来。其中,ViewModel充当了连接View和Model的纽带,通过ViewModel将数据绑定到View中实现了数据的双向绑定。

二、双向绑定的实现

在Vue中实现双向绑定的方式,主要是通过数据劫持和发布者-订阅者模式的结合来实现的。

1. 数据劫持

在Vue中,通过Object.defineProperty()方法将数据属性转化为getter和setter,来实现数据劫持,当数据属性发生变化时,通过setter方法通知模板进行更新。

下面是一个数据劫持的示例。

var obj = {};
var value = '';
Object.defineProperty(obj, 'msg', {
  get() {
    console.log('get value');
    return value;
  },
  set(newValue) {
    console.log('set value');
    value = newValue;
  }
})
obj.msg = 'hello';
console.log(obj.msg);

运行结果:

set value
get value
hello

输出结果可以看到,当对obj.msg属性进行赋值时,会触发setter方法,然后再通过getter方法获取值,实现了对数据的劫持。

2. 发布者-订阅者模式

在Vue中,通过对属性的getter和setter进行封装,实现属性的订阅和发布,来实现对数据的双向绑定。

下面是一个发布者-订阅者模式的示例。

class Dep {
  constructor() {
    this.subs = [];
  }

  addSub(sub) {
    this.subs.push(sub);
  }

  notify() {
    this.subs.forEach(sub => {
      sub.update();
    })
  }
}

class Watcher {
  constructor() {
    Dep.target = this;
  }

  update() {
    console.log('update view');
  }
}

Dep.target = null;

var dep = new Dep();
var watcher = new Watcher();
dep.addSub(watcher);
dep.notify();

运行结果:

update view

输出结果可以看到,在订阅者Watcher中,通过将Dep.target属性标记为当前Watcher,然后在属性的getter方法中,向依赖的订阅者列表中添加当前Watcher,然后在属性的setter方法中,通过Dep.notify()方法通知依赖该数据的所有订阅者进行更新。

三、数据双向绑定的实现

数据双向绑定实现方式有很多,下面以Vue的实现为例,进行说明。

1. 实现双向绑定的构造函数

首先,我们需要实现一个构造函数,用于观察并监听ViewModel中的数据,当数据发生改变时,触发回调函数。

function Observer(data) {
  this.data = data;
  this.walk(data);
}

Observer.prototype = {
  walk(data) {
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key]);
    })
  },

  defineReactive(data, key, val) {
    let dep = new Dep();
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get() {
        if (Dep.target) {
          dep.addSub(Dep.target);
        }
        return val;
      },
      set(newValue) {
        if (val === newValue) {
          return;
        }
        val = newValue;
        dep.notify();
      }
    })
  }
}

解析:

Observer构造函数接收一个参数data,data表示要建立双向绑定的ViewModel对象。

walk()方法遍历data的所有属性,并调用defineReactive()方法。

defineReactive()方法对data对象的每个属性进行处理。首先创建一个Dep实例,用于存储该属性所依赖的订阅者。然后使用Object.defineProperty()方法将data[key]转换为getter/setter方法,当属性被读取时,如果有订阅者依赖该属性,就将该订阅者添加到Dep实例中,当属性被修改时,通知Dep实例告知所有订阅者进行更新。

2. 实现模板编译

为了一个页面的数据与ViewModel建立双向绑定,需要实现模板编译的功能,将模板中的表达式与ViewModel中的数据绑定起来,这里我们可以使用正则表达式进行解析。

function compile(el) {
  var element = document.querySelector(el);

  var childNodes = element.childNodes;

  Array.prototype.slice.call(childNodes).forEach(node => {
    if (node.nodeType === 3) {
      let text = node.textContent;
      let reg = /\{\{(.*)\}\}/;
      if (reg.test(text)) {
        let matchValue = RegExp.$1.trim();
        new Watcher(vm, matchValue, function(newVal) {
          node.textContent = text.replace(reg, newVal);
        })
      }
    } else if (node.nodeType === 1) {
      let nodeAttrs = node.attributes;
      Array.prototype.slice.call(nodeAttrs).forEach(attr => {
        let attrName = attr.name;
        let attrValue = attr.value;
        if (attrName.indexOf('v-') === 0) {
          let dir = attrName.substring(2);
          if (dir === 'model') {
            new Watcher(vm, attrValue, function(newVal) {
              node.value = newVal;
            })
            node.addEventListener('input', e => {
              let newValue = e.target.value;
              if (attrValue.indexOf('.') > -1) {
                let keys = attrValue.split('.');
                let data = vm;
                keys.forEach((key, index) => {
                  if (index === keys.length - 1) {
                    data[key] = newValue;
                  } else {
                    data = data[key];
                  }
                })
              } else {
                vm[attrValue] = newValue;
              }
            })
          }
        }
      })
    }

    if (node.childNodes && node.childNodes.length) {
      compile(node);
    }
  })
}

解析:

compile()函数接收一个参数el,el表示需要编译的模板元素。首先获取该元素下的所有子节点,通过Array.prototype.slice.call()将NodeList转换为数组。

遍历每个子节点,如果是文本节点(nodeType=3),则使用正则表达式将模板中的表达式解析出来,并创建一个Watcher,并在回调函数中更新视图。如果是元素节点(nodeType=1),则遍历节点的所有属性,并判断属性名是否以'v-'开头,如果是,则进行特殊处理,将该属性值与ViewModel中的数据建立双向绑定,并添加事件监听器,当该元素的value值修改时,将数据同步更新到ViewModel中。

最后,如果该节点还包含子节点,则递归遍历子节点,进行编译。

3. 实现Watcher

Watcher是连接ViewModel和View的桥梁,当ViewModel中的数据发生变化时,Watcher会触发回调函数来更新视图,同时将新的值传递到回调函数中。

function Watcher(vm, exp, cb) {
  this.vm = vm;
  this.exp = exp;
  this.cb = cb;
  this.value = this.get();
}

Watcher.prototype = {
  update() {
    let newVal = this.get();
    let oldVal = this.value;
    if (newVal !== oldVal) {
      this.value = newVal;
      this.cb.call(vm, newVal, oldVal);
    }
  },

  get() {
    Dep.target = this;
    let value = this.getVMValue(this.vm, this.exp);
    Dep.target = null;
    return value;
  },

  getVMValue(vm, exp) {
    let data = vm;
    exp.split('.').forEach(key => {
      data = data[key];
    })
    return data;
  }
}

解析:

Watcher构造函数接收三个参数,vm表示ViewModel对象,exp表示ViewModel中的数据对象的属性,cb为回调函数,在数据发生变化时会触发。

在Watcher中,通过get()方法获取ViewModel中的数据对象的属性的值,并将Watcher对象存储到Dep实例中的subs数组中。

当数据发生变化时,触发Dep的set()方法,通知Dep实例中的所有订阅者进行更新,并调用Watcher的update()方法来更新视图,同时将新的值传递到回调函数cb中。

四、示例说明

下面给出两个示例,分别演示了基本的数据绑定和模板编译如何工作。

示例1:基本双向绑定

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>双向绑定实例1</title>
</head>
<body>
  <div id="app">
    <input type="text" v-model="msg">
    <p>{{ msg }}</p>
  </div>

  <script src="observer.js"></script>
  <script src="watcher.js"></script>
  <script src="compile.js"></script>
  <script>
    let vm = new Observer({
      msg: 'hello world'
    })
    compile('#app', vm)
  </script>
</body>
</html>

当用户在input框中输入文字时,可以看到下面的p标签实时更新为input框中的文字。

示例2:模板编译

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>双向绑定实例2</title>
</head>
<body>
  <div id="app">
    <ul>
      <li v-for="item in list">
        <input type="checkbox" v-model="item.checked">{{ item.title }}
      </li>
    </ul>
    <button v-on:click="addItem">新增</button>
  </div>

  <script src="observer.js"></script>
  <script src="watcher.js"></script>
  <script src="compile.js"></script>
  <script>
    let vm = new Observer({
      list: [
        {title: '任务1', checked: true},
        {title: '任务2', checked: false},
        {title: '任务3', checked: false}
      ]
    })
    compile('#app', vm)
  </script>
</body>
</html>

该示例中,展示了如何在模板中使用v-for、v-model和v-on等指令,并通过模板编译将其和ViewModel中的数据建立起有效的双向绑定关系。

用户可以尝试增加、选择或取消选择其中一个任务,并观察页面的变化。

五、总结

本文主要介绍了Vue中实现数据双向绑定的原理以及基本实现方法,通过对数据劫持和发布者-订阅者模式的结合,实现了数据的双向绑定。同时,我们还实现了模板编译的功能,将ViewModel中的数据与模板建立双向绑定,在数据发生变化时,实现了及时地更新视图。这就实现了Vue的MVVM模式,使得前端开发更加容易维护和扩展。

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:vue MVVM双向绑定实例详解(数据劫持+发布者-订阅者模式) - Python技术站

(0)
上一篇 2023年6月8日
下一篇 2023年6月8日

相关文章

  • node实现的爬虫功能示例

    下面我来为你详细讲解如何使用Node.js实现网页爬虫功能。 准备工作 在开始编写代码之前,我们需要先安装Node.js和一些相关的模块。具体步骤如下: 1.1 安装Node.js 请先在官网https://nodejs.org/zh-cn/下载Node.js的安装包,然后按照提示安装即可。 1.2 安装Request模块 我们使用Request模块来发起h…

    node js 2023年6月8日
    00
  • nodejs简单访问及操作mysql数据库的方法示例

    针对“nodejs简单访问及操作mysql数据库的方法示例”的攻略,可以分为以下几个步骤: 1. 安装 mysql 和 mysql2 包 首先需要在项目中安装 mysql 和 mysql2 包,这两个包可以通过 npm 进行安装。 npm install mysql mysql2 –save 这里需要注意的是,mysql2 是 mysql 的升级版,性能更…

    node js 2023年6月9日
    00
  • Nodejs进阶之服务端字符编解码和乱码处理

    Nodejs进阶之服务端字符编解码和乱码处理 字符编解码 在服务端处理字符编解码时,需要注意以下几个方面: 请求头中的字符编码 浏览器发送请求时,会将当前页面的字符编码信息放在请求头中,服务端在解析请求时需注意此处的字符编码信息。 示例代码: const http = require(‘http’); const server = http.createSe…

    node js 2023年6月8日
    00
  • 安装nvm并使用nvm安装nodejs及配置环境变量的全过程

    安装nvm并使用nvm安装nodejs及配置环境变量的全过程可以分为以下几个步骤: 1. 安装nvm nvm是Node Version Manager的缩写,可以帮助我们安装和管理不同版本的Node.js。 在命令行中输入以下命令进行安装(以下示例以macOS为例): curl -o- https://raw.githubusercontent.com/nv…

    node js 2023年6月8日
    00
  • nodejs的require模块(文件模块/核心模块)及路径介绍

    当我们在 Node.js 中编写代码时,我们通常需要使用一些外部的模块或者 Node.js 自带的一些模块。在 Node.js 中,我们可以通过使用 require 方法来引入所需要的模块,这个方法接受一个参数,表示要引入的模块的名称或路径。 文件模块 文件模块是我们写的一些自定义的模块,这些模块的代码通常包含在一个 JavaScript 文件中。我们可以通…

    node js 2023年6月8日
    00
  • 手机Web APP如何实现分享多平台功能

    分享是手机Web APP中常见的功能之一,让用户可以将自己喜欢的内容快速分享到自己的社交媒体账号上,从而实现增加用户粘性、提升用户体验的效果。实现多平台分享,可以让用户同时分享到不同的社交媒体平台,扩大传播范围,提高品牌曝光率。下面是实现手机Web APP多平台分享功能的完整攻略。 1. 获取分享渠道的授权 在实现多平台分享之前,需要先获取对应社交媒体平台的…

    node js 2023年6月8日
    00
  • node.js中的fs.fstatSync方法使用说明

    Node.js中的fs.fstatSync方法使用说明 一、方法介绍 fs.fstatSync(fd[, options]) 方法返回传入文件描述符的文件信息。该方法是同步(阻塞)的。 参数说明 fd:文件描述符,类型为整数。 options:可选参数,类型为对象,包含以下属性。 bigint:默认值为 false,表示返回的 stats 对象中的数值类型为…

    node js 2023年6月8日
    00
  • nodejs读写json文件的简单方法(必看)

    下面为您详细讲解“nodejs读写json文件的简单方法(必看)”。 标题 文章标题应简明扼要地概括全文,下文主要介绍如何在Node.js中简单地读写json文件。 简介 Node.js是一种基于Chrome V8引擎的JavaScript运行环境,它可以使JavaScript的运行环境脱离浏览器。当需要在Node.js中进行json文件的读写时,可以使用N…

    node js 2023年6月8日
    00
合作推广
合作推广
分享本页
返回顶部