源码篇 剖析 Vue2 双向绑定原理

news2025/4/25 17:56:49

前置操作

源码代码仓地址: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(),直接操作对象即可。

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

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

相关文章

单例模式与消费者生产者模型,以及线程池的基本认识与模拟实现

前言 今天我们就来讲讲什么是单例模式与线程池的相关知识&#xff0c;这两个内容也是我们多线程中比较重要的内容。其次单例模式也是我们常见设计模式。 单例模式 那么什么是单例模式呢&#xff1f;上面说到的设计模式又是什么&#xff1f; 其实单例模式就是设计模式的一种。…

STM32配置系统时钟

1、STM32配置系统时钟的步骤 1、系统时钟配置步骤 先配置系统时钟&#xff0c;后面的总线才能使用时钟频率 2、外设时钟使能和失能 STM32为了低功耗&#xff0c;一开始是关闭了所有的外设的时钟&#xff0c;所以外设想要工作&#xff0c;首先就要打开时钟&#xff0c;所以后面…

React 与 Vue:两大前端框架的深度对比

在前端开发领域&#xff0c;React 和 Vue 无疑是当下最受欢迎的两大框架。它们各自拥有独特的优势和特点&#xff0c;吸引了大量开发者。无论是初学者还是经验丰富的工程师&#xff0c;选择 React 还是 Vue 都是一个常见的问题。本文将从多个角度对 React 和 Vue 进行对比&…

Java24新增特性

Java 24&#xff08;Oracle JDK 24&#xff09;作为Java生态的重要更新&#xff0c;聚焦AI开发支持、后量子安全、性能优化及开发者效率提升&#xff0c;带来20余项新特性和数千项改进。以下是核心特性的分类解析&#xff1a; 一、语言特性增强&#xff1a;简化代码与模式匹配 …

Sentinel源码—6.熔断降级和数据统计的实现一

大纲 1.DegradeSlot实现熔断降级的原理与源码 2.Sentinel数据指标统计的滑动窗口算法 1.DegradeSlot实现熔断降级的原理与源码 (1)熔断降级规则DegradeRule的配置Demo (2)注册熔断降级监听器和加载熔断降级规则 (3)DegradeSlot根据熔断降级规则对请求进行验证 (1)熔断降级…

Volcano 实战快速入门 (一)

一、技术背景 随着大型语言模型&#xff08;LLM&#xff09;的蓬勃发展&#xff0c;其在 Kubernetes (K8s) 环境下的训练和推理对资源调度与管理提出了前所未有的挑战。这些挑战主要源于 LLM 对计算资源&#xff08;尤其是 GPU&#xff09;的巨大需求、分布式任务固有的复杂依…

用交换机连接两台电脑,电脑A读取/写电脑B的数据

1、第一步&#xff0c;打开控制面板中的网络和共享中心&#xff0c;如下图配置&#xff0c;电脑A和电脑B均要配置&#xff1b; 注意&#xff1a;要保证电脑A和电脑B在同一子网掩码下&#xff0c;不同的IP地址&#xff1b; 2、在电脑上同时按‘CommandR’&#xff0c;在弹出的输…

问道数码兽 怀旧剧情回合手游源码搭建教程(反查重优化版)

本文将对"问道数码兽"这一经典卡通风格回合制手游的服务端部署与客户端调整流程进行详细拆解&#xff0c;适用于具备基础 Windows 运维和手游源码调试经验的开发者参考使用。教程以实战为导向&#xff0c;基于原始说明内容重构优化&#xff0c;具备较高的内容查重避重…

WLAN共享给以太网后以太网IP为169.254.xx.xx以及uboot无法使用nfs下载命令的的解决方案

WLAN共享网络给以太网&#xff0c;实际上是把以太网口当作一个路由器&#xff0c;这个路由器的IP是由WLAN给他分配的&#xff0c;169.254.xx.xx是windows设定的ip&#xff0c;当网络接口无法从上一级网络接口获得ip时&#xff0c;该网络接口的ip被设置为169.254 &#xff0c;所…

ROS 快速入门教程03

8.编写Subscriber订阅者节点 8.1 创建订阅者节点 cd catkin_ws/src/ catkin_create_pkg atr_pkg rospy roscpp std_msgs ros::Subscriber sub nh.subscribe(话题名, 缓存队列长度, 回调函数) 回调函数通常在你创建订阅者时定义。一个订阅者会监听一个话题&#xff0c;并在有…

