当前位置: 首页 > news >正文

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

UI = render(state)

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

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

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

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

1.Object数据变得可观测

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

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的数据对象已经是可观测的了

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

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类,代码如下:

// 源码位置: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中通知更新了

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实例去通知真正的视图

类的具体实现如下:

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接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • vue2 实现简易版的模糊查询功能
  • linux 的 InterlockedIncrement
  • Pandas重命名列的各种方法
  • 巨形象,这样看TCP和UDP的区别太简单了
  • Tomcat 乱码问题彻底解决
  • prober found high clock drift,Linux服务器时间不能自动同步,导致服务器时间漂移解决办法。
  • 【Python报错已解决】TypeError: can only concatenate str (not “float“) to str
  • Python排序算法揭秘:冒泡、插入、选择与快速排序的艺术
  • 基于Prometheus和Grafana的现代服务器监控体系构建
  • 艾默生电源维修ASTEC电源模块MP4-2Q-1E-4EE-0N
  • VmWare安装虚拟机保姆级教程(centos7,虚拟机网络设置,虚拟机桌面显示)
  • 高频 SQL 50 题(基础版)| 570. 至少有5名直接下属的经理
  • cocos creator 集成ffmpeg
  • VideoFileClip 切割视频
  • 《家庭无线网络覆盖项目》
  • 【附node操作实例】redis简明入门系列—字符串类型
  • 【个人向】《HTTP图解》阅后小结
  • axios请求、和返回数据拦截,统一请求报错提示_012
  • Git的一些常用操作
  • HTTP 简介
  • Java 多线程编程之:notify 和 wait 用法
  • Lsb图片隐写
  • Mysql优化
  • php ci框架整合银盛支付
  • Spring声明式事务管理之一:五大属性分析
  • 读懂package.json -- 依赖管理
  • 浮现式设计
  • 搞机器学习要哪些技能
  • 更好理解的面向对象的Javascript 1 —— 动态类型和多态
  • 记一次删除Git记录中的大文件的过程
  • 漫谈开发设计中的一些“原则”及“设计哲学”
  • 前端攻城师
  • 深入 Nginx 之配置篇
  • 十年未变!安全,谁之责?(下)
  • #{}和${}的区别是什么 -- java面试
  • #LLM入门|Prompt#3.3_存储_Memory
  • #我与Java虚拟机的故事#连载19:等我技术变强了,我会去看你的 ​
  • (02)Unity使用在线AI大模型(调用Python)
  • (24)(24.1) FPV和仿真的机载OSD(三)
  • (Qt) 默认QtWidget应用包含什么?
  • (Redis使用系列) Springboot 使用redis实现接口幂等性拦截 十一
  • (附源码)springboot掌上博客系统 毕业设计063131
  • (回溯) LeetCode 77. 组合
  • (机器学习-深度学习快速入门)第三章机器学习-第二节:机器学习模型之线性回归
  • (六)什么是Vite——热更新时vite、webpack做了什么
  • (一)认识微服务
  • (转)C语言家族扩展收藏 (转)C语言家族扩展
  • .NET 反射的使用
  • .net企业级架构实战之7——Spring.net整合Asp.net mvc
  • .net下简单快捷的数值高低位切换
  • .Net转Java自学之路—SpringMVC框架篇六(异常处理)
  • /deep/和 >>>以及 ::v-deep 三者的区别
  • [ JavaScript ] JSON方法
  • [AI]文心一言爆火的同时,ChatGPT带来了这么多的开源项目你了解吗
  • [AIGC] 使用Curl进行网络请求的常见用法