源码的角度分析Vue2数据双向绑定原理

什么是双向绑定

我们先从单向绑定切入,其实单向绑定非常简单,就是把Model绑定到View,当我们用JavaScript代码更新Model时,View就会自动更新。那么双向绑定就可以从此联想到,即在单向绑定的基础上,用户更新了View,Model数据也会自动被更新,这种情况就是双向绑定。实例如下:

 当用户填写表单,View的状态就被更新了,如果此时可以自动更新Model的状态,那就相当于我们把Model和View做了双向绑定关系图如下:

双向绑定的原理是什么

我们都知道Vue是数据双向绑定的框架,双向绑定由三个重要部分组成

  • Model:应用数据以及业务逻辑
  • View:应用视图,各类UI组件
  • ViewModel:框架封装的核心,它负责将数据与视图关联起来

上面这个分层的架构方案,即是我们经常耳熟能详的MVVM,他的控制层的核心功能便是“数据双向绑定”

理解ViewModel

它的主要职责就是:

  • 数据变化后更新视图
  • 视图变化后更细数据

当然,它还有两个主要部分组成

  • 监听器:对所有的数据进行监听
  • 解析器(Compiler):对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数

实现双向绑定

我们还是以Vue为例,先看看Vue中双向绑定流程是什么

1.new Vue()首先执行初始化,对data执行响应化处理,这个过程发生在Observe中(类似于Vue生命周期created之前执行的一系列初始化操作)

2.同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Complie中(类似于Vue生命周期mounted之前执行的一系列初始化操作)

3.同时定义一个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数

4.由于data的某个key在一个视图中可能会出现多次,所以每个key都需要一个管家Dep来管理多个Watcher

5.将来data中数据一旦发生变化,会首先找到ui应的Dep,同时所有Watcher执行更新函数

流程图如下:

劫持监听所有属性Observe

先来一个构造函数:执行初始化,对data执行响应化处理

  
 class Vue {  
  constructor(options) {  
    this.$options = options;  
    this.$data = options.data;  
        
    // 对data选项做响应式处理  
    observe(this.$data);  
        
    // 代理data到vm上  
    proxy(this);  
        
    // 执行编译  
    new Compile(options.el, this);  
  }  
}  

对data选项进行响应化具体操作


function proxy(vm) {
  Object.keys(vm.$data).forEach(key=>{
    Object.defineProperty(vm, key, {
      get() {
        return vm.$data[key]
      },
      set(newVal) {
        vm.$data[key] = newVal
      }
    })
  })
}

function observe(obj) {  
  if (typeof obj !== "object" || obj == null) {  
    return;  
  }  
  new Observer(obj);  
}  
  
class Observer {  
  constructor(value) {  
    this.value = value;  
    this.walk(value);  
  }  
  walk(obj) {  
    Object.keys(obj).forEach((key) => {  
      defineReactive(obj, key, obj[key]);  
    });  
  }  
}  

编译Complie

对每个元素节点的指令进行扫面和解析,根据指令模板替换数据,同时绑定相应的更新函数

class Compile {  
  constructor(el, vm) {  
    this.$vm = vm;  
    this.$el = document.querySelector(el);  // 获取dom  
    if (this.$el) {  
      this.compile(this.$el);  
    }  
  }  
  compile(el) {  
    const childNodes = el.childNodes;   
    Array.from(childNodes).forEach((node) => { // 遍历子元素  
      if (this.isElement(node)) {   // 判断是否为节点  
        console.log("编译元素" + node.nodeName);  
      } else if (this.isInterpolation(node)) { // 判断是否为插值文本 {{}} 
        console.log("编译插值⽂本" + node.textContent);  
      }  
      if (node.childNodes && node.childNodes.length > 0) {  // 判断是否有子元素  
        this.compile(node);  // 对子元素进行递归遍历  
      }  
    });  
  }  
  isElement(node) {  
    return node.nodeType == 1;  
  }  
  isInterpolation(node) {  
    return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);  
  }  
}  
  

