首页 > 其他分享 >深入理解 Vue 2 的双向绑定原理与实现

深入理解 Vue 2 的双向绑定原理与实现

时间:2024-08-22 11:26:25浏览次数:11  
标签:Vue Dep 绑定 视图 Watcher key 双向 属性

在 Vue 2 中,双向绑定是 Vue 的核心功能之一,它通过数据响应式系统使得数据的变化自动反映在视图上,同时用户在视图上做的更改也能够同步回数据模型。这种双向绑定是通过 数据劫持(Data Hijacking) 和 发布-订阅模式(Publish-Subscribe Pattern) 实现的。以下是双向绑定原理及实现方式的详细解析:

1. 数据劫持(Data Hijacking)

Vue 2 使用 Object.defineProperty() 来实现数据劫持。对于每一个数据属性,Vue 会将它转化为一个 gettersetter,从而拦截对数据的访问和修改操作。

实现原理:

  • 当访问一个数据属性时,getter 会被调用,Vue 可以通过这个 getter 来跟踪这个属性依赖的所有视图组件。

  • 当修改一个数据属性时,setter 会被调用,Vue 可以通知所有依赖这个属性的视图组件进行更新。

// 定义一个函数,用于将对象的某个属性转换为响应式属性
function defineReactive(obj, key, val) {
  
  // 使用 Object.defineProperty 对 obj 对象的 key 属性进行拦截
  Object.defineProperty(obj, key, {
    
    // 这个属性是否可以被枚举,例如在 for...in 循环中是否能遍历到
    enumerable: true,
    
    // 这个属性是否可以被删除或再次修改属性描述符
    configurable: true,
    
    // getter:当外部访问 obj.key 时,会调用此方法
    get() {
      console.log(`访问属性:${key}`);
      return val;  // 返回属性的当前值
    },
    
    // setter:当外部修改 obj.key 时,会调用此方法
    set(newVal) {
      if (newVal !== val) {  // 检查新值是否与旧值不同
        console.log(`属性${key}从${val}变为${newVal}`);
        val = newVal;  // 更新属性的值
        // 通知机制可以放在这里,告知相关视图进行更新
      }
    }
  });
}

解释:

  1. defineReactive 函数:用于将对象的某个属性(如 key)转化为响应式属性。它接收三个参数:对象 obj,属性名 key,以及属性的初始值 val

  2. Object.defineProperty():这是 JavaScript 中用于定义或修改对象属性的 API。在这里,Vue 用它来拦截对象属性的访问和赋值操作。

  3. enumerableconfigurable:这两个选项分别控制属性是否可枚举和是否可配置。将 enumerable 设置为 true,意味着该属性可以被循环枚举;configurable: true 允许属性描述符被修改或属性被删除。

  4. getter 方法:当外部代码访问 obj.key 时,getter 会被调用。getter 会返回属性的当前值 val,并在调试时打印访问日志。

  5. setter 方法:当外部代码为 obj.key 赋新值时,setter 会被调用。setter 首先检查新值是否与旧值不同,如果不同则更新属性的值,并可以在此处触发视图更新逻辑。

2. 发布-订阅模式(Publish-Subscribe Pattern)

在 Vue 2 中,视图和数据之间的关联是通过一个叫做 Watcher 的类来实现的。Watcher 作为发布者和订阅者之间的桥梁,每一个 Watcher 订阅了某个数据属性,当该属性发生变化时,Watcher 就会被通知并触发相应的视图更新。

实现步骤:

  1. Dep(依赖收集器):每个数据属性都有一个对应的 Dep 对象,用来收集所有依赖于该属性的 Watcher

  2. Watcher:当视图模板被解析时,Vue 会为每一个依赖数据的地方创建一个 Watcher,并将其添加到对应数据属性的 Dep 中。

  3. 通知更新:当数据属性发生变化时,对应的 Dep 会通知所有相关的 Watcher,进而触发视图更新。

// Dep 类用于收集依赖,每个属性都对应一个 Dep 实例
class Dep {
  constructor() {
    this.subs = [];  // 存储所有依赖于这个属性的 Watcher 实例
  }
  
  // 添加依赖的方法,传入一个 Watcher 实例
  addSub(sub) {
    this.subs.push(sub);
  }
  
  // 通知所有依赖,触发它们的更新
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}

// Watcher 类,用于当数据变化时执行特定的回调函数
class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm;  // 保存组件实例
    this.getter = expOrFn;  // 将表达式或函数保存起来
    this.cb = cb;  // 当数据变化时执行的回调函数
    this.value = this.get();  // 初始化时获取并保存属性值,并触发依赖收集
  }
  
  // 获取属性值并进行依赖收集
  get() {
    Dep.target = this;  // 将当前的 Watcher 实例指向 Dep.target
    let value = this.getter.call(this.vm, this.vm);  // 调用表达式或函数获取值
    Dep.target = null;  // 释放 Dep.target,避免重复收集
    return value;  // 返回获取到的属性值
  }
  
  // 当数据变化时,Watcher 会调用这个方法来更新视图
  update() {
    const oldValue = this.value;  // 保存旧值
    this.value = this.get();  // 获取新值并重新依赖收集
    this.cb.call(this.vm, this.value, oldValue);  // 执行回调函数,传入新旧值
  }
}

