根据源码解析Vue2中对于对象的变化侦测

news/2024/9/29 3:28:55/

UI = render(state)

VUE的特点是数据驱动视图,在这里可以把数据理解为状态,而视图就是用户可以看到的页面,页面是动态变化的,而他的变化或是用户操作引起,或是后端数据变化引起,这些都可以说是数据的状态变了,导致页面随之变化。

把顶部公式拆为三部分:UI、render、state。state和UI都是用户定的,不变的是render,所以VUE就担任了这个角色,当VUE发现state变化后,就通过一系列操作,把他反应在了UI上。

那么VUE是如何发现state变化的?引出了VUE的变化侦测:

JS为我们提供了object.defineProperty()方法,通过此方法我们可以知道数据什么时候发生了变化。

1.Object数据变得可观测

数据每次读写能够被我们看到,我们能知道数据什么时候发生了变化,将其称为数据的可观测。通过使用上述对象方法,可以实现。如下

javascript">let person = {}
let nameval = 'lwh'
Object.defineProperty(person,name,{get() {console.log('name被读取了')return nameval}set(newval) {console.log('name被修改了')nameval = newval
}
})

通过Object.defineProperty()方法定义了person的Name属性,并把这个属性的读写分别使用get和set方法进行拦截,每当该属性进行读或写操作时候就会触发这两个方法,意味着person的数据对象已经是可观测的了

但是为了将此对象的所有属性都变得可观测,可以编写以下代码: 

javascript">export class Observe {constructor(value) {this.value = value//给value新增一个__ob__属性,值为该value的observe实例//相当于给value打上标记,表示他已经转化为响应式了def(value,'__ob__',this)if (Array.isArray(value)) {//为数组的逻辑} else {this.walk(value)}}walk(obj: Object) {const keys = Object.keys(obj)for (let i = 0; i < keys.length; i++) {defineReactive(obj,key[i])}}
}
​
function defineReactive(obj, key, val) {if(typeof val === 'object') {new Observable(val)}object.defineProperty(obj, key, {enumerable: true,configurable: true,get() {console.log(`${key}属性被读取`)return val;},set(newval) {if(val === newval) {return}console.log(`${key}属性被修改了`)val = newval}})
}

上面的代码中,我们定义了observe类,用来将一个普通的Object转化为可观测的object。并且给value新增一个ob属性,表示这个已经被转化成响应式了。

然后判断数据类型,只有Object 类型才会调用walk方法将每一个属性都转换成getter/setter形式来侦测变化,最后在defineReactive中当传入的属性值还是一个object时使用new observe(val)来递归子属性,这样我们就可以把obj中所有属性都变为可观测型的,只要将object传入observe中,那么这个object就会变成可观测,响应式的Object.

observer类位于Vue2源码的src/core/observer/index.ts中。

2.依赖收集

当Object变得可观测后,我们就能知道数据什么时候发生了变化,发生了什么变化,这样就可以通知视图更新。但是问题来了,我们怎么知道哪些视图需要更新?总不能一个数据变化全部视图全部更新一次吧。所以,应该是视图里哪个使用到了这个数据的就更新。

视图里谁用到了这个数据谁更新,说官方一点就是谁依赖了这个数据谁更新。我们建立一个依赖数组(因为一个数据可能被多个地方使用,所以需要建立数组),谁依赖了这个数据就把谁放进这个数组。当数据变化时,就根据它的依赖数组去通知哪些视图需要更新。这个过程就是依赖收集。

那么我们该如何进行依赖收集,如何知道哪些依赖这个数据?

谁用了这个数据,那么当这个数据变化时就通知谁,所谓谁用了这个数据,就是谁获取了这个数据。根据上文可观测数据在被获取时会调用get方法,那么我们就可以在get中收集这个依赖。同样,数据变化会触发Set,所以,我们就在set中通知更新。

