Vue2 与 Vue3 的数据绑定原理及实现

目录
  • 介绍
  • Object.defineProperty
  • Proxy

介绍

数据绑定是一种把用户界面元素(控件)的属性绑定到特定对象上面并使其同步的机制,使开发人员免于编写同步视图模型和视图的逻辑。

观察者模式又称为发布-订阅模式,定义对象间的一种一对多的依赖关系,当它本身的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。比如用户界面可以作为一个观察者,业务数据是被观察者,用户界面观察业务数据的变化,发现数据变化后,就同步显示在界面上。这样可以确保界面和数据之间划清界限,假定应用程序的需求发生变化,需要修改界面的表现,只需要重新构建一个用户界面,业务数据不需要发生变化。有以下几个角色:

  • 抽象主题(Subject):提供一个接口,把所有观察者对象的引用保存到一个集合里,可以增加和删除观察者对象。
  • 具体主题(Concrete Subject):将有关状态信息存入观察者对象,在本身的内部状态改变时,给所有登记过的观察者发出通知。
  • 抽象观察者(Observer):为所有的具体观察者定义一个接口,在得到主题通知时更新自己。
  • 具体观察者(Concrete Observer):实现更新接口。

Vue2 和 Vue3 的数据绑定都是观察者模式的实现,前者使用 Object.defineProperty,后者使用的是 Proxy。

有以下 HTML:

<div id="app">
  <input type="radio" name="hello" id="hello" value="hello" v-model="title">
  <label for="hello">hello</label>
  <input type="radio" name="hello" id="hello2" value="hello2" v-model="title">
  <label for="hello2">hello2</label>
  <div v-bind="title"></div>
  <input v-model="content">
  <select v-model="content">
    <option>world</option>
    <option>world1</option>
    <option>world2</option>
  </select>
  <div v-bind="content"></div>
  <input type="checkbox" id="hobby1" value="hobby1" v-model="hobby">
  <label for="hobby1">hobby1</label>
  <input type="checkbox" id="hobby2" value="hobby2" v-model="hobby">
  <label for="hobby2">hobby2</label>
  <input type="checkbox" id="hobby3" value="hobby3" v-model="hobby">
  <label for="hobby3">hobby3</label>
  <br>
  {{ hobby }}
</div>
<script>
  const vm = new Vue({
    el: '#app',
    data: {
      title: 'hello',
      content: 'world2',
      hobby: ['hobby2'],
    }
  });
</script>

下面使用两种方法进行简单实现上面的双向绑定。

Object.defineProperty

语法:

Object.defineProperty(obj, prop, descriptor)
  • obj:要定义属性的对象。
  • prop:要定义或修改的属性的名称或 Symbol 。
  • descriptor:要定义或修改的属性描述符。
  • 返回值:被传递给函数的对象。

首先定义一个观察者构造函数,并实现得到主题通知时更新自己的逻辑。第一行将当前观察者绑定到函数属性上面,是为了避免全局作用域变量。

function Observer(vm, node, name, nodeType) {
  // 构造函数被调用时,将当前对象绑定到函数属性上面,接下来触发 getter 时使用
  Observer.target = this;
  this.update = () => {
    // 这里 vm[name] 读取操作会触发 getter
    if (node.type === 'radio') node.checked = node.value === vm[name];
    else if (node.type !== 'checkbox') node[nodeType] = vm[name];
  };
  this.update();
  Observer.target = null; // 设置为空,避免首次触发get后重复添加
}

然后定义 Vue 构造函数,遍历 options.data 对象,为每个属性都生成一个主题(包含当前属性的观察者数组),然后使用 Object.defineProperty 劫持属性的读取和写入操作,在首次读取时添加一个对应的观察者对象,为了避免后面读取操作重复添加,在观察者构造函数里面首次更新操作完成后设置了空。

function Vue(options) {
  const obj = options.data;
  Object.keys(obj).forEach(key => {
    const subjects = [];
    Object.defineProperty(this, key, {
      get() {
        if (Observer.target) subjects.push(Observer.target);
        return obj[key];
      },
      set(newVal) {
        if (newVal === obj[key]) return;
        obj[key] = newVal;
        // 给当前主题所有登记过的观察者发出通知
        subjects.forEach(observer => observer.update());
      }
    });
  });
}

接下来就是遍历根节点(这里只遍历一层),根据子节点的类型,传入不同的参数调用 Observer 构造函数,然后首次更新视图,并触发 getter 将观察者对象都对应放到 options.data 的每个属性主题中,然后按属性类型添加不同的事件监听。