解释:

  1. Dep 类:每个属性对应一个 Dep 实例,用于收集所有依赖于该属性的 Watchersubs 数组用于存储这些 Watcher 实例。

  2. addSub 方法:将一个 Watcher 实例添加到 subs 数组中,这是依赖收集的过程。

  3. notify 方法:当属性值发生变化时,调用 notify 方法遍历 subs 数组,并触发每个 Watcherupdate 方法,从而通知视图更新。

  4. Watcher 类:用于创建一个观察者实例。当依赖的数据发生变化时,Watcher 会被通知并执行相应的更新操作。Watcher 保存了一个回调函数 cb,用于在数据变化时执行。

  5. get 方法:当创建 Watcher 实例时,会调用 get 方法获取数据的初始值,并触发依赖收集。Dep.target 用于临时存储当前的 Watcher 实例,以便在 getter 中添加依赖。

  6. update 方法:当数据变化时,update 方法会被调用,它会重新获取数据的当前值,并调用回调函数来更新视图。

3. 双向绑定的完整流程

结合以上内容,Vue 2 的双向绑定机制大致可以描述为以下步骤:

  1. 数据劫持:通过 Object.defineProperty() 拦截对象属性的访问和修改,将属性转化为响应式属性。

  2. 依赖收集:在初始化组件时,Vue 解析模板并为每个依赖于响应式属性的地方创建一个 Watcher。这些 Watcher 会被添加到相应属性的 Dep 中。

  3. 数据变更通知:当响应式属性的值发生变化时,setter 被触发,对应的 Dep 通知所有依赖此属性的 Watcher,调用它们的 update 方法更新视图。

总结

Vue 2 的双向绑定通过数据劫持和发布-订阅模式实现了数据和视图的自动同步,Object.defineProperty 提供了数据响应性,而 WatcherDep 通过依赖收集和通知机制保证了视图与数据的联动性。这种机制的实现虽然强大,但由于 Object.defineProperty 的限制,对对象属性的劫持只能在初始化时完成,这也是为什么 Vue 3 转向了使用 Proxy 来实现更灵活的响应式系统。

标签:Vue,Dep,绑定,视图,Watcher,key,双向,属性
From: https://blog.csdn.net/qq_67572731/article/details/141423684

相关文章

  • Vue 插槽
    如果向组件标签中输入内容,会被忽略掉1、props接受父组件传递过来的数据2、插槽,接受父组件向子组件传递的html文本在组件中通过slot标签可以接收父组件传递过来的html文本,就是将slot标签添加到template标签中如下:现在组件中添加标签<sub-component:pmsg='msg'>......
  • 计算机毕业设计django+vue网上水果商城系统【开题+论文+程序】
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容研究背景随着互联网技术的飞速发展,电子商务已成为人们日常生活中不可或缺的一部分,尤其在后疫情时代,线上购物更是成为了消费者获取商品与服务的主要......
  • 计算机毕业设计django+vue超市会员管理系统设计与实现【开题+论文+程序】
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容研究背景随着信息技术的飞速发展和电子商务的普及,超市管理逐渐向数字化、智能化转型。传统超市在会员管理、商品信息维护、订单处理及积分兑换等方......
  • 【Vue】el-autocomplete禁用时仍会触发下拉的问题
    文章目录问题解决问题el-autocomplete禁用时,点击后仍会触发下拉,导致出现bug<el-autocompletev-model="modelValue":fetch-suggestions="queryAsync"class="autocomplete":disabled="isDis"@select="doSelect($event,item)&......
  • 使用 Vue I18n 进行 Vue.js 应用的国际化
    随着互联网的全球化发展,开发多语言支持的应用变得越来越重要。Vue.js作为一个流行的前端框架,通过vue-i18n插件,能够非常方便地实现应用的国际化(i18n)。本文将介绍如何在Vue.js应用中使用vue-i18n进行国际化设置。什么是国际化(i18n)?国际化(Internationalization)通常简写......
  • vue3解决跨域问题
     vue3登录提示错误 解决方法1,修改根目录下vite.config.ts文件 修改host、proxy、target,修改后文件如下(红色为修改),具体内容根据后台实际修改 server:{ host:'localhost', port:env.VITE_PORTasunknownasnumber, open:JSON.parse(env.VITE_OPEN),......
  • 043、Vue3+TypeScript基础,pinia库使用action,在函数中对存储数据进行修改
    01、main.js代码如下://引入createApp用于创建Vue实例import{createApp}from'vue'//引入App.vue根组件importAppfrom'./App.vue'//第一步:引入piniaimport{createPinia}from'pinia'constapp=createApp(App);//第二步:创建pinia实例constpinia=......
  • 从源码分析 SpringBoot 的 LoggingSystem → 它是如何绑定日志组件的
    开心一刻今天心情不好,想约哥们喝点我:心情不好,给你女朋友说一声,来我家,过来喝点哥们:行!我给她说一声我:你想吃啥?我点外卖哥们:你俩定吧,我已经让她过去了我:???我踏马让你过来!和她说一声哥们:哈哈哈,我踏马寻思让她过去呢前情回顾SpringBoot2.7霸王硬上弓Logback1.3→不甜但解渴......
  • [vue3] vue3更新组件流程与diff算法
    在Vue3中,组件的更新通过patch函数进行处理。patch函数源码位置:core/packages/runtime-core/src/renderer.tsatmain·vuejs/core(github.com)constpatch:PatchFn=(n1,n2,container,anchor=null,parentComponent=null,parentSuspen......
  • 042、Vue3+TypeScript基础,pinia库存储数据修改的两种方式
    01、main.ts代码如下://引入createApp用于创建Vue实例import{createApp}from'vue'//引入App.vue根组件importAppfrom'./App.vue'//第一步:引入piniaimport{createPinia}from'pinia'constapp=createApp(App);//第二步:创建pinia实例constpinia=......