所以,我们应该给每个数据都配置一个依赖数组,谁依赖了它就放入谁。不过,单单使用数组的话,功能可能有点欠缺且代码有点耦合。所以拓展一下的话,我们给每一个数据建立一个类,用作依赖管理。所以在vue2源码中有了这个依赖管理器Dep类,代码如下:

javascript">// 源码位置:src/core/observer/dep.ts    
export default class Dep {constructor () {this.subs = []}addSub (sub) {this.subs.push(sub)}// 删除一个依赖removeSub (sub) {remove(this.subs, sub)}// 添加一个依赖depend () {if (window.target) {this.addDep(window.target)}}// 通知所有依赖更新notify () {const subs = this.subs.slice()for (let i = 0, l = subs.length; i < l; i++) {subs[i].update()}}
}
​
/*** Remove an item from an array*/
export function remove (arr, item) {if (arr.length) {const index = arr.indexOf(item)if (index > -1) {return arr.splice(index, 1)}}
}

在上面依赖管理器中,初始化了一个sub数组用来存放依赖,并且定义了几个附加方法。

有了依赖管理器后,我们就可以在getter中收集依赖,setter中通知更新了

javascript">function defineReactive (obj,key,val) {if(typeof val === 'object'){new Observable(val)}const dep = new Dep()  //实例化一个依赖管理器,生成一个依赖管理数组depObject.defineProperty(obj, key, {enumerable: true,configurable: true,get(){dep.depend()    // 在getter中收集依赖return val;},set(newVal){if(val === newVal){return}val = newVal;dep.notify()   // 在setter中通知依赖更新}})
}

接下来需要知道的是,我们收集的依赖到底是谁?

谁用到了这个数据谁就是依赖,那代码上该如何描述这个谁呢?

在Vue中还实现了一个叫做Watcher的类,这个类的实例就是我们上面说的那个谁。换句话说,谁用到了数据,我们就为谁创建一个Watcher实例。在之后数据变化时,我们不直接去通知依赖更新,而是去通知依赖对应的watch实例,由watcher实例去通知真正的视图

类的具体实现如下:

javascript">export default class Watcher {constructor (vm,expOrFn,cb) {this.vm = vm;this.cb = cb;this.getter = parsePath(expOrFn)this.value = this.get()}get () {window.target = this;const vm = this.vmlet value = this.getter.call(vm, vm)window.target = undefined;return value}update () {const oldValue = this.valuethis.value = this.get()this.cb.call(this.vm, value, oldValue)}
}
​
/*** Parse simple path.* 把一个形如'data.a.b.c'的字符串路径所表示的值,从真实的data对象中取出来* 例如:* data = {a:{b:{c:2}}}* parsePath('a.b.c')(data)  // 2*/
const bailRE = /[^\w.$]/
export function parsePath (path) {if (bailRE.test(path)) {return}const segments = path.split('.')return function (obj) {for (let i = 0; i < segments.length; i++) {if (!obj) returnobj = obj[segments[i]]}return obj}
}

分析代码:

1.实例化Watcher类时,会先执行其构造函数;

2.在构造函数中调用了this.get()实例方法;

3.在get()方法中,首先通过window.target = this 把实例自身赋给了全局的一个唯一对象window.target上,然后通过let value = this.getter.call(vm,vm)获取一下被依赖的数据,这里的目的是触发该数据的getter,在上文说过,调用getter会随之调用dep.depend()方法收集依赖,而在dep.depend()上取到挂载到window.target的值并将其存到依赖数组中,在get()方法最后需要将window.target释放掉

4.当数据变化时,会触发setter,在其中执行了dep.notify()方法,在这个方法中,遍历所有的依赖(即watcher实例),执行依赖的update()方法,在方法中调用数据变化的更新回调函数,从而更新视图。

3.不足之处

通过defineProperty方法实现了对Object数据的观测,但是这个方法仅仅只能观测到其中数据及设置值,如果向Object中添加或删除值时,他是无法观察到的,导致添加或者删除值时,无法通知依赖,无法驱动视图更新。vue也注意到了这一点,所以为了解决这个问题,vue添加了两个全局api:$set $delete,在后面学习全局API时会说到。