依赖收集

        Vue2.x中的响应式原理主要死依赖于Object.defineProperty()方法实现属性的getter和setter。在Vue中,每个组件实例都有一个对应的Watcher实例,Watcher实例会负责依赖的收集以及触发更新。

        具体来说,当一个组件渲染时,会执行render函数来生成Virtual DOM,并且在执行过程中,当访问到组件的data中的属性时,会触发属性的getter方法。并在getter方法中,会进行依赖收集,将当前的Watcher对象存储到当前属性的依赖列表中。

当个属性收集具体如下图:

多个属性的收集如下:

依赖收集的过程可以简单描述如下:

1.在组件渲染过程中,当访问data中的属性时,会触发属性的getter方法;

2.在getter方法中,会将当前Watcher对象存储到当前依赖列表中(Dep);

3.当属性被修改时,会触发属性的setter方法;

4.在setter方法中,会通知所有依赖于该属性的Watcher对象,执行更新操作;

这样,当数据发生变化 时,Vue能够精确的知道哪些地方需要更新,并且只更新相关的部分,提高了性能(因为只有存储了触发getter方法时的watcher,做到了对应关系)

简化版的实现代码如下:

// 定义 Dep 类,用于管理依赖
class Dep {
  constructor() {
    this.subscribers = new Set(); // 存储 Watcher 实例的集合
  }

  // 添加依赖
  depend() {
    if (activeWatcher) {
      this.subscribers.add(activeWatcher);
    }
  }

  // 通知依赖更新
  notify() {
    this.subscribers.forEach(watcher => {
      watcher.update();
    });
  }
}

let activeWatcher = null;

// 定义 Watcher 类,用于观察数据变化
class Watcher {
  constructor(update) {
    this.update = update; // 更新函数
    this.value = null; // 存储当前值
    this.get(); // 初始化时进行依赖收集
  }

  // 获取当前值,并进行依赖收集
  get() {
    activeWatcher = this;
    // 在这里模拟读取 data 中的属性的过程
    this.value = this.update();
    activeWatcher = null;
  }
}

// 定义 reactive 函数,将对象转换为响应式对象
function reactive(obj) {
  // 遍历对象的每个属性,转换为响应式属性
  for (let key in obj) {
    let value = obj[key];
    const dep = new Dep(); // 每个属性对应一个依赖管理对象

    Object.defineProperty(obj, key, {
      get() {
        dep.depend(); // 依赖收集
        return value;
      },
      set(newValue) {
        value = newValue;
        dep.notify(); // 通知依赖更新
      }
    });
  }
  return obj;
}

// 示例用法
const data = reactive({
  count: 0
});

new Watcher(() => {
  console.log("Value updated:", data.count);
});

data.count++; // 触发更新

在这个示例中

  • Dep类用于管理依赖,每个响应式属性都会对应一个'Dep'实例,用于存储依赖于该属性的'Watcher'对象
  • ’Watcher‘类用于观察数据变化,当数据发生改变时会执行更新函数
  • ’reactive‘函数用于将对象转为响应式对象,在该函数中,通过'Object.defineProperty'来定义对象的属性,实现了属性的getter和setter,从而在读取和修改属性时进行依赖收集和通知更新

 在实际的 Vue 源码中,会有更复杂的逻辑和优化,但基本原理与上述代码类似。

个人备注说明:

1.上述代码设计中为什么activeWatcher变量是全局存储,同时在Watcher类的get方法中先是指向了this,然后又赋值为空?

答疑:在Vue源码中,activeWatcher 通常是通过栈结构来管理的,这里这样可以支持嵌套的依赖收集。而上述代码Watcher 类的 get 方法中,将activeWatcher 设置为当前的Watcher实例的原因是依赖收集过程中给需要知道当前的依赖是谁,从而在属性发生变化时可以通知到相关的 Watcher 实例进行更新。在依赖收集完成后,将activeWatcher 设置为空的原因时为了防止在非依赖收集的情况下,误操作导致activeWatcher 保留了值。

