前置操作
源码代码仓地址:https://github.com/vuejs/vue/tree/main
1.查看源码当前版本
当前版本为 v2.7.16
2.Clone 代码
在【Code】位置点击,复制 URL 用于 Clone 代码
3.执行 npm install
4.执行 npm run dev
前言
在 Vue 中最经典的问题就是双向绑定,在面试中,也会经常遇到这个问题。在没有阅读源码之前,曾经大段的文字并不好记忆,甚至会遗漏内容或者说错。本文将根据这一部分的源码深度理解一下它的实现原理。
开始之前,我们先了解一个词:“数据驱动视图”,我们看下面这张图,这可以理解为 MVVM(Model - View - ViewModel)的概念图:
Model 是数据与业务逻辑,View 是视图,ViewModel 用于连接 View 和 Model
数据可以理解为状态,视图可以理解为页面,页面会不断变化,不管是通过后端返回数据的变化还是用户操作,引起状态变化了,页面随之发生变化。有一篇文章的描述非常贴切:
变化的是 state 和 UI,不变的是 render(),因此 Vue 就是 render(),追踪 state 的变化去通知 UI 的更新。这个过程也叫做【变化侦测】。变化侦测即状态追踪,当数据一发生变化,就去更新视图。双向绑定的原理就是了解 Vue 如何对数据变化进行侦测的。
正文
我们常说的双向绑定的八股是这样的,下面将针对四点分开说明:
- 在初始化 data 数据时,会实例化一个 Observe 类,它对 data 数据中的每个属性进行递归遍历,并通过 Object.defineProperty() 给每个属性创建 getter 和 setter。
- 在数据读取时,getter 负责依赖收集;在对属性赋值时,setter 会触发依赖更新。
- 在模板编译阶段,Vue 使用 Compile 解析模板指令,每个指令对应的 DOM 节点都绑定一个 Watcher 实例,并通过该 Watcher 观察数据的变化,当相关的响应式数据有变动,Watcher 收到通知,调用更新函数重新渲染视图。
- Object.defineProperty() 的缺点:只能监听到对象的已有属性的读取和设置,而无法监听到新增属性或删除属性。无法监听 Array 数组元素的变化,需要重写数组的部分方法来手动收集依赖并触发更新。
我们将文章分为两个部分进行描述:对象(Object)的变化侦测 和 数组(Array)的变化侦测
对象(Object)的变化侦测
根据前言中提到的【数据驱动视图】:数据变化引起视图变化。首先明确数据何时变化,对数据的变化进行侦测。
1.“可观测” 化 Object 数据
当我们对数据的读取和写入进行追踪时,就可以明确知道数据何时被读取或修改,这时数据是“可观测的”。JS 的 Object.defineProperty 方法可以帮助我们侦测数据在何时变化。
通常我们通过Object构造函数或者字面量的方式定义对象,这里我们使用字面量定义一个 person 对象:
let person = {
name: 'John',
age: 30,
gender: 'male'
}
我们通过 person.name,person.age,person.gender 来访问并修改 person 对应的属性值,但这种读取和修改并不能主动通知我们,于是我们通过 Object.defineProperty 方法对它进行改写:
enumerable: true
enumerable 表示该属性是否可枚举。设置为 true 后,age 属性会出现在 for...in 循环或者 Object.keys() 等方法中。
configurable: true
configurable 表示该属性是否可删除或修改其属性描述符。如果设置为 true,则可以删除或更改属性的配置。如果设置为 false,则该属性就不能被删除或更改。
let person = {}
let val = 30
Object.defineProperty(person, 'age', {
enumerable: true,
configurable: true,
get(){
console.log('age属性被读取')
return val
},
set(newVal){
console.log('age属性被修改')
val = newVal
}
})
通过 Object.defineProperty 方法定义一个对象 person 的 age 属性,使得该属性具有 自定义的 getter 和 setter 方法,并且能在读取或修改时输出日志。
假如我们做如下操作:
person.age // 读取 age 属性
person.age = 35 // 修改 age 属性
console.log(person.age) // 再次读取 age 属性
将会输出:
age属性被读取
age属性被修改
age属性被读取
此时 person 的 age 属性就是“可观测的”,接下来使 person 的所有属性变得“可观测”:
该代码片段源码位置:src/core/observer/index.ts,此处只展示相关的代码部分
源码分析:
/**
* Observer类会通过递归的方式把一个对象的所有属性都转化成可观测对象
*/
export class Observer {
constructor(public value: any) {
this.value = value
def(value, '__ob__', this)
if (isArray(value)) {
// ...
} else {
this.walk(value)
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
// ...
}
定义的 Observer 类用于递归地将一个对象的所有属性转化为可观测对象。
- value:Observer 类的一个成员,表示需要观察的对象或数组。
- def(value, '__ob__', this):为 value 对象添加了一个名为 __ob__ 的特殊属性,值是当前的 Observer 实例。这样就能在对象上追踪其响应式系统。
- isArray(value):检查 value 是否是一个数组,此处暂时不讨论,在下部分数组的变化侦测中描述。
- this.walk(value):如果 value 不是数组,调用 walk 方法递归地将该对象的所有属性转化为响应式。
下面的 defineReactive 通过 Object.defineProperty(),为对象的某个属性定义了 getter 和 setter,使得该属性的值可以被观察并在访问或修改时触发相应的操作。
/**
* 使一个对象转化成可观测对象
* @param { Object } obj 对象
* @param { String } key 对象的key
* @param { Any } val 对象的某个key的值
*/
export function defineReactive(obj: object, key: string, val?: any) {
// ...
// 获取该属性的 getter 和 setter,如果该属性本身有 getter 或 setter,则会被用于后续的访问操作
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) { // 该属性的 configurable 为 false,函数直接返回
return
}
// 如果该属性有预定义的 getter 和 setter,就将它们分别存储在 getter 和 setter 中。
const getter = property && property.get
const setter = property && property.set
// 如果属性值没有传入(即 val 为 undefined),则将 val 设置为 obj[key] 的当前值。
if (
(!getter || setter) &&
(val === NO_INITIAL_VALUE || arguments.length === 2)
) {
val = obj[key]
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log(`${key}属性被读取了`)
return val
},
set: function reactiveSetter(newVal) {
if(val === newVal){
return
}
console.log(`${key}属性被修改了`)
val = newVal
}
})
}
因此我们可以这样定义 person 了,使得 person 中的每个属性都是“可观测的”:
let person = new Observer({
name: 'John',
age: 30,
gender: 'male'
})
至此,八股中的第一点说明完毕:
- 在初始化 data 数据时,会实例化一个 Observe 类,它对 data 数据中的每个属性进行递归遍历,并通过 Object.defineProperty() 给每个属性创建 getter 和 setter。
在第二点中,提到了依赖收集及通知依赖更新,那何为依赖?何时收集依赖?何时通知依赖更新?
2.1何为依赖?
当属性变得“可观测”之后,当数据发生变化即可通知视图更新,但通知哪个视图更新就是本段需要解决的问题,如何找到这个视图呢?很好理解的是:谁用到了这个数据 即 谁依赖了这个数据,此时将它收集到依赖数组中即可。 当数据变化后,将对应的依赖数组中将所有依赖通知一遍从而触发视图更新就可以了。
2.2何时收集依赖?何时通知依赖更新?
当数据变化时,在 getter 中收集依赖,在 setter 中通知依赖更新
2.3如何依赖收集?
简单而言,依赖收集的核心思想是为每个数据创建一个依赖数组,记录哪些对象或组件依赖于该数据。当这个数据发生变化时,通知依赖于它的所有对象进行更新。然而,直接使用数组来存放这些依赖可能会带来一定的代码耦合,难以扩展和维护。因此可以为每个数据创建一个专门的依赖管理器,将该数据的所有依赖集中管理。这个依赖管理器通常通过 Dep 类来实现。
Dep 类负责管理和存储所有依赖该数据的观察者(即依赖项)。每当一个属性被访问时,Dep 会将当前的观察者添加到该数据的依赖队列中,这样在数据更新时,所有依赖于该数据的观察者都会被及时通知。
该代码片段源码位置:src/core/observer/dep.ts,此处只展示相关的代码部分
观察者
(Observer)和订阅者
(Subscriber)实际上是同一个概念,只是使用不同的术语
export default class Dep {
static target?: DepTarget | null
id: number
subs: Array<DepTarget | null>
_pending = false
constructor() {
this.id = uid++
this.subs = []
}
addSub(sub: DepTarget) {
this.subs.push(sub)
}
removeSub(sub: DepTarget) {
this.subs[this.subs.indexOf(sub)] = null
if (!this._pending) {
this._pending = true
pendingCleanupDeps.push(this)
}
}
depend(info?: DebuggerEventExtraInfo) {
if (Dep.target) {
Dep.target.addDep(this)
// ...
}
}
notify(info?: DebuggerEventExtraInfo) {
const subs = this.subs.filter(s => s) as DepTarget[]
for (let i = 0, l = subs.length; i < l; i++) {
const sub = subs[i]
// ...
sub.update()
}
}
}
源码分析:
static target?: DepTarget | null
id: number
subs: Array<DepTarget | null>
_pending = false
- target:一个静态属性,用于存储当前正在收集依赖的目标。Dep.target 会指向当前的观察者(即正在访问该属性的组件或对象)。
- id:每个 Dep 实例都会有一个唯一的 id,通常用来标识不同的依赖管理器。
- subs:用于存储所有依赖于这个 Dep 的观察者。这些观察者会在属性发生变化时被通知更新。
- _pending:标记当前 Dep 是否处于待清理状态。
addSub(sub: DepTarget) {
this.subs.push(sub)
}
removeSub(sub: DepTarget) {
this.subs[this.subs.indexOf(sub)] = null
if (!this._pending) {
this._pending = true
pendingCleanupDeps.push(this)
}
}
- addSub(sub):一个观察者(sub)添加到 subs 数组中。
- removeSub(sub):移除一个订阅者(sub)。用 null 来标记被移除的观察者,并通过 _pending 标志进行延迟清理,避免在遍历观察者时修改数组结构。
- pendingCleanupDeps.push(this):当某个观察者被移除时,将当前 Dep 添加到待清理的数组中,清理过程会在稍后的某个时机执行。
depend(info?: DebuggerEventExtraInfo) {
if (Dep.target) {
Dep.target.addDep(this)
// ...
}
}
- depend():用于依赖收集。在属性的 getter 被访问时,depend() 被调用,当前的观察者(Dep.target)会被添加到当前 Dep 的观察者列表中。
notify(info?: DebuggerEventExtraInfo) {
const subs = this.subs.filter(s => s) as DepTarget[]
for (let i = 0, l = subs.length; i < l; i++) {
const sub = subs[i]
// ...
sub.update()
}
}
当数据发生变化时,调用 notify() 来通知所有依赖该数据的观察者。
- 过滤空值:通过 this.subs.filter(s => s) 移除 null 或无效的观察者。
- 调用更新:遍历所有观察者,调用每个观察者的 update() 方法。
至此,第二大点说明完毕:
- 在数据读取时,getter 负责依赖收集;在对属性赋值时,setter 会触发依赖更新。
总结依赖收集和更新通知流程:
依赖收集(depend()):
当属性的 getter 被访问时,Dep.target 会指向当前正在访问该属性的观察者。depend() 会将该观察者添加到 Dep 的订阅者列表 subs 中。
更新通知(notify()):
当数据发生变化时,notify() 会被调用。它会遍历所有依赖该数据的观察者,并通知它们更新。
3.Watcher 类的实例
在上文中,我们提到过:“谁用到了这个数据,即谁依赖了这个数据”,这里的“谁”实际上是指 Watcher 类的实例。谁依赖这个数据,谁就是依赖,我们就为谁创建一个 Watcher 实例。
当数据发生变化时,我们并不直接通知所有依赖的数据更新,而是首先通知对应的 Watcher 实例,由 Watcher 实例通知与之相关的视图组件。
好处是,能够将数据的变化和视图的更新解耦,避免直接操作视图。
源码分析:
该代码片段源码位置:src/core/observer/watcher.ts,此处只展示相关的代码部分
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.vm
let value = this.getter.call(vm, vm)
window.target = undefined;
return value
}
update () {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
- vm:绑定 Vue 实例。Watcher 需要知道在哪个组件实例上执行。
- cb:观察者的数据变化时调用的回调函数。
- value:通过 get() 获取初始值。
在 get() 方法中,Vue 会将当前的 Watcher 实例赋值给全局变量 window.target,以便在访问数据的 getter 时进行依赖收集。每次读取某个响应式数据时,Watcher 会被加入到对应数据的 Dep 实例中,依赖收集过程通过调用 dep.depend() 完成。在 dep.depend() 中,Watcher 被从 window.target 中获取,并将其添加到该数据的依赖数组中。
当数据发生变化时,setter 被触发,进而调用 dep.notify() 方法,通知所有依赖该数据的 Watcher 实例。在 dep.notify() 中,遍历所有依赖的 Watcher,并调用它们的 update() 方法。Watcher 的 update() 方法会执行数据变化的回调函数,从而触发视图的重新渲染,确保视图和数据保持同步。
我们可以绘制如下关系图便于加深理解:
该代码片段源码位置:src/core/util/lang.ts,此处只展示相关的代码部分
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
export function parsePath(path: string): any {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
至此,第三大点说明完毕:
- 在模板编译阶段,Vue 使用 Compile 解析模板指令,每个指令对应的 DOM 节点都绑定一个 Watcher 实例,并通过该 Watcher 观察数据的变化,当相关的响应式数据有变动,Watcher 收到通知,调用更新函数重新渲染视图。
4.Object.defineProperty 缺点
- Object.defineProperty() 的缺点:
- 只能监听到对象的已有属性的读取和设置,而无法监听到新增属性或删除属性。
- 无法监听 Array 数组元素的变化,需要重写数组的部分方法来手动收集依赖并触发更新。
这里我们分为两部分来说,首先缺点1:Object.defineProperty() 只能监听已经存在的属性的 读取(getter) 和 设置(setter)。如果动态添加一个新的属性,Object.defineProperty() 不会自动为新属性定义 getter 和 setter。对于删除属性的操作,Object.defineProperty() 也无法感知,删除属性不会触发视图更新。
而解决这一问题,可以用到 Vue 的两个全局 API(Vue.set,Vue.delete),为了使内容连贯,这里只说明与内容有关的两个 API,后续文章会针对全局 API 进行说明。
Vue.set 允许我们添加新的属性到响应式对象上,并确保该属性会成为响应式的,能触发视图更新。
Vue.set(obj, 'newKey', 'newValue');
为什么要用 Vue.set?
如果通过普通的 JS 赋值方式(obj.key = value)来添加新属性,Vue 无法侦测到这个新属性的变化,不会触发视图更新。
let obj = { a: 1 }; obj.b = 2; // 这里 Vue 不会侦测到 b 属性的变化 // 使用 Vue.set Vue.set(obj, 'b', 2); // 这里 Vue 会侦测到 b 属性的变化
Vue.delete 解决了 删除属性 的问题。
Vue.delete(obj, 'key');
为什么要用 Vue.delete?
如果使用 delete obj.key 删除对象的属性,Vue 的响应式系统并不会触发视图更新。let obj = { a: 1, b: 2 }; delete obj.b; // Vue 无法检测到 b 属性被删除 // 使用 Vue.delete Vue.delete(obj, 'b'); // Vue 会侦测到 b 属性被删除
5.总结
- 首先,通过 Object.defineProperty() 方法使对象数据可观测,并封装了 Observer 类,方便将对象及其所有属性转化为 getter 和 setter,以便侦测数据变化。
- 接着,了解依赖收集的概念:在 getter 中收集依赖,在 setter 中通知依赖更新。使用 Dep 类管理这些依赖,将依赖项存储在其中。
- 最终,我们为每个依赖创建了一个 Watcher 实例。当数据发生变化时,setter 会触发依赖通知,Watcher 实例接收到更新通知后执行视图更新或回调函数等操作。
数组(Array)的变化侦测
之所以将 Object 数据和 Array 数据分开描述,通过 Object.defineProperty() 缺点2可知:Object.defineProperty() 这个方法是在对象的原型链上操作的,而 Array 是内建的特殊对象,它有自己的方法和行为。
原理相同:数据读取时收集依赖,数据变化时通知依赖更新。并且依赖收集的方式都是在 getter 中收集,此处产生疑惑,Array 无法使用 Object.defineProperty() 方法,如何在 getter 中收集依赖?
在 Vue 中,定义 data 数据的时候,是这样写的:
data(){ return { arr: [1, 2, 3] } }
arr 数据是通过对象(例如 data 中的一个属性)存储的,当我们访问 arr 时,实际上是通过访问包含它的对象(例如 this.data)的 getter,从而触发了依赖收集。
当 Object 数据变化时,会触发这个数据上的 setter,但 Array 数据没有 setter,但 JS 中提供了操作数组的方法,只要对这些方法进行重写,在不改变原有功能的前提下,新增一些功能即可:
let arr = [1, 2, 3];
// 在 Array 的原型上添加一个新的 `newPush` 方法
Array.prototype.newPush = function(val) {
console.log('arr 被修改了');
// 调用原生的 push 方法,将值添加到数组中
this.push(val);
};
// 使用新的 `newPush` 方法
arr.newPush(4); // 输出: arr 被修改了
// 查看修改后的数组
console.log(arr); // 输出: [1, 2, 3, 4]
这里我们不仅可以实现在原数组中 push 一个新元素4,还可以输出日志“arr 被修改了”,或进行其他的操作例如“通知变化”。这是实现数据变化追踪的一种常见做法,类似于 Vue 这样的框架,正是通过这种方式来处理数组的响应式更新。
1.数组方法拦截器
在 Vue 中,创建了一个数组方法拦截器,它位于数组实例和 Array.prototype 之间。拦截器通过重写部分数组操作方法,确保当数组实例调用这些方法时,实际执行的是拦截器中定义的重写方法,而不是 Array.prototype 上的原生方法。
对数组内容有直接修改作用的方法最常见的7个分别是:push,pop,shift,unshift,splice,sort,reverse
该代码片段源码位置:src/core/observer/array.ts,此处只展示相关的代码部分
源码分析:
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
- arrayProto 引用的是 Array.prototype,即所有数组实例的原型对象。
- arrayMethods 是通过 Object.create(arrayProto) 创建的一个新对象,它继承自 Array.prototype。因此 arrayMethods 会拥有 Array.prototype 中的方法和属性。
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
// ...
ob.dep.notify()
return result
})
})
- forEach: 遍历 methodsToPatch 中定义的所有方法,逐个进行劫持。
- original: 保存原始的数组方法(push、pop、shift 等),以便稍后在劫持的函数中调用。
- def: 用来给 arrayMethods 添加新方法的函数。这种方式将会通过 Object.defineProperty 来劫持原有的方法,确保 arrayMethods 上的这些方法能够实现我们自定义的行为。
ob.dep.notify()
- ob 是数组的响应式对象(通过 this.__ob__ 访问)。它代表被 Observer 监控的对象。
- dep.notify() 是通知依赖更新的操作。
const result = original.apply(this, args)
return result
- original.apply(this, args) 调用原始的数组方法并传入参数 args。保证数组的基本功能不被破坏。
- 返回 result,即原始数组方法的返回值。
拦截器实现之后,需要将它挂载到数组实例与 Array.prototype 之间才会生效(也就是把数据的 __proto__ 属性设置为拦截器 arrayMethods)。在 Object 数据变化侦测部分中我们说到了 index.ts 文件,对 Array 数据处理的源码省略了,如下图:
现在我们针对 Array 数据的处理进行说明:
export class Observer {
constructor (value) {
this.value = value
this.dep = new Dep() // 实例化一个依赖管理器,用来收集数组依赖
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
} else {
this.walk(value)
}
}
}
Array.isArray(value):如果 value 是一个数组,Vue 会使用 augment 函数来扩展数组的原型方法(让它具备响应式能力)。
- augment 选择了不同的函数,具体取决于浏览器是否支持 __proto__
export const hasProto = '__proto__' in {}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
function protoAugment (target, src: Object, keys: any) {
target.__proto__ = src
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
- hasProto: 检测当前环境是否支持 __proto__ 属性,它用于访问或设置对象的原型。
- arrayKeys: 获取 arrayMethods 上所有的属性名称。
- 如果浏览器支持 __proto__,protoAugment 会直接修改 target(数组)的原型,将其指向 src(arrayMethods)。target 就会继承 arrayMethods 中定义的劫持方法,触发响应式操作。
- 如果浏览器不支持 __proto__,则通过 Object.defineProperty 来将 arrayMethods 中的每个方法逐个拷贝到 target(数组)上,从而实现劫持。
2.如何依赖收集?
在 Object 数据的依赖收集部分,源码我们已经进行说明,它是在 defineReactive 方法中实现。而 Array 数据通过 observe 函数,首先检查传入数据是否已有 __ob__
属性,表示是否已经是响应式。如果没有,创建一个新的 Observer
实例将其转化为响应式,并返回该实例。
export function observe (value, asRootData){
if (!isObject(value) || value instanceof VNode) {
return
}
let ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}
3.如何通知依赖?
通知依赖在 methodsToPatch 拦截器中进行,访问到依赖之后,调用依赖管理器的 dep.notify() 方法,让它去通知依赖更新即可。
4.侦测数组新增元素
上面我们处理了数组中已有元素的侦测,如果数组中新增一个元素,也需要将这个元素转换成可侦测的响应式数据。
源码分析:
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// ...
ob.dep.notify()
return result
})
})
对于新增元素的方法,主要有三个:push、unshift、splice。每个方法都被包裹在一个新的 mutator 函数中。该函数用于在执行原始数组方法之前和之后,做一些自定义的逻辑处理。
- 对于 push 和 unshift:inserted 会指向传入的参数,因为这些方法会向数组中添加新元素。
- 对于 splice:inserted 会指向数组的第三个及之后的参数,因为 splice 的前两个参数分别是起始索引和删除的元素个数,只有从第三个参数开始的才是新插入的元素。
5.拦截器缺点
通过拦截器可以实现 Array 数据的变化侦测,但只有通过数组原型上的方法对数组进行操作才可以被侦测,如果我们使用数组下标的方式操作数组就无法侦测,比如:
let arr = [1,2,3]
arr[0] = 4; //访问数组下标修改数组数据
arr.length = 0 // 通过修改数组长度清空数组
同理,这里也可以使用上一部分【Object.defineProperty 缺点】中 Vue.set 和 Vue.delete 两个全局 API 解决。
至此,第四大点说明完毕:
- Object.defineProperty() 的缺点:只能监听到对象的已有属性的读取和设置,而无法监听到新增属性或删除属性。无法监听 Array 数组元素的变化,需要重写数组的部分方法来手动收集依赖并触发更新。
6.总结
- 创建 Observer 实例,对数组及其中元素进行响应式处理。
- 重写数组的方法,通过 Object.defineProperty 拦截数组的常用方法,触发依赖更新。
- 使用 Dep 管理依赖,确保数组变化时可以触发相关视图的更新。
- 对数组内部的对象或其他数组进行递归响应式处理,保证数据的深层变化也能够被侦测。
总结
Vue2 中的 Object.property 的缺点在 Vue3 中的 proxy 得以解决,Vue3 中响应式系统加强,不再需要 Vue.set() 和 Vue.delete(),直接操作对象即可。