vue2源码解析——响应式原理

文章目录

  • 引言
  • 数据劫持
  • 收集依赖
  • 数组处理
  • 渲染watcher
  • vue3中的响应式

引言

vue的设计思想是数据双向绑定、数据与UI自动同步,即数据驱动视图。

为什么会这样呢?这就不得不提vue的响应式原理了,在使用vue的过程中,我被vue的响应式设计深深着迷 ,下面我们就从源码的角度,来分析一下vue是如何实现响应式原理的。

在vue2中,主要分为三个过程:

  1. 数据劫持:Vue 会遍历组件实例的所有属性,并使用 Object.defineProperty 将这些属性转换为 getter/setter 形式。这样做的目的是为了追踪依赖以及触发更新。
  2. 依赖收集:当渲染函数执行时,如果访问了响应式数据,那么这个访问会被记录下来,形成一个“依赖”。这意味着哪些视图或计算属性依赖于当前的数据。
  3. 视图更新:一旦某个响应式数据发生改变(即调用了 setter),Vue 就会通知所有依赖于该数据的视图进行重新渲染。

数据劫持

Vue使用Object.defineProperty来进行数据劫持。

Object.defineProperty 是 JavaScript 中的一个内置方法,它允许开发者在一个对象上定义新的属性或修改现有属性,并配置这些属性的特性。

Object.defineProperty(obj, prop, descriptor);
//obj: 要在其上定义属性的对象。
//prop: 要定义或修改的属性名称。
//descriptor: 将被定义或修改的属性描述符。

属性描述符(Descriptor)

descriptor 参数是一个对象,它可以包含以下几种键:

数据描述符

  • value: 属性对应的值,默认为 undefined
  • writable: 如果为 false,则该属性的值不能被改变,默认为 false
  • enumerable: 如果为 true,则该属性会出现在对象的属性枚举中(例如通过 for...in 循环或者 Object.keys()),默认为 false
  • configurable: 如果为 true,则可以删除该属性以及重新定义其描述符,默认为 false

存取描述符

  • get: 一个给属性提供 getter 方法的函数,如果没有 getter 则为 undefined。当访问该属性时会调用此函数,默认为 undefined
  • set: 一个给属性提供 setter 方法的函数,如果没有 setter 则为 undefined。当属性值被修改时会调用此函数,默认为 undefined

示例:

let person = {};
let age = 20;
Object.defineProperty(person, 'age', {get: function() {console.log('get age');return age;},set: function(value) {if (value < 0) {console.log('年龄不能是负数');} else {age = value;}}
});person.age = 25; // 正常设置年龄
console.log(person.age); // 输出 25person.age = -5; // 尝试设置负数年龄
// 输出 "年龄不能是负数."
console.log(person.age); // 仍然输出 25

当我们访问age的时候,可以看到输出age,设置新的值的时候,如果符合条件,就被修改,不符合条件,就被拦截到,使用自定义的gettersetter来重写了原有的行为,对obj.age进行取值和赋值,这就是数据劫持

但是上面的代码有个问题:属性的值都是局部的

所以我们需要一个全局的变量来保存这个属性的值.

// value使用了参数默认值
function defineReactive(data, key, value = data[key]) {Object.defineProperty(data, key, {get: function reactiveGetter() {return value},set: function reactiveSetter(newValue) {if (newValue === value) returnvalue = newValue}})
}defineReactive(obj, age, 29)

如果有多个属性呢,我们要用Observer类来遍历对象,对每个属性都进行defineProperty劫持。

class Observer {constructor(value) {this.value = valuethis.walk()}walk() {Object.keys(this.value).forEach((key) => defineReactive(this.value, key))}
}