4.总结

通过Object.defineProperty方法实现了对object数据的可观测,并且封装了Observer类,让我们能够方便的把object数据中的所有属性(包括子属性)都转换成getter/seter的形式来侦测变化。

接着,知道了在getter中收集依赖,在setter中通知依赖更新,以及封装了依赖管理器Dep,用于存储收集到的依赖。

最后,我们为每一个依赖都创建了一个Wtcher实例,当数据发生变化时,通知Watcher实例,由Watcher实例去做真实的更新操作。

其整个流程大致如下:

  1. Data通过observer转换成了getter/setter的形式来追踪变化。

  2. 当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。

  3. 当数据发生了变化时,会触发setter,从而向Dep中的依赖(即Watcher)发送通知。

  4. Watcher接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。


http://www.ppmy.cn/news/1530877.html

相关文章

DC00015基于java web校园网上购物系统

1、项目功能演示 DC00015【含配套文档】基于java web校园网上购物系统 2、项目功能描述 基于java web校园网上购物系统分为用户和系统管理员两个角色。 2.1 用户功能 1、系统登录、用户注册、忘记密码 2、商品浏览、商品详情 3、购物车、付款 4、我的订单 5、修改密码、个人信…

vue3中echarts柱状图横轴文字太多放不下怎么解决

问题&#xff1a;在做数据展示的时候&#xff0c;使用的是echarts&#xff0c;遇到了个问题&#xff0c;就是数据过多&#xff0c;但是设置的x轴的文字名称又太长&#xff0c;往往左边第一个或右边最后一个的名称展示不全&#xff0c;只有半个。 从网上找到了几种办法&#xff…

openmv与stm32通信

控制小车视觉循迹使用 OpenMV 往往是不够的。一般使用 OpenMV 对图像进行处理&#xff0c;将处理过后的数据使用串口发送给STM32&#xff0c;使用STM32控制小车行驶。本文主要讲解 OpenMV 模块与 STM32 间的串口通信以及两种循迹方案&#xff0c;分别是划分检测区域和线性回归。…

SpringBoot整合邮件功能(带附件)

前言 这篇文章只是记录一下在开发中使用邮箱功能 本人在项目中需要使用邮箱发送带附件的邮件&#xff0c;我们的附件可能是在线的阿里云OSS,也可能是系统生成的Excel&#xff0c;写了一个工具类&#xff0c;方便使用。 整合流程 QQ邮箱作为案例 1、添加依赖 <dependency&g…

服务器配置虚拟环境及离线安装python

本篇文章基于已经装好conda环境进行哈&#xff0c;不会安装conda可参考服务器离线安装anaconda-CSDN博客 1.打印现有虚拟环境列表 conda env list&#xff0c;可以看见我现在有base、ai、py38三个环境 2.删除指定虚拟环境 conda remove -n py38 --all&#xff0c;回车后输入ye…

Qwen2-VL论文阅读笔记

第1章介绍 论文亮点&#xff1a; 1、 the Naive Dynamic Resolution mechanism 2、Multimodal Rotary Position Embedding (M-RoPE) 2D Rotary Position Embedding 3、统一图片和视频的处理范式、增i强视觉感知能力 4、LVLMs的scaling laws&#xff1a;2B、8B、72B 5、 dynamic…

SpringBoot之登录校验关于JWT、Filter、interceptor、异常处理的使用

什么是登录校验&#xff1f; 所谓登录校验&#xff0c;指的是我们在服务器端接收到浏览器发送过来的请求之后&#xff0c;首先我们要对请求进行校验。先要校验一下用户登录了没有&#xff0c;如果用户已经登录了&#xff0c;就直接执行对应的业务操作就可以了&#xff1b;如果用…

甘蔗茎节检测系统源码分享

甘蔗茎节检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Vis…