const el = document.querySelector(options.el);
el.childNodes.forEach(node => {
  if (node.nodeType === 1) {
    if (node.hasAttribute('v-model')) {
      const name = node.getAttribute('v-model');
      if (node.type === 'checkbox') node.checked = this[name].includes(node.value);
      const eventType = (node.tagName === 'INPUT' && node.type === 'text') || node.tagName == 'TEXTAREA' ? 'input' : 'change';
      node.addEventListener(eventType, e => {
        // 这里 this[name] 写入操作会触发 setter
        if (node.type === 'checkbox') {
          if (node.checked) this[name] = this[name].concat(node.value).sort();
          else this[name] = this[name].filter(v => v !== node.value).sort();
        } else this[name] = node.value;
      });
      new Observer(this, node, name, 'value');
    } else if (node.hasAttribute('v-bind')) {
      new Observer(this, node, node.getAttribute('v-bind'), 'textContent');
    }
  } else if (node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.nodeValue)) {
    new Observer(this, node, RegExp.$1.trim(), 'nodeValue');
  }
});
<div id="app">
  <input type="radio" name="hello" id="hello" value="hello" v-model="title">
  <label for="hello">hello</label>
  <input type="radio" name="hello" id="hello2" value="hello2" v-model="title">
  <label for="hello2">hello2</label>
  <p v-bind="title"></p>
  <input v-model="content">
  <select v-model="content">
    <option>world</option>
    <option>world1</option>
    <option>world2</option>
  </select>
  <p v-bind="content"></p>
  <input type="checkbox" id="hobby1" value="hobby1" v-model="hobby">
  <label for="hobby1">hobby1</label>
  <input type="checkbox" id="hobby2" value="hobby2" v-model="hobby">
  <label for="hobby2">hobby2</label>
  <input type="checkbox" id="hobby3" value="hobby3" v-model="hobby">
  <label for="hobby3">hobby3</label>
  <br>
  {{ hobby }}
</div>

运行:

Proxy

语法:

new Proxy(target, handler)
  • target:被代理的对象
  • handler:被代理对象上的自定义行为,和 Reflect 对象的所有静态方法对应,所以可以在其中调用对应的 Reflect 方法,完成默认行为,然后再部署额外的功能。

第一步定义观察者构造函数,和 Object.defineProperty 方式相同。

第二步也是定义 Vue 构造函数,不同的是使用 Proxy 劫持属性的读取和写入操作,不需要为 options.data 对象每个属性都添加主题了。其他和 Object.defineProperty 方式相同。

function Vue(options) {
  const subjects = [];
  this.proxy = new Proxy(options.data, {
    get(obj, key, receiver) {
      if (Observer.target) subjects.push(Observer.target);
      const value = Reflect.get(...arguments);
      return value;
    },
    set(obj, key, value, receiver) {
      if (value === obj[key]) return;
      const result = Reflect.set(...arguments);
      subjects.forEach(observer => observer.update());
      return result;
    }
  });
}

第三步遍历根节点,触发 getter 将观察者对象都放到主题的数组中,然后添加事件监听时,要触发 Proxy 的写入操作,而不是原对象。

const el = document.querySelector(options.el);
el.childNodes.forEach(node => {
  if (node.nodeType === 1) {
    if (node.hasAttribute('v-model')) {
      const name = node.getAttribute('v-model');
      if (node.type === 'checkbox') node.checked = this.proxy[name].includes(node.value);
      const eventType = (node.tagName === 'INPUT' && node.type === 'text') || node.tagName == 'TEXTAREA' ? 'input' : 'change';
      node.addEventListener(eventType, e => {
        // 这里 this.proxy[name] 写入操作会触发 setter
        if (node.type === 'checkbox') {
          let value = this.proxy[name];
          if (node.checked) {
            this.proxy[name] = value.concat(node.value).sort();
          } else this.proxy[name] = value.filter(v => v !== node.value).sort();
        } else this.proxy[name] = node.value;
      });
      new Observer(this.proxy, node, name, 'value');
    } else if (node.hasAttribute('v-bind')) {
      new Observer(this.proxy, node, node.getAttribute('v-bind'), 'textContent');
    }
  } else if (node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.nodeValue)) {
    new Observer(this.proxy, node, RegExp.$1.trim(), 'nodeValue');
  }
});
<div id="app">
  <input type="radio" name="hello" id="hello" value="hello" v-model="title">
  <label for="hello">hello</label>
  <input type="radio" name="hello" id="hello2" value="hello2" v-model="title">
  <label for="hello2">hello2</label>
  <p v-bind="title"></p>
  <input v-model="content">
  <select v-model="content">
    <option>world</option>
    <option>world1</option>
    <option>world2</option>
  </select>
  <p v-bind="content"></p>
  <input type="checkbox" id="hobby1" value="hobby1" v-model="hobby">
  <label for="hobby1">hobby1</label>
  <input type="checkbox" id="hobby2" value="hobby2" v-model="hobby">
  <label for="hobby2">hobby2</label>
  <input type="checkbox" id="hobby3" value="hobby3" v-model="hobby">
  <label for="hobby3">hobby3</label>
  <br>
  {{ hobby }}