一般来说,在Vue的相应式系统中,activeWatcher 在以下几种情况下会被设置为某个具体的 Watcher 对象:

  • 组件渲染过程中:在组件的渲染过程中,Vue会创建一个Watcher对象来实现观察组件的渲染函数。此时activeWatcher 会被设置为这个渲染Watcher对象,以便在渲染函数中访问组件的响应式数据时进行依赖收集
  • 计算属性或者侦听的求职过程中:当计算属性或者侦听器的值被求值时,Vue会创建一个Watcher对象来观察相关的响应式数据,以便在求值过程中访问相关的响应式数据时进行依赖收集。
  • 用户手动创建的Watcher

以上情况下,activeWatcher 都会在相应的Watcher对象的get方法中被设置为当前Watcher实例。在依赖收集完成后,activeWatcher 会被重新设置为null,以便下一次依赖收集的时候再次被设置为新的Watcher对象。

2.Watcher 类中的value的作用是什么?

答疑:在 Vue 的响应式系统中,Watcher 类负责观察数据的变化,value 的存在可以让 Watcher 在依赖收集时记录当前的值,在数据发生变化时,可以通过对比新旧值来判断是否需要触发更新操作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/418011.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

压缩视频大小的软件有哪些?5款软件推荐

压缩视频大小的软件有哪些?随着高清摄像设备的普及和网络速度的不断提升,视频文件变得越来越庞大,动辄数百兆甚至数GB的大小常常让用户在分享和存储时感到头疼。幸运的是,市面上有许多优秀的视频压缩软件可以帮助我们轻松应对这一…

CDH6.3.1离线安装

一、从官方文档整体认识CDH 官方文档地址如下: CDH Overview | 6.3.x | Cloudera Documentation CDH是Apache Hadoop和相关项目中最完整、测试最全面、最受欢迎的发行版。CDH提供Hadoop的核心元素、可扩展存储和分布式计算,以及基于Web的用户界面和重…

使用docker方式测试部署django项目(客户催)

需求 1:已有django项目–weidanyewu 2:希望在服务器上测试部署–客户催 3:没完善django的启动 4:使用临时数据库进行演示 5:使用python3.10版本镜像 6:展示端口80 7:后台执行django程序 8&#…

js 面试 sort背后原理及排序使用方法

1 sort 概念 2 原理 之前版本是用的插入排序 和 快速排序。 现在用的是冒泡排序。 3 排序方式: 2-1 根据字符码点排序 2-2 从小到大或从大到小排序 2-3 对数组中的对象进行排序

android开发技术论文,Android中为什么需要Handler

前言 马爸爸总结了一句话:跳槽,要么是钱不到位,要么是受了委屈。 我给自己这次的跳槽经历做了一个分析,希望能对那些想换工作的朋友有所帮助。 许多朋友想换工作,但是对“换工作”的理解可能仅限于写简历、投简历、…

“智农”-数字乡村可视化

“智农”打造数字乡村可视化,结合乡村区域实际情况,以规划完善乡村机制体系和更好服务乡村振兴为目标,坚持可持续、可复制、可扩展的建设原则,着力解决农村信息孤岛,提高农村种植技术,加快农村信息化和产业…

ES系列之Logstash实战入门

概述 作为ELK技术栈一员,Logstash用于将数据采集到ES,通过简单配置就能把各种外部数据采集到索引中进行保存,可提高数据采集的效率。 原理 数据源提供的数据进入Logstash的管道后需要经过3个阶段: input:负责抽取数…

数据库常见理论常见面试题(总结)

一、前言 这里呢,博主会介绍一些平时关于数据库的理论的问题,比如数据库的三级模式两级映射、触发器、范式、存储过程、视图等等概念知识,但是像索引、事务、锁等内容,之前的文章就讲解过了,这里就不作过多的介绍了&am…

