文章目录
- 引言
- 数据劫持
- 收集依赖
- 数组处理
- 渲染watcher
- vue3中的响应式
引言
vue的设计思想是数据双向绑定、数据与UI自动同步,即数据驱动视图。
为什么会这样呢?这就不得不提vue的响应式原理了,在使用vue的过程中,我被vue的响应式设计深深着迷 ,下面我们就从源码的角度,来分析一下vue是如何实现响应式原理的。
在vue2中,主要分为三个过程:
- 数据劫持:Vue 会遍历组件实例的所有属性,并使用
Object.defineProperty
将这些属性转换为 getter/setter 形式。这样做的目的是为了追踪依赖以及触发更新。 - 依赖收集:当渲染函数执行时,如果访问了响应式数据,那么这个访问会被记录下来,形成一个“依赖”。这意味着哪些视图或计算属性依赖于当前的数据。
- 视图更新:一旦某个响应式数据发生改变(即调用了 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,设置新的值的时候,如果符合条件,就被修改,不符合条件,就被拦截到,使用自定义的getter
和setter
来重写了原有的行为,对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
,之后新建子组件watcher
,window.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()
}
Watcher
的get
方法做如下修改
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
方法会读取数据的值,从而触发了数据的getter
,getter
执行完毕后,实例的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 }
我们先实例化了一个watcher1
,watcher1
依赖obj.a
,那么window.target
就是watcher1
。之后我们访问了obj.b
,会发生什么呢?访问obj.b
会触发obj.b
的getter
,getter
会调用dep.depend()
,那么obj.b
的dep
就会收集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
来实现数据的响应式转换,但是这种方法对数组的某些操作不起作用。
- 无法检测数组变化:使用
Object.defineProperty
可以很好地追踪对象属性的变化(通过 getter 和 setter),但是对于数组,直接修改数组元素(例如arr[0] = newValue
或者arr.length = newLength
)不会触发 setter,因此 Vue 不能检测到这些变化并更新视图。 - 数组方法的直接调用问题:虽然 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__
,闭包中的dep
与childOb.dep
保存的内容相同。
也就是说,每个属性(比如arr
属性)的getter
和setter
不仅通过闭包保存了属于自己的dep
,而且通过__ob__
保存了自己的Observer
实例,Observer
实例上又有一个dep
属性。
但是dep
和childOb.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)}}
}
newDeps
和newDepIds
用来再一次取值过程中避免重复依赖,比如:{{ name }} -- {{ name }}
deps
和depIds
用来再重新渲染的取值过程中避免重复依赖
再执行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设计,存在以下缺陷:
- 递归调用导致性能损耗
- 无法检测:
- 属性的新增/删除
- 数组索引的直接修改
- 数组长度的直接修改
Vue2 通过 Vue.set 和 Vue.delete 方法解决了前两个问题,但数组长度修改的问题始终无法解决。
使用Proxy之后
相比之下,Proxy 专门用于对象代理,提供对象级别的拦截。它可以拦截几乎所有对象操作,包括:
- 属性访问/修改/删除
- 枚举操作
- in 运算符
- 函数调用
- 原型操作
- new 操作符调用
Proxy 的工作机制是对原对象创建代理,只有通过代理对象的操作才会被拦截。所有对代理对象的修改最终都会作用于原对象,Proxy 仅作为操作拦截和处理的中介层。
可以说 Proxy 完美弥补了 Object.defineProperty 的缺点,Vue3 使用 Proxy 后不再需要递归操作、不再需要重写数组的那七个方法、不再需要 Vue.set 和 Vue.delete。