在 macOS 上合并 IntelliJ IDEA 的项目窗口

在使用 IntelliJ IDEA 开发时&#xff0c;可能会打开多个项目窗口&#xff0c;这可能会导致界面变得混乱。为了提高工作效率&#xff0c;可以通过合并项目窗口来简化界面。本文将介绍如何在 macOS 上合并 IntelliJ IDEA 的项目窗口。 操作步骤 打开 IntelliJ IDEA: 启动你的 I…

基于多用户商城系统的行业资源整合模式与商业价值探究

随着电子商务的蓬勃发展&#xff0c;传统的单一商家电商模式逐渐显现出一定的局限性。为了解决商家成本过高、市场竞争激烈等问题&#xff0c;多用户商城系统应运而生&#xff0c;成为一种新型的电商平台模式。通过整合行业资源&#xff0c;这种模式不仅极大地提升了平台和商家…

Three.js + React 实战系列 : 从零搭建 3D 个人主页

可能你对tailiwindcss毫不了解&#xff0c;别紧张&#xff0c;记住我们只是在学习&#xff0c;学习的是作者的思想和技巧&#xff0c;并不是某一行代码。 在之前的几篇文章中&#xff0c;我们已经熟悉了 Three.js 的基本用法&#xff0c;并通过 react-three-fiber 快速构建了一…

如何用大模型技术重塑物流供应链

摘要 在数字化转型加速的背景下&#xff0c;大模型技术凭借其强大的数据分析、逻辑推理和决策优化能力&#xff0c;正成为物流供应链领域的核心驱动力。本文深入探讨大模型如何通过需求预测、智能调度、供应链协同、风险管控等关键环节&#xff0c;推动物流行业从 "经验驱…

【银河麒麟高级服务器操作系统】磁盘只读问题分析

系统环境及配置 系统环境 物理机/虚拟机/云/容器 虚拟机 网络环境 外网/私有网络/无网络 私有网络 硬件环境 机型 KVM Virtual Machine 处理器 Kunpeng-920 内存 32 GiB 整机类型/架构 arm64 固件版本 EFI Development Kit II / OVMF 软件环境 具体操作系统版…

机器视觉的智能手机屏贴合应用

在智能手机制造领域&#xff0c;屏幕贴合工艺堪称"微米级的指尖芭蕾"。作为影响触控灵敏度、显示效果和产品可靠性的关键工序&#xff0c;屏幕贴合精度直接决定了用户体验。传统人工对位方式已无法满足全面屏时代对极窄边框和超高屏占比的严苛要求&#xff0c;而Mast…

AIM Robotics电动胶枪:智能分配,让机器人点胶涂胶精准无误

在现代工业自动化和智能制造领域&#xff0c;精确的液体分配技术正成为提升生产效率和产品质量的重要因素。AIM Robotics作为这一领域的创新者&#xff0c;提供了多种高效、灵活的点胶涂胶分配解决方案。本文将带您了解AIM Robotics的核心技术、产品系列以及在各行业的成功应用…

负环-P3385-P2136

通过选择标签&#xff0c;洛谷刷一个类型的题目还是很方便的 模版题P3385 P3385 【模板】负环 - 洛谷 Tint(input())def bellman(n,edges,sta):INFfloat(inf)d[INF]*(n1)d[sta]0for i in range(n-1):for u,v,w in edges:ncostd[u]wif ncost<d[v]:d[v]ncostfor u,v,w in e…

抖音的逆向工程获取弹幕(websocket和protobuf解析)

目录 声明前言第一节 获取room_id和ttwid值第二节 signture值逆向python 实现signature第三节 Websocket实现长链接请求protubuf反序列化pushFrame反序列化Response解压和反序列化消息体Message解析应答ack参考博客声明 本文章中所有内容仅供学习交流使用,不用于其他任何目的…

WPF 图片文本按钮 自定义按钮

效果 上面图片,下面文本 样式 <!-- 图片文本按钮样式 --> <Style x:Key="ImageTextButtonStyle" TargetType="Button"><Setter Property="Background" Value="Transparent"/><Setter Property="BorderTh…