Google大模型Bard更名Gemini,现在实力如何?(VS gpt系列)

名人说:一花独放不是春,百花齐放花满园。——《增广贤文》 作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 目录 一、简要介绍1、Gemini是什么?2、主要特点3、Gemini的版本4、应用潜力5、…

docker容器配置mysql5.7主从复制

介绍 本文将通过docker创建3个mysql数据库容器,实现数据库主从复制功能,三个数据库容器分别为主库mysql-master:3307,从库mysql-slave-01:3308,mysql-slave-02:3309。使用的是mysql5.7版本 1. 拉取mongo镜像 docker pull mysql…

springboot230基于Spring Boot在线远程考试系统的设计与实现

在线远程考试系统设计与实现 摘 要 信息数据从传统到当代,是一直在变革当中,突如其来的互联网让传统的信息管理看到了革命性的曙光,因为传统信息管理从时效性,还是安全性,还是可操作性等各个方面来讲,遇到…

学不动系列-git-hooks和husky+lintstage

git-hooks 为了保证提交的代码符合规范,可以在上传代码时进行校验。常用husky来协助进行代码提交时的eslint校验。husky是基于git-hooks来实现,在使用husky之前,我们先来研究一下git-hooks。 构建git-hooks测试项目 需要使用git-hooks就需…

网络安全概述(一)

目录 资产保护 资产的类型 资产损失类型 资产保护考虑因素 安全强度、安全代价和侵入可能性的关系 信息安全技术发展 单机系统的信息保密阶段 信息保障阶段 信息保障技术框架IATF PDRR模型 网络攻击类型 阻断攻击、截取攻击、篡改攻击、伪造攻击 被动攻击和主动攻…

【反哺开源】我们计划把“这个”商业化功能贡献给Apache DolphinScheduler

今年,白鲸开源计划将Gitops功能反馈贡献给Apache DolphinScheduler社区,这个功能主要解决了开发、生产环境的同步问题。 在没有这个功能之前,我们只能通过导入导出的方式,以 JSON 文件作为媒介将开发环境的内容同步到生产环境&…

WebSocket介绍与应用

介绍 WebSocket 是基于TCP的一种的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。 HTTP协议和WebSocket协议对比: HTTP是短连接WebSocket是长连接…

K8S中POD的控制器

一、Pod控制器及其功用 Pod控制器,又称之为工作负载(workload),是用于实现管理pod的中间层,确保pod资源符合预期的状态,pod的资源出现故障时,会尝试进行重启,当根据重启策略无效&am…

GL绘制自定义线条4_使用OpenGL ES实现钢笔效果

在以前的文章里http://t.csdnimg.cn/TgCtl,我简述了如何使用OpenGL ES实现光滑的粗线条的绘制效果,在闲暇时间我把它再进一步进化,实现了端点长度按照压感大小实现伸缩的逻辑,从而实现了如下的笔锋效果: 书写过程中的效…

python统计分析——逻辑回归

参考资料:用python动手学统计学 逻辑回归即logistic回归。概率分布为二项分布、联系函数为logit函数的广义线性模型叫作logistic回归。解释变量可以有多个,连续型和分类型的解释变量也可以同时存在。 1、logit函数 logit函数的数学式如下,对…

Vue之监测数据的原理(对象)

大家有没有想过,为什么vue可以监测到数据发生改变?其实底层借助了Object.defineProperty,底层有一个Observer的构造函数 让我为大家简单的介绍一下吧! 我用对象为大家演示一下 const vm new Vue({el: "#app",data: {ob…

LNMP架构(搭建论坛+博客)

目录 一、LNMP架构概述 1、LNMP架构的概念 2、LNMP架构的优点 二、编译安装nginx软件 1、准备工作 1.1 关闭防火墙 1.2 安装依赖包 1.3 创建运行nginx用户 1.4 压缩包解压 2、编译与安装 3、添加nginx自启动文件 三、编译安装mysql软件 1、准备工作 1.1 安装mysq…