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

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。

相关文章:

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

文章目录 引言数据劫持收集依赖数组处理渲染watchervue3中的响应式 引言 vue的设计思想是数据双向绑定、数据与UI自动同步&#xff0c;即数据驱动视图。 为什么会这样呢&#xff1f;这就不得不提vue的响应式原理了&#xff0c;在使用vue的过程中&#xff0c;我被vue的响应式设…...

基于 GitLab CI + Inno Setup 实现 Windows 程序自动化打包发布方案

在 Windows 桌面应用开发中&#xff0c;实现自动化构建与打包发布是一项非常实用的工程实践。本文以我在开发PackTes项目时的为例&#xff0c;介绍如何通过 GitLab CI 配合 Inno Setup、批处理脚本、Qt 构建工具&#xff0c;实现版本化打包并发布到共享目录的完整流程。 项目地…...

做好 4个基本动作,拦住性能优化改坏原功能的bug

缺陷分析 “小李&#xff0c;202504300989这个现场缺陷你负责测试漏测分析&#xff0c;要求用5why方法找到漏测根因&#xff0c;根据找到的根因制定改进措施。你今天下班前完成&#xff0c;完成后立刻通知我&#xff0c;质量部现在每天都在催现场缺陷分析结果。”周二刚上班&a…...

【HarmonyOS 5】针对 Harmony-Cordova 性能优化,涵盖原生插件开发、线程管理和资源加载等关键场景

1. ‌原生图片处理插件&#xff08;Java&#xff09; package com.example.plugin; import ohos.media.image.ImageSource; import ohos.media.image.PixelMap; import ohos.app.Context; public class ImageProcessor { private final Context context; public ImagePro…...

零基础认知企业级数据分析平台如何落实数据建模(GAI)

理解数据建模的基本概念 数据建模是将业务需求转化为数据结构和关系的过程&#xff0c;核心目标是构建可支撑分析、预测或决策的数据模型。零基础需从以下维度入手&#xff1a; 业务理解&#xff1a;明确业务问题&#xff08;如销售预测、用户分群&#xff09;&#xff0c;与…...

web架构2------(nginx多站点配置,include配置文件,日志,basic认证,ssl认证)

一.前言 前面我们介绍了一下nginx的安装和基础配置&#xff0c;今天继续来深入讲解一下nginx的其他配置 二.nginx多站点配置 一个nginx上可以运行多个网站。有多种方式&#xff1a; http:// ip/域名 端口 URI 其中&#xff0c;ip/域名变了&#xff0c;那么网站入口就变了…...

AI 的早期萌芽?用 Swift 演绎约翰·康威的「生命游戏」

文章目录 摘要描述题解答案题解代码分析示例测试及结果时间复杂度空间复杂度总结 摘要 你有没有想过&#xff0c;能不能通过简单的规则模拟出生与死亡&#xff1f;「生命游戏」正是这样一种充满魅力的数学模拟系统。这篇文章我们来聊聊它的规则到底有多神奇&#xff0c;并用 S…...

【DBA】MySQL经典250题,改自OCP英文题库中文版(2025完整版)

【DBA】MySQL经典250题&#xff0c;改自OCP英文题库中文版&#xff08;2025完整版&#xff09; ——2025.5.15 文章目录 P1&#xff1a;1-50&#xff08;划重点&#xff09;P2&#xff1a;51-100&#xff08;划重点&#xff09;P3&#xff1a;101-150&#xff08;划重点打标记&…...

Cursor 编辑器介绍:专为程序员打造的 AI 编程 IDE

在现代软件开发中&#xff0c;AI 辅助编程正逐步改变开发者的工作方式。Cursor 正是这场变革中的佼佼者&#xff0c;它不仅是一个现代化的代码编辑器&#xff0c;更是将强大的 AI 编程助手深度集成到 IDE 的一次探索性尝试。 一、什么是 Cursor&#xff1f; Cursor 是一款基于…...

go|channel源码分析