如果obj是这种嵌套结构呢?{a:{b:{age:20}}

你可能想到了用递归,其实vue也是这么做的。

// 入口函数
function observe(data) {if (typeof data !== 'object') return// 调用Observernew Observer(data)
}class Observer {constructor(value) {this.value = valuethis.walk()}walk() {// 遍历该对象,并进行数据劫持Object.keys(this.value).forEach((key) => 		 defineReactive(this.value, key))}
}function defineReactive(data, key, value = data[key]) {observe(value)// 如果value是对象,递归调用observe来监测该对象// 如果value不是对象,observe函数会直接返回Object.defineProperty(data, key, {get: function reactiveGetter() {return value},set: function reactiveSetter(newValue) {if (newValue === value) returnvalue = newValueobserve(newValue) // 设置的新值也要被监听}})
}const obj = {a: 1,b: {age: 20}
}observe(obj)

observe、new Observer、defineReactive三者的关系:

执行 observe(obj)
├── 检查 obj 是否为对象
│   └── true: new Observer(obj),并执行 this.walk() 遍历 obj 的属性,执行 defineReactive()
│       ├── defineReactive(obj, 'a')
│       │   ├── 检查 'a' 的值是否为对象
│       │   │   └── 如果是对象: 递归调用 observe(value_of_a)
│       │   └── 使用 Object.defineProperty 对 'a' 进行 getter/setter 劫持
│       ├── defineReactive(obj, 'b')
│       │   ├── 检查 'b' 的值是否为对象
│       │   │   └── 如果是对象: 递归调用 observe(value_of_b)
│       │   └── 使用 Object.defineProperty 对 'b' 进行 getter/setter 劫持
│       └── ...(继续遍历 obj 的其他属性)
└── false: 直接返回

三个函数相互调用从而形成了递归。

这一部分只完成了对数据的劫持,有人可能想到,可以在setter中调用渲染函数,那不就可以更新页面了,也可以这样做,但是这样做有个弊端:只要有数据变化,页面就会重新更新。为了解决这个问题,数据变化时只更新与这个数据有关的DOM结构,怎么才能做到这样的效果,那就涉及到依赖。

收集依赖

依赖

什么是依赖呢?

假设你想借一本书。你需要向图书管理员询问这本书是否可用。如果这本书已经被借出去了,你会等待直到它被归还。一旦这本书被归还到图书馆,图书管理员会通知你这本书现在可以借阅了。

在这个例子中

  • 读者相当于Vue组件。它们需要根据数据的变化来决定何时重新渲染自己。
  • 图书管理员相当于Vue的Watcher机制。他们监视着数据的变化,并在数据发生变化时采取行动。
  • **书籍的状态(是否可借)**相当于Vue中的响应式数据。这些数据可以是变量、对象属性等,当它们发生变化时,依赖于这些数据的组件(读者)需要得到通知并作出相应的更新。

Watcher就是我们说的依赖,Watcher是一个抽象的类。

每个Watcher实例订阅一个或者多个数据,这些数据也被称为wacther的依赖;当依赖发生变化,Watcher实例会接收到数据发生变化这条消息,之后会执行一个回调函数来实现某些功能,比如更新页面。

[模板解析]↓
[生成渲染函数]↓
[执行渲染函数] → 访问 data.message↓
[触发 getter] → message 属性的 getter 被调用↓
[创建 Watcher] ← 当前正在执行的渲染函数↓
[Dep 收集 Watcher] ← 将当前 Watcher 添加到 message 的依赖列表中↓
[生成虚拟 DOM → 真实 DOM]

实现watcher类

class Watcher {constructor(data, expression, cb) {// data: 数据对象// expression:表达式,如b.c,根据data和expression就可以获取watcher依赖的数据// cb:依赖变化时触发的回调this.data = datathis.expression = expressionthis.cb = cb// 初始化watcher实例时订阅数据this.value = this.get()}get() {const value = parsePath(this.data, this.expression)return value}// 当收到数据变化的消息时执行该方法,从而调用cbupdate() {this.value = parsePath(this.data, this.expression) // 对存储的数据进行更新cb()}
}function parsePath(obj, expression) {const segments = expression.split('.')for (let key of segments) {if (!obj) returnobj = obj[key]}return obj
}

这里的update方法有点瑕疵,我们可以在定义的回调中访问this,并且该回调可以接收到监听数据的新值和旧值,因此做如下修改

update() {const oldValue = this.valuethis.value = parsePath(this.data, this.expression)this.cb.call(this.data, this.value, oldValue)
}

在源码中,有targetStack这样一个变量,也就是我们写的window.target

我们写的方式有一个弊端:当我们有两个嵌套的父子组件,渲染父组件时会新建一个父组件的watcher,渲染过程中发现还有子组件,就会开始渲染子组件,也会新建一个子组件的watcher。在我们的实现中,新建父组件watcher时,window.target会指向父组件watcher,之后新建子组件watcherwindow.target将被子组件watcher覆盖,子组件渲染完毕,回到父组件watcher时,window.target变成了null,这就会出现问题,因此,我们用一个栈结构来保存watcher

const targetStack = []function pushTarget(_target) {targetStack.push(window.target)window.target = _target
}function popTarget() {window.target = targetStack.pop()
}

Watcherget方法做如下修改

get() {pushTarget(this) // 修改const value = parsePath(this.data, this.expression)popTarget() // 修改return value
}

依赖收集

每个数据都应该维护一个属于自己的数组,该数组来存放依赖自己的watcher,我们可以在defineReactive中定义一个数组dep,这样通过闭包,每个属性就能拥有一个属于自己的dep.

function defineReactive(data, key, value = data[key]) {const dep = [] // 存放watcherobserve(value)Object.defineProperty(data, key, {get: function reactiveGetter() {return value},set: function reactiveSetter(newValue) {if (newValue === value) returnvalue = newValueobserve(newValue)dep.notify()}})
}

那么dep是如何收集watcher的呢?

new Watcher()时执行constructor,调用了实例的get方法,实例的get方法会读取数据的值,从而触发了数据的gettergetter执行完毕后,实例的get方法执行完毕,并返回值,constructor执行完毕,实例化完毕。

所以我们只需要对getter进行一些修改:

get: function reactiveGetter() {dep.push(watcher) // 新增return value
}

watcher这个变量从哪里来呢?我们是在模板编译函数中的实例化watcher的,getter中取不到这个实例。为了解决这个问题,需要把watcher放到全局中,比如说window对象中。

其实可以把dep抽象成一个类(有点像发布订阅模式)

Dep类

class Dep {constructor() {this.subs = []}depend() {this.addSub(Dep.target)}notify() {const subs = [...this.subs]subs.forEach((s) => s.update())}addSub(sub) {this.subs.push(sub)}
}

defineReactive函数只需做相应的修改

function defineReactive(data, key, value = data[key]) {const dep = new Dep() // 修改observe(value)Object.defineProperty(data, key, {get: function reactiveGetter() {dep.depend() // 修改return value},set: function reactiveSetter(newValue) {if (newValue === value) returnvalue = newValueobserve(newValue)dep.notify() // 修改}})
}

watcher中的代码里

get() {window.target = thisconst value = parsePath(this.data, this.expression)return value
}

大家可能注意到了,我们没有重置window.target。有些同学可能认为这没什么问题,但是考虑如下场景:有一个对象obj: { a: 1, b: 2 }我们先实例化了一个watcher1watcher1依赖obj.a,那么window.target就是watcher1。之后我们访问了obj.b,会发生什么呢?访问obj.b会触发obj.bgettergetter会调用dep.depend(),那么obj.bdep就会收集window.target, 也就是watcher1,这就导致watcher1依赖了obj.b,但事实并非如此。为解决这个问题,我们做如下修改:

// Watcher的get方法
get() {window.target = thisconst value = parsePath(this.data, this.expression)window.target = null // 新增,求值完毕后重置window.targetreturn value
}// Dep的depend方法
depend() {if (Dep.target) { // 新增this.addSub(Dep.target)}
}

为什么不能写成window.target = new Watcher()?

因为执行到getter的时候,实例化watcher还没有完成,所以window.target还是undefined

依赖收集过程:渲染页面时碰到插值表达式,v-bind等需要数据等地方,会实例化一个watcher,实例化watcher就会对依赖的数据求值,从而触发getter,数据的getter函数就会添加依赖自己的watcher,从而完成依赖收集。我们可以理解为watcher在收集依赖,而代码的实现方式是在数据中存储依赖自己的watcher

vue2的做法是每个组件对应一个watcher,实例化watcher时传入的也不再是一个expression,而是渲染函数,渲染函数由组件的模板转化而来,这样一个组件的watcher就能收集到自己的所有依赖,以组件为单位进行更新,是一种中等粒度的方式。要实现vue2的响应式系统涉及到很多其他的东西,比如组件化,虚拟DOM等。

派发更新

实现依赖收集后,我们最后要实现的功能是派发更新,也就是依赖变化时触发watcher的回调。

set: function reactiveSetter(newValue) {if (newValue === value) returnvalue = newValueobserve(newValue)dep.forEach(d => d.update()) // 新增 update方法见Watcher类
}

依赖收集图示:

[执行 this.message = "Hello World"]↓
[触发 setter]↓
[通知 Dep] → 所有订阅了 message 的 Watcher 都会被通知↓
[Watcher.update()] → 标记为脏,准备重新渲染↓
[异步更新队列] → Vue 使用 nextTick 批量更新视图↓
[重新执行渲染函数] → 生成新的虚拟 DOM 并对比差异↓
[更新真实 DOM]

总体过程

+------------------+       +------------------+
|   渲染函数/组件   |       |   Watcher 对象   |
|  使用 message    |<----->|  记录哪些组件在  |
+--------+---------+       |   使用该数据      ||                 +--------+---------+|                          ||                          |v                          v
+--------+---------------------------+---------+
|                  Dep(依赖收集器)            |
|  每个响应式属性都有一个 Dep,用来保存 Watcher |
+--------+--------------------------------------+  ||   +---------------------+|   |                     |v   v                     v[message: 'Hello Vue!']   [其他响应式属性]|| setter/getter↓数据变化 → 通知 Dep → Dep 通知 Watcher → 更新组件

总体代码

// 调用该方法来检测数据
function observe(data) {if (typeof data !== 'object') returnnew Observer(data)
}class Observer {constructor(value) {this.value = valuethis.walk()}walk() {Object.keys(this.value).forEach((key) => defineReactive(this.value, key))}
}// 数据拦截
function defineReactive(data, key, value = data[key]) {const dep = new Dep()observe(value)Object.defineProperty(data, key, {get: function reactiveGetter() {dep.depend()return value},set: function reactiveSetter(newValue) {if (newValue === value) returnvalue = newValueobserve(newValue)dep.notify()}})
}// 依赖
class Dep {constructor() {this.subs = []}depend() {if (Dep.target) {this.addSub(Dep.target)}}notify() {const subs = [...this.subs]subs.forEach((s) => s.update())}addSub(sub) {this.subs.push(sub)}
}Dep.target = nullconst TargetStack = []function pushTarget(_target) {TargetStack.push(Dep.target)Dep.target = _target
}function popTarget() {Dep.target = TargetStack.pop()
}// watcher
class Watcher {constructor(data, expression, cb) {this.data = datathis.expression = expressionthis.cb = cbthis.value = this.get()}get() {pushTarget(this)const value = parsePath(this.data, this.expression)popTarget()return value}update() {const oldValue = this.valuethis.value = parsePath(this.data, this.expression)this.cb.call(this.data, this.value, oldValue)}
}// 工具函数
function parsePath(obj, expression) {const segments = expression.split('.')for (let key of segments) {if (!obj) returnobj = obj[key]}return obj
}// for test
let obj = {a: 1,b: {m: {n: 4}}
}observe(obj)let w1 = new Watcher(obj, 'a', (val, oldVal) => {console.log(`obj.a 从 ${oldVal}(oldVal) 变成了 ${val}(newVal)`)
})

数组处理

要对数组处理的原因

在 Vue 2 的响应式系统中,数组方法需要被重写的原因主要与 Vue 的响应式机制以及 JavaScript 数组的特性有关。Vue 2 使用 Object.defineProperty 来实现数据的响应式转换,但是这种方法对数组的某些操作不起作用。

  1. 无法检测数组变化:使用 Object.defineProperty 可以很好地追踪对象属性的变化(通过 getter 和 setter),但是对于数组,直接修改数组元素(例如 arr[0] = newValue 或者 arr.length = newLength)不会触发 setter,因此 Vue 不能检测到这些变化并更新视图。
  2. 数组方法的直接调用问题:虽然 Vue 不能检测到上述的数组变化,但它可以拦截对数组原型方法的调用(如 push, pop, shift, unshift, splice, sort, reverse)。这是因为这些方法会改变原始数组的内容。如果不对这些方法进行重写,当用户调用它们时,Vue 将无法知道数组发生了变化,从而导致视图不更新

为了确保数组的变化能够被 Vue 检测到,并且相应地更新视图,Vue 2 对数组的以下几种方法进行了重写:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

这些方法被重写后,在执行时不仅会对数组本身做出相应的变更,还会触发视图更新。这通常通过在原生方法的基础上包裹一层来实现,即在调用原生方法之前或之后,手动通知依赖该数组的所有 watcher 进行更新。

先对Observer进行修改

class Observer {constructor(value) {this.value = valueif (Array.isArray(value)) {// 代理原型...this.observeArray(value)} else {this.walk(value)}}walk(obj) {Object.keys(obj).forEach((key) => defineReactive(obj, key, obj[key]))}// 需要继续监听数组内的元素(如果数组元素是对象的话)observeArray(arr) {arr.forEach((i) => observe(i))}
}

对原型进行代理

在数组实例和Array.prototype之间增加了一层代理来实现派发更新),数组调用代理原型的方法来派发更新,代理原型再调用真实原型的方法实现原有的功能:

// Observer.js
if (Array.isArray(value)) {Object.setPrototypeOf(value, proxyPrototype) // value.__proto__ === proxyPrototypethis.observeArray(value)
}// array.js
const arrayPrototype = Array.prototype // 缓存真实原型// 需要处理的方法
const reactiveMethods = ['push','pop','unshift','shift','splice','reverse','sort'
]// 增加代理原型 proxyPrototype.__proto__ === arrayProrotype
const proxyPrototype = Object.create(arrayPrototype)// 定义响应式方法
reactiveMethods.forEach((method) => {const originalMethod = arrayPrototype[method]// 在代理原型上定义变异响应式方法Object.defineProperty(proxyPrototype, method, {value: function reactiveMethod(...args) {const result = originalMethod.apply(this, args) // 执行默认原型的方法// ...派发更新...return result},enumerable: false,writable: true,configurable: true})
})

如何派发更新呢,对象是调用dep.nofity来派发更新,由于形成了闭包,每个属性都有自己的dep。但是如果我们在array.js中定义一个dep,所有数组都会共享,为了解决这个问题,vue在每个对象身上添加了一个自定义属性:__ob__,这个属性保存自己的Observer实例,然后再Observer上添加一个属性dep

observe做一个修改:

// observe.js
function observe(value) {if (typeof value !== 'object') returnlet ob// __ob__还可以用来标识当前对象是否被监听过if (value.__ob__ && value.__ob__ instanceof Observer) {ob = value.__ob__} else {ob = new Observer(value)}return ob
}

Observer做修改:

constructor(value) {this.value = valuethis.dep = new Dep()// 在每个对象身上定义一个__ob__属性,指向每个对象的Observer实例def(value, '__ob__', this)if (Array.isArray(value)) {Object.setPrototypeOf(value, proxyPrototype)this.observeArray(value)} else {this.walk(value)}
}// 工具函数def,就是对Object.defineProperty的封装
function def(obj, key, value, enumerable = false) {Object.defineProperty(obj, key, {value,enumerable,writable: true,configurable: true})
}
//obj: { arr: [...] }变成了obj: { arr: [..., __ob__: {} ], __ob__: {} }这种形式
// array.js
reactiveMethods.forEach((method) => {const originalMethod = arrayPrototype[method]Object.defineProperty(proxyPrototype, method, {value: function reactiveMethod(...args) {const result = originalMethod.apply(this, args)const ob = this.__ob__ // 新增ob.dep.notify()        // 新增return result},enumerable: false,writable: true,configurable: true})
})

push, unshift, splice可能会向数组中增加元素,这些增加的元素也应该被监听:

Object.defineProperty(proxyPrototype, method, {value: function reactiveMethod(...args) {const result = originalMethod.apply(this, args)const ob = this.__ob__// 对push,unshift,splice的特殊处理let inserted = nullswitch (method) {case 'push':case 'unshift':inserted = argsbreakcase 'splice':// splice方法的第三个及以后的参数是新增的元素inserted = args.slice(2)}// 如果有新增元素,继续对齐进行监听if (inserted) ob.observeArray(inserted)ob.dep.notify()return result},enumerable: false,writable: true,configurable: true
})

在对象身上新增一个__ob__属性,完成了数组的派发更新,接下来是依赖收集。

依赖收集

执行observe(obj)后,obj变成了下面的样子

obj: {arr: [{a: 1,__ob__: {...} // 增加},__ob__: {...}   // 增加],__ob__: {...}     // 增加
}

defineReactive函数中,为了递归地为数据设置响应式,调用了observe(val),而现在的observe()会返回ob,也就是value.__ob__,那接收一下这个返回值

// defineReactive.js
let childOb = observe(val) // 修改set: function reactiveSetter(newVal) {if (val === newVal) {return}val = newValchildOb = observe(newVal) // 修改dep.notify()
}

childOb是什么?

childOb就是obj.prop.__ob__,闭包中的depchildOb.dep保存的内容相同。

也就是说,每个属性(比如arr属性)的gettersetter不仅通过闭包保存了属于自己的dep,而且通过__ob__保存了自己的Observer实例,Observer实例上又有一个dep属性。

但是depchildOb.dep保存的watcher并不完全相同,看obj[arr][0].a,由于这是一个基本类型,对它调用observe会直接返回,因此所以没有__ob__属性,但是这个属性闭包中的dep能够收集到依赖自己的watcher

所以对get触发依赖时进行修改:

get: function reactiveGetter() {if (Dep.target) {dep.depend()if (childOb) {childOb.dep.depend() // 新增  }}return val
}

Vue认为,只要依赖了数组,就等价于依赖了数组中的所有元素,因此,我们需要进一步处理

// defineReactive.js
get: function reactiveGetter() {if (Dep.target) {dep.depend()if (childOb) {childOb.dep.depend()// 新增if (Array.isArray(val)) {dependArray(val)}}}return val
}function dependArray(array) {for (let e of array) {e && e.__ob__ && e.__ob__.dep.depend()if (Array.isArray(e)) {dependArray(e)}}
}

当依赖是数组时,遍历这个数组,为每个元素的__ob__.dep中添加watcher

渲染watcher

渲染watcher不需要回调函数,渲染watcher接收一个渲染函数而不是依赖的表达式,当依赖发生变化时,自动执行渲染函数

new Watcher(app, renderFn)

如何做到自动渲染呢,需要对原来的Watcher的构造函数做一些改造

constructor(data, expOrFn, cb) {this.data = data// 如果是函数的话if (typeof expOrFn === 'function') {this.getter = expOrFn} else {this.getter = parsePath(expOrFn)}this.cb = cbthis.value = this.get()
}// parsePath的改造,返回一个函数
function parsePath(path) {const segments = path.split('.')return function (obj) {for (let key of segments) {if (!obj) returnobj = obj[key]}return obj}
}

get修改

get() {pushTarget(this)const data = this.dataconst value = this.getter.call(data, data) // 修改popTarget()return value
}

依赖变化时重新执行渲染函数,需要在派发更新阶段做一个更新,修改update方法

update() {// 重新执行get方法const value = this.get()// 渲染watcher的value是undefined,因为渲染函数没有返回值// 因此value和this.value都是undefined,不会进入if// 如果依赖是对象,要触发更新if (value !== this.value || isObject(value)) {const oldValue = this.valuethis.value = valuethis.cb.call(this.vm, value, oldValue)}
}function isObject(target) {return typeof target === 'object' && target !== null
}

重复收集

对于相同的属性,可能会重复收集,为了避免这种情况发生,vue采用了以下方式

为每个dep添加一个id

let uid = 0constructor() {this.subs = []this.id = uid++ // 增加
}

watcher修改的地方比较多,首先为增加四个属性deps, depIds, newDeps, newDepIds

this.deps = []             // 存放上次求值时存储自己的dep
this.depIds = new Set()    // 存放上次求值时存储自己的dep的id
this.newDeps = []          // 存放本次求值时存储自己的dep
this.newDepIds = new Set() // 存放本次求值时存储自己的dep的id

当需要收集watcher时,由watcher来决定自己是否需要被dep收集

// dep.depend
depend() {if (Dep.target) {Dep.target.addDep(this) // 让watcher来决定自己是否被dep收集}
}// watcher.addDep
addDep(dep) {const id = dep.id// 如果本次求值过程中,自己没有被dep收集过则进入ifif (!this.newDepIds.has(id)) {// watcher中记录收集自己的dpthis.newDepIds.add(id)this.newDeps.push(dep)if (!this.depIds.has(id)) {dep.addSub(this)}}
}
  1. newDepsnewDepIds用来再一次取值过程中避免重复依赖,比如:{{ name }} -- {{ name }}
  2. depsdepIds用来再重新渲染的取值过程中避免重复依赖

再执行get方法最后会清空newDeps,newDepIds

cleanUpDeps() {// 交换depIds和newDepIdslet tmp = this.depIdsthis.depIds = this.newDepIdsthis.newDepIds = tmp// 清空newDepIdsthis.newDepIds.clear()// 交换deps和newDepstmp = this.depsthis.deps = this.newDepsthis.newDeps = tmp// 清空newDepsthis.newDeps.length = 0}

重新收集依赖

分为两种,一是删除无效依赖,二是收集新的依赖,收集新的依赖前面代码已经展示,但是能够收集到依赖的基本前提是Dep.target存在,从Watcher的代码中可以看出,只有在get方法执行过程中,Dep.target是存在的,因此,我们在update方法中使用了get方法来重新触发渲染函数,而不是getter.call()

//删除无效依赖
cleanUpDeps() {// 增加let i = this.deps.lengthwhile (i--) {const dep = this.deps[i]if (!this.newDepIds.has(dep.id)) {dep.removeSub(this)}}let tmp = this.depIds// ...
}
//在dep中删除
// Dep.js
removeSub(sub) {remove(this.subs, sub)
}function remove(arr, item) {if (!arr.length) returnconst index = arr.indexOf(item)if (index > -1) {return arr.splice(index, 1)}
}

vue3中的响应式

vue3中使用proxy来进行响应式处理。Object.defineProperty 和 Proxy 有什么区别呢?
Object.defineProperty的缺陷
Object.defineProperty 主要用于修改对象属性,它通过属性描述符来实现属性级别的操作,如数据劫持和属性变化监听。但由于属性描述符的选项有限,其功能也相对有限。该API兼容性良好,因此在早期被广泛使用。

Vue2 选择 Object.defineProperty 作为响应式系统的基础,主要看中其良好的兼容性。通过递归定义 getter/setter 和重写数组方法来实现响应式。但受限于API设计,存在以下缺陷:

  1. 递归调用导致性能损耗
  2. 无法检测:
    • 属性的新增/删除
    • 数组索引的直接修改
    • 数组长度的直接修改
      Vue2 通过 Vue.set 和 Vue.delete 方法解决了前两个问题,但数组长度修改的问题始终无法解决。
      使用Proxy之后
      相比之下,Proxy 专门用于对象代理,提供对象级别的拦截。它可以拦截几乎所有对象操作,包括:
  • 属性访问/修改/删除
  • 枚举操作
  • in 运算符
  • 函数调用
  • 原型操作
  • new 操作符调用

Proxy 的工作机制是对原对象创建代理,只有通过代理对象的操作才会被拦截。所有对代理对象的修改最终都会作用于原对象,Proxy 仅作为操作拦截和处理的中介层。

可以说 Proxy 完美弥补了 Object.defineProperty 的缺点,Vue3 使用 Proxy 后不再需要递归操作、不再需要重写数组的那七个方法、不再需要 Vue.set 和 Vue.delete。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

gcc相关内容

gcc 介绍&#xff1a;linux就是由gcc编译出来的&#xff0c;而且好像之前Linux只支持gcc编译。gcc全称为gnu compiler collection&#xff0c;它是gnu项目的一个组成部分。gnu致力于创建一个完全自由的操作系统&#xff0c;我感觉意思就是完全开源的操作系统。gnu有很多组件和…

android 图片背景毛玻璃效果实现

图片背景毛玻璃效果实现 1 依赖 // Glide implementation("com.github.bumptech.glide:glide:4.16.0") kapt("com.github.bumptech.glide:compiler:4.16.0") implementation("jp.wasabeef:glide-transformations:4.3.0") 2 布局<com.googl…

【Java开发日记】你会不会5种牛犇的yml文件读取方式?

前言 除了烂大街的Value和ConfigurationProperties外&#xff0c;还能够通过哪些方式&#xff0c;来读取yml配置文件的内容&#xff1f; 1、Environment 在Spring中有一个类Environment&#xff0c;它可以被认为是当前应用程序正在运行的环境&#xff0c;它继承了PropertyReso…

Spring Boot事务失效场景及解决方案

事务失效场景1&#xff1a;方法非public修饰 原因 Spring事务基于动态代理&#xff08;AOP&#xff09;实现&#xff0c;非public方法无法被代理拦截&#xff0c;导致事务失效。 代码示例 Service public class OrderService {Transactionalprivate void createOrder() { //…

电子电路:怎么理解时钟脉冲上升沿这句话?

时钟脉冲是数字电路中用于同步各组件操作的周期性信号&#xff0c;通常表现为高低电平交替的方波。理解其关键点如下&#xff1a; 时钟脉冲的本质&#xff1a; 由晶振等元件生成&#xff0c;呈现0/1&#xff08;低/高电平&#xff09;的规律振荡每个周期包含上升沿→高电平→下…

docker部署redis mysql nacos seata rabbitmq minio onlyoffice nginx实战

docker部署redis mysql nacos seata rabbitmq minio onlyoffice nginx实战 一、环境介绍 操作系统&#xff1a;ubuntu22.04 软件环境&#xff1a;docker、docker-compose 二、docker安装 版本规定到26.1.3版本过低会引起莫名其妙的问题。打开终端。更新软件包列表&#x…

全面解析:npm 命令、package.json 结构与 Vite 详解

全面解析&#xff1a;npm 命令、package.json 结构与 Vite 详解 一、npm run dev 和 npm run build 命令解析 1. npm run dev 作用&#xff1a;启动开发服务器&#xff0c;用于本地开发原理&#xff1a; 启动 Vite 开发服务器提供实时热更新&#xff08;HMR&#xff09;功能…

【Oracle】TCL语言

个人主页&#xff1a;Guiat 归属专栏&#xff1a;Oracle 文章目录 1. TCL概述1.1 什么是TCL&#xff1f;1.2 TCL的核心功能 2. 事务基础概念2.1 事务的ACID特性2.2 事务的生命周期 3. COMMIT语句详解3.1 COMMIT基础语法3.2 自动提交与手动提交3.3 提交性能优化 4. ROLLBACK语句…

OpenCV CUDA模块直方图计算------用于在 GPU 上执行对比度受限的自适应直方图均衡类cv::cuda::CLAHE

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 cv::cuda::CLAHE 是 OpenCV 的 CUDA 模块中提供的一个类&#xff0c;用于在 GPU 上执行对比度受限的自适应直方图均衡&#xff08;Contrast Limi…

OpenGAN:基于开放数据生成的开放集识别

简介 简介&#xff1a;这次学习的OpenGAN主要学习一个思路&#xff0c;跳出传统GAN对于判断真假的识别到判断是已知种类还是未知种类。重点内容不在于代码而是思路&#xff0c;会简要给出一个设计的代码。 论文题目&#xff1a;OpenGAN: Open-Set Recognition via Open Data …

随机游动算法解决kSAT问题

input&#xff1a;n个变量的k-CNF公式 ouput&#xff1a;该公式的一组满足赋值或宣布没有满足赋值 算法步骤&#xff1a; 随机均匀地初始化赋值 a ∈ { 0 , 1 } n a\in\{0,1\}^n a∈{0,1}n.重复t次&#xff08;后面会估计这个t&#xff09;&#xff1a; a. 如果在当前赋值下…

企业上线ESOP电子作业指导书系统实现车间无纸化的投入收益数据综合分析

企业上线ESOP电子作业指导书系统实现车间无纸化的投入收益数据综合分析 一、成本节约&#xff1a;无纸化直接降低运营成本 纸张与耗材费用锐减 o 杭州科创致远案例&#xff1a;某汽配企业引入无纸化系统后&#xff0c;年节省纸张耗材费用超50万元。通过电子化替代传统纸质文档…

高并发抽奖系统优化方案

引子 最近接触了一个抽奖的项目&#xff0c;由于用户量比较大&#xff0c;而且第三方提供的认证接口并发量有限&#xff0c;为了保证服务的高可用性&#xff0c;所以对高并限制发有一定的要求。经过一系列研究和讨论&#xff0c;做出了以下一些优化方案。 需求分析 根据用户量…

STM32八股【10】-----stm32启动流程

启动流程 1.上电复位 2.系统初始化 3.跳转到 main 函数 启动入口&#xff1a; cpu被清空&#xff0c;程序从0x00000000开始运行0x00000000存放的是reset_handler的入口地址0x00000000的实际位置会变&#xff0c;根据不同的启动模式决定启动模式分为&#xff1a; flash启动&a…

LLMTIME: 不用微调!如何用大模型玩转时间序列预测?

今天是端午节&#xff0c;端午安康&#xff01;值此传统佳节之际&#xff0c;我想和大家分享一篇关于基于大语言模型的时序预测算法——LLMTIME。随着人工智能技术的飞速发展&#xff0c;利用大型预训练语言模型&#xff08;LLM&#xff09;进行时间序列预测成为一个新兴且极具…

在VirtualBox中打造高效开发环境:CentOS虚拟机安装与优化指南

&#x1f525;「炎码工坊」技术弹药已装填&#xff01; 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 一、为何选择VirtualBox CentOS组合&#xff1f; 对于程序员而言&#xff0c;构建隔离的开发测试环境是刚需。VirtualBox凭借其跨平台支持&#xff08;W…

LeeCode 98. 验证二叉搜索树

给你一个二叉树的根节点 root &#xff0c;判断其是否是一个有效的二叉搜索树。 有效 二叉搜索树定义如下&#xff1a; 节点的左子树只包含 小于 当前节点的数。节点的右子树只包含 大于 当前节点的数。所有左子树和右子树自身必须也是二叉搜索树。 提示&#xff1a; 树中节…

Python简易音乐播放器开发教程

&#x1f4da; 前言 编程基础第一期《12-30》–音乐播放器是日常生活中常用的应用程序&#xff0c;使用Python和pygame库可以轻松实现一个简易的音乐播放器。本教程将详细讲解如何开发一个具有基本功能的音乐播放器&#xff0c;并解析其中涉及的Python编程知识点。 &#x1f6e…

ssh连接断开,保持任务后台执行——tmux

目录 **核心用途****基础使用方法**1. **安装 tmux**2. **启动新会话**3. **常用快捷键&#xff08;需先按 Ctrlb 前缀&#xff09;**4. **会话管理命令**5. **窗格操作进阶** **典型工作流****注意事项****配置文件&#xff08;~/.tmux.conf&#xff09;** tmux&#xff08; …

3D Gaussian splatting 04: 代码阅读-提取相机位姿和稀疏点云

目录 3D Gaussian splatting 01: 环境搭建3D Gaussian splatting 02: 快速评估3D Gaussian splatting 03: 用户数据训练和结果查看3D Gaussian splatting 04: 代码阅读-提取相机位姿和稀疏点云3D Gaussian splatting 05: 代码阅读-训练整体流程3D Gaussian splatting 06: 代码…