</div>

 运行:

到此这篇关于Vue2 与 Vue3 的数据绑定原理及实现的文章就介绍到这了,更多相关Vue数据绑定内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Vue2项目升级到Vue3的详细教程

    目录 应不应该从 Vue 2 升级到 Vue 3 Vue 3 不兼容的那些写法 Vue 3 生态现状介绍 使用自动化升级工具进行 Vue 的升级 总结 应不应该从 Vue 2 升级到 Vue 3 应不应该升级?这个问题不能一概而论. 首先,如果你要开启一个新项目,那直接使用 Vue 3 是最佳选择.后面课程里,我也会带你使用 Vue 3 的新特性和新语法开发一个项目. 以前我独立使用 Vue 2 开发应用的时候,不管我怎么去组织代码,我总是无法避免在 data.template.methods

  • 一篇搞懂Vue2、Vue3响应式源码的原理

    目录 前言 Vue2响应式操作 响应式函数的封装 Depend类的封装 监听对象的变化 Vue3响应式操作 Proxy.Reflect Vue3响应式 前言 我们在编写Vue2,Vue3代码的时候,经常会在data中定义某些数据,然后在template用到的时候,可能会在多处用到这些数据,通过对这些数据的操作,可以达到改变视图的作用,即所谓数据驱动视图. 我们可以通过Mustache 语法,让data可以在页面上显示,随着data的变化,视图中也会随之改变. 那么,这种响应式操作在Vue2.Vu

  • vue2与vue3中生命周期执行顺序的区别说明

    目录 vue2与vue3中生命周期执行顺序区别 生命周期比较 简单例子说明 三种情况下的生命周期执行顺序 1.单页面下生命周期顺序 2.父子.兄弟组件的生命周期顺序 3.不同页面跳转时各页面生命周期的执行顺序 vue2与vue3中生命周期执行顺序区别 生命周期比较 vue2中执行顺序 beforeCreate=>created=>beforeMount =>mounted=>beforeUpdate =>updated=>beforeDestroy=>destro

  • vue2如何实现vue3的teleport

    目录 vue2实现vue3的teleport vue3新特性teleport介绍 teleport是什么 teleport怎么使用 vue2实现vue3的teleport 不支持同一目标上使用多个teleport(代码通过v-if就能实现) 组件 <script>     export default {         name: 'teleport',         props: {             /* 移动至哪个标签内,最好使用id */             to: {

  • Vue mixins混入使用解析

    目录 前言 一.什么是Mixins 二.什么时候使用Mixins 三.如何创建Mixins 四.如何使用Mixins 五.Mixins的特点 六.Mixins合并冲突 七.与vuex的区别 八.与公共组件的区别 前言 当我们的项目越来越大,我们会发现组件之间可能存在很多相似的功能,你在一遍又一遍的复制粘贴相同的代码段(data,method,watch.mounted等),如果我们在每个组件中去重复定义这些属性和方法会使得项目出现代码冗余并提高了维护难度,针对这种情况官方提供了Mixins特性

  • Vue2 与 Vue3 的数据绑定原理及实现

    目录 介绍 Object.defineProperty Proxy 介绍 数据绑定是一种把用户界面元素(控件)的属性绑定到特定对象上面并使其同步的机制,使开发人员免于编写同步视图模型和视图的逻辑. 观察者模式又称为发布-订阅模式,定义对象间的一种一对多的依赖关系,当它本身的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新.比如用户界面可以作为一个观察者,业务数据是被观察者,用户界面观察业务数据的变化,发现数据变化后,就同步显示在界面上.这样可以确保界面和数据之间划清界限,假定应用程序的需

  • vue2与vue3双向数据绑定的区别说明

    目录 vue2与vue3双向数据绑定区别 Vue2双向数据绑定存在的问题 原理 vue3.0双向数据绑定Proxy vue语法:数据的双向绑定 1.指令v-model 2.v-model与修饰符 3.v-model与自定义组件 vue2与vue3双向数据绑定区别 新的响应式机制采用了ES6的ProxyApi,抛弃了Object.defineProperty() Vue2双向数据绑定存在的问题 关于对象: Vue 无法检测 property 的添加或移除.由于 Vue 会在初始化实例时对prope

  • 手动实现vue2.0的双向数据绑定原理详解

    一句话概括:数据劫持(Object.defineProperty)+发布订阅模式 双向数据绑定有三大核心模块(dep .observer.watcher),它们之间是怎么连接的,下面来一一介绍. 为了大家更好的理解双向数据绑定原理以及它们之间是如何实现关联的,先带领大家复习一下发布订阅模式. 一.首先了解什么是发布订阅模式 直接上代码: 一个简单的发布订阅模式,帮助大家更好的理解双向数据绑定原理 //发布订阅模式 function Dep() { this.subs = []//收集依赖(也就是

  • 使用Vue3进行数据绑定及显示列表数据

    目录 一.与 Vue2 对比 1. Vue3 新特性 2. Vue2.Vue3 响应原理对比 3.重写数组的方法,检测数组变更 4.直观感受 二.使用Vue3进行数据绑定示例 1.使用ref实现数据绑定 2.使用reactive实现数据绑定 三.写在最后 一.与 Vue2 对比 1. Vue3 新特性 数据响应重新实现( ES6 的 proxy 代替 Es5 的 Object.defineProperty ) 源码使用 ts 重写,更好的类型推导 虚拟 DOM 新算法(更快,更小) 提供了 co

  • vue3实现v-model原理详解

    vue3 源码正式放出来了,想必大家也都开始争先恐后的学习 vue3 的知识了.由于 vue3 已经不再支持 v-model 了,而使用 .sync 来代替,但是为了这篇文章可以帮助大家快速了解 vue 的双向绑定实现原理,部分使用了 vue2.x v-model 的实现原理 proxy 的基础知识,相信大家已经都很了解了,让我们一起来回顾一下吧 proxy 是对一个对象的代理,并返回一个已代理的对象,已代理的对象如果发生任何 set 跟 get 的方法都可以被捕获到,我们写一个简单的 :che

  • vue中的数据绑定原理的实现

    本文主要介绍了vue中的数据绑定原理的实现,分享给大家,也给自己留个笔记,具体如下: vue中的响应式数据绑定是通过数据劫持和观察者模式来实现的.当前学习源码为vue2.0 源码关键目录 src |---core | |---instance | |---init.js | |---state.js | |---observer | |---dep.js | |---watcher.js 当我们实例化一个vue应用的时候,会伴随着各种的初始化工作,相关的初始化工作代码在init.js文件中 //

  • 稍微学一下Vue的数据响应式(Vue2及Vue3区别)

    什么是数据响应式 从一开始使用 Vue 时,对于之前的 jq 开发而言,一个很大的区别就是基本不用手动操作 dom,data 中声明的数据状态改变后会自动重新渲染相关的 dom. 换句话说就是 Vue 自己知道哪个数据状态发生了变化及哪里有用到这个数据需要随之修改. 因此实现数据响应式有两个重点问题: 如何知道数据发生了变化? 如何知道数据变化后哪里需要修改? 对于第一个问题,如何知道数据发生了变化,Vue3 之前使用了 ES5 的一个 API Object.defineProperty Vue

  • Vue2.x 的双向绑定原理及实现

    目录 1.实现过程 2.显示一个 Observer 3.实现 Watcher 4.实现 Compile 5.添加解析事件 6.完整版 myVue Vue 是利用的 Object.defineProperty() 方法进行的数据劫持,利用 set.get 来检测数据的读写. https://jsrun.net/RMIKp/embedded/all/light MVVM 框架主要包含两个方面,数据变化更新视图,视图变化更新数据. 视图变化更新数据,如果是像 input 这种标签,可以使用 oninp

  • 浅谈React双向数据绑定原理

    目录 什么是双向数据绑定 实现双向数据绑定 数据影响视图 视图影响数据 如果已经学过Vue,并且深入了解过Vue的双向数据绑定的话,就会明白 Vue 2.0 双向数据绑定的核心其实是通过Object.defineProperty来实现数据劫持和监听. 在 Vue 3.0 中则通过 Proxy来实现对整体对象的监听,对 Vue2.0 的优化. 什么是双向数据绑定 数据模型和视图之间的双向绑定. 当数据发生变化的时候,视图也就发生变化,当视图发生变化的时候,数据也会跟着同步变化:可以这样说用户在视图

  • 一文详解Vue3响应式原理

    目录 回顾 vue2.x 的响应式 vue3的响应式 Reflect 回顾 vue2.x 的响应式 实现原理: 对象类型:通过object.defineProperty()对属性的读取.修改进行拦截(数据劫持) 数组类型:通过重写更新数组的一系列方法来实现拦截(对数组的变更方法进行了包裹) Object.defineProperty(data,'count ",{ get(){}, set(){} }) 存在问题: 新增属性.删除属性,界面不会更新 直接通过下标修改数组,界面不会自动更新 但是

随机推荐