文章目录 channelhchanmakechanchansendchanrecvcomplieclosechan channel 先看一下源码中的说明 At least one of c.sendq and c.recvq is empty, except for the case of an unbuffered channel with a single goroutine blocked on it for both sending and receiving usin…...

【大模型学习】项目练习:视频文本生成器

&#x1f680;实现视频脚本生成器 视频文本生成器 &#x1f4da;目录 一、游戏设计思路二、完整代码解析三、扩展方向建议四、想说的话 一、⛳设计思路 本视频脚本生成器采用模块化设计&#xff0c;主要包含三大核心模块&#xff1a; 显示模块&#xff1a;处理用户输入和…...

【Rust】Rust获取命令行参数以及IO操作

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…...

【Redis】Zset 有序集合

文章目录 常用命令zaddzcardzcountzrange && zrevrangezrangebyscorezpopmax && bzpopmaxzpopmin && zpopmaxzrank && zrevrankzscorezremzremrangebyrankzremrangebyscorezincrby 集合间操作交集 zinterstore并集 zunionstore 内部编码应用场…...

manus对比ChatGPT-Deep reaserch进行研究类论文数据分析!谁更胜一筹?

目录 没有账号&#xff0c;只能挑选一个案例 1、manus的效果 Step-1&#xff1a;直接看结果 Step-2&#xff1a;看看其他文件的细节 Step-3&#xff1a;看最终报告 2、Deep reaserch 3、Deep reaserch进行行业分析 总结一下&#xff1a; 大家好这里是学术Anan&#xff…...

【 HarmonyOS 5 入门系列 】鸿蒙HarmonyOS示例项目讲解

【 HarmonyOS 5 入门系列 】鸿蒙HarmonyOS示例项目讲解 一、前言&#xff1a;移动开发声明式 UI 框架的技术变革 在移动操作系统的发展历程中&#xff0c;UI 开发模式经历了从命令式到声明式的重大变革。 根据华为开发者联盟 2024 年数据报告显示&#xff0c;HarmonyOS 设备…...

AWS Transit Gateway实战:构建DMZ隔离架构,实现可控的网络互通

在企业云网络架构中,如何实现不同VPC之间的安全互通是一个常见挑战。本文将通过AWS Transit Gateway实战,展示如何构建一个DMZ隔离架构,使DMZ可以与Test和Production环境互通,而Test和Production环境之间相互隔离。 1. Transit Gateway架构设计概述 在开始实践前,让我们先…...

用提示词写程序(3),VSCODE+Claude3.5+deepseek开发edge扩展插件V2

edge扩展插件;筛选书签,跳转搜索,设置背景 链接: https://pan.baidu.com/s/1nfnwQXCkePRnRh5ltFyfag?pwd86se 提取码: 86se 导入解压的扩展文件夹: 导入扩展成功: edge扩展插件;筛选书签,跳转搜索,设置背景...

栈与队列:数据结构的有序律动

在数据结构的舞台上&#xff0c;栈与队列宛如两位优雅的舞者&#xff0c;以独特的节奏演绎着数据的进出规则。它们虽不像顺序表与链表那般复杂多变&#xff0c;却有着令人着迷的简洁与实用&#xff0c;在众多程序场景中发挥着不可或缺的作用。今天&#xff0c;就让我们一同去探…...

初识PS(Photoshop)

初识PS&#xff08;Photoshop&#xff09; 1、Photoshop界面 2、常用快捷键...

go语言的GMP(基础)

1.概念梳理 1.1线程 通常语义中的线程&#xff0c;指的是内核级线程&#xff0c;核心点如下&#xff1a; &#xff08;1&#xff09;是操作系统最小调度单元&#xff1b; &#xff08;2&#xff09;创建、销毁、调度交由内核完成&#xff0c;cpu 需完成用户态与内核态间的切…...

电路图识图基础知识-高、低压供配电系统电气系统的继电自动装置(十三)

电气系统的继电自动装置 在供电系统中为保证系统的可靠性&#xff0c;保证重要负荷的不间断供电&#xff0c;常采用自动重合闸装置和备用电源自动投入装置。 1 自动重合闸装置 供配电系统多年运行实践表明&#xff0c;架空线路发生的故障多属于暂时性故障&#xff0c;如雷击…...

JDK21深度解密 Day 9:响应式编程模型重构

【JDK21深度解密 Day 9】响应式编程模型重构 引言&#xff1a;从Reactor到虚拟线程的范式转变 在JDK21中&#xff0c;虚拟线程的引入彻底改变了传统的异步编程模型。作为"JDK21深度解密"系列的第91天&#xff0c;我们将聚焦于响应式编程模型重构这一关键主题。通过…...

在 Linux 服务器上无需 sudo 权限解压/打包 .7z 的方法(实用命令)

7z的压缩比很高&#xff0c;可以把100G的文件压到3-5G&#xff0c;在大文件传输上很有优势但是一般服务器上是只有tar解压&#xff0c;用户没法&#xff08;没有权限&#xff09;直接安装7z工具来解压因此使用conda安装p7zip库可以很好地解决这个问题~ 关于7z的相关背景知识&am…...

微信小程序(uniapp)实现腾讯云 IM 消息撤回

uniapp 实现腾讯云 IM 消息撤回功能实战指南 一、功能实现原理 腾讯云 IM 的消息撤回功能通过 消息修订&#xff08;Message Revision&#xff09; 机制实现&#xff0c;核心流程如下&#xff1a; 发送方调用撤回 API 删除指定消息云端生成撤回通知消息&#xff08;类型为 T…...

设计学生管理系统的数据库

在设计学生管理系统的数据库时&#xff0c;需要考虑多个实体及其关系。以下是一个基本的学生管理系统表结构设计&#xff0c;涵盖了核心实体和关系&#xff1a; 1. 用户表 (user) 存储所有系统用户的基本信息&#xff0c;包括学生、教师和管理员。 sql CREATE TABLE user (u…...

ArcGIS Pro 3.4 二次开发 - 图形图层

环境:ArcGIS Pro SDK 3.4 + .NET 8 文章目录 图形图层1.1 创建图形图层1.2 访问GraphicsLayer1.3 复制图形元素1.4 移除图形元素2 创建图形元素2.1 使用CIMGraphic创建点图形元素2.2 使用CIMGraphic创建线图元素2.3 使用 CIMGraphic 的多边形图形元素2.4 使用CIMGraphic创建多…...

Linux配置DockerHub镜像源配置

个人博客地址&#xff1a;Linux配置DockerHub镜像源配置 | 一张假钞的真实世界 因为某些原因&#xff0c;DockerHub官方镜像源已不可用&#xff0c;国内一些镜像源也已不可用&#xff0c;大家可以搜索可用的镜像源并修改配置。推荐一篇良心博文&#xff1a;https://zhuanlan.z…...

JDK21深度解密 Day 11:云原生环境中的JDK21应用

【JDK21深度解密 Day 111】云原生环境中的JDK21应用 本文是《JDK21深度解密:从新特性到生产实践的全栈指南》专栏的第11天内容,聚焦云原生环境中的JDK21应用。我们将深入探讨如何在容器化、微服务、Serverless等云原生架构中充分发挥JDK21的技术优势,提升Java应用的性能、稳…...

如何学习才能更好地理解人工智能工程技术专业和其他信息技术专业的关联性?

要深入理解人工智能工程技术专业与其他信息技术专业的关联性&#xff0c;需要跳出单一专业的学习框架&#xff0c;通过 “理论筑基 - 实践串联 - 跨学科整合” 的路径构建系统性认知。以下是分阶段、可落地的学习方法&#xff1a; 一、建立 “专业关联” 的理论认知框架 绘制知…...

Qt实现的水波进度条和温度进度条

一.效果 二.原理 1.水波 要模拟波浪,就要首先画出一条波浪线,正弦余弦曲线就很适合。 y=A*sin(ω*x+φ)+k y=A*cos(ω*x+φ)+k 这是正弦余弦曲线的公式,要想实现水波效果,那需要两条曲线,一条曲线的波峰对着另外一条曲线的波谷,要实现这样的曲线效果,只有让正弦曲线前移…...