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

news2025/6/5 16:12:44

文章目录

  • 引言
  • 数据劫持
  • 收集依赖
  • 数组处理
  • 渲染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); // 输出 25

person.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) return
      value = newValue
    }
  })
}

defineReactive(obj, age, 29)

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

class Observer {
  constructor(value) {
    this.value = value
    this.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
  // 调用Observer
  new Observer(data)
}

class Observer {
  constructor(value) {
    this.value = value
    this.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) return
      value = newValue
      observe(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 = data
    this.expression = expression
    this.cb = cb
    // 初始化watcher实例时订阅数据
    this.value = this.get()
  }
  
  get() {
    const value = parsePath(this.data, this.expression)
    return value
  }
  
  // 当收到数据变化的消息时执行该方法,从而调用cb
  update() {
    this.value = parsePath(this.data, this.expression) // 对存储的数据进行更新
    cb()
  }
}

function parsePath(obj, expression) {
  const segments = expression.split('.')
  for (let key of segments) {
    if (!obj) return
    obj = obj[key]
  }
  return obj
}

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

update() {
  const oldValue = this.value
  this.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 = [] // 存放watcher
  observe(value)
  Object.defineProperty(data, key, {
    get: function reactiveGetter() {
      return value
    },
    set: function reactiveSetter(newValue) {
      if (newValue === value) return
      value = newValue
      observe(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) return
      value = newValue
      observe(newValue)
      dep.notify() // 修改
    }
  })
}

watcher中的代码里

get() {
  window.target = this
  const 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 = this
  const value = parsePath(this.data, this.expression)
  window.target = null // 新增,求值完毕后重置window.target
  return 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) return
  value = newValue
  observe(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') return
  new Observer(data)
}

class Observer {
  constructor(value) {
    this.value = value
    this.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) return
      value = newValue
      observe(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 = null

const 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 = data
    this.expression = expression
    this.cb = cb
    this.value = this.get()
  }

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

  update() {
    const oldValue = this.value
    this.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) return
    obj = 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 = value
    if (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__ === proxyPrototype
  this.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') return
  let ob
  // __ob__还可以用来标识当前对象是否被监听过
  if (value.__ob__ && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else {
    ob = new Observer(value)
  }
  return ob
}

Observer做修改:

constructor(value) {
  this.value = value
  this.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 = null
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case '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 = newVal
  childOb = 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 = cb
  this.value = this.get()
}

// parsePath的改造,返回一个函数
function parsePath(path) {
  const segments = path.split('.')
  return function (obj) {
    for (let key of segments) {
      if (!obj) return
      obj = obj[key]
    }
    return obj
  }
}

get修改

get() {
  pushTarget(this)
  const data = this.data
  const 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.value
    this.value = value
    this.cb.call(this.vm, value, oldValue)
  }
}

function isObject(target) {
  return typeof target === 'object' && target !== null
}

重复收集

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

为每个dep添加一个id

let uid = 0

constructor() {
  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收集过则进入if
  if (!this.newDepIds.has(id)) {
    // watcher中记录收集自己的dp
    this.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和newDepIds
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    // 清空newDepIds
    this.newDepIds.clear()
    // 交换deps和newDeps
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    // 清空newDeps
    this.newDeps.length = 0
  }

重新收集依赖

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

//删除无效依赖
cleanUpDeps() {
  // 增加
  let i = this.deps.length
  while (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) return
  const 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.coloradmin.cn/o/2397388.html

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

相关文章

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

在 Windows 桌面应用开发中&#xff0c;实现自动化构建与打包发布是一项非常实用的工程实践。本文以我在开发PackTes项目时的为例&#xff0c;介绍如何通过 GitLab CI 配合 Inno Setup、批处理脚本、Qt 构建工具&#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…

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 设备…

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

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

初识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;如雷击…

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

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

WEBSTORM前端 —— 第3章:移动 Web —— 第4节:移动适配-VM

目录 一、适配方案 二、VM布局 ​编辑 三、vh布局 四、案例—酷我音乐 一、适配方案 二、VM布局 三、vh布局 四、案例—酷我音乐

【Zephyr 系列 3】多线程与调度机制:让你的 MCU 同时干多件事

好的,下面是Zephyr 系列第 3 篇:聚焦 多线程与调度机制的实践应用,继续面向你这样的 Ubuntu + 真板实战开发者,代码清晰、讲解通俗、结构规范,符合 CSDN 高质量博客标准。 🧠关键词:Zephyr、线程调度、k_thread、k_sleep、RTOS、BluePill 📌适合人群:想从裸机开发进…

Kotlin-特殊类型

文章目录 数据类型枚举类型匿名类和伴生对象单例类伴生对象 数据类型 声明一个数据类非常简单: //在class前面添加data关键字表示为一个数据类 data class Student(var name: String, var age: Int)数据类声明后,编译器会根据主构造函数中声明的所有属性自动为其生成以下函数…

nssctf第二题[SWPUCTF 2021 新生赛]简简单单的逻辑

这是题目&#xff0c;下载后得到一个python文件,打开 解读代码&#xff1a; for i in range(len(list)):key (list[i]>>4)((list[i] & 0xf)<<4)result str(hex(ord(flag[i])^key))[2:].zfill(2)list[i]>>4&#xff1a;从列表中取数字同时高4位向右位…

《Discuz! X3.5开发从入门到生态共建》第3章 Discuz! X3.5 核心目录结构解析-优雅草卓伊凡

《Discuz! X3.5开发从入门到生态共建》第3章 Discuz! X3.5 核心目录结构解析-优雅草卓伊凡 3.1 系统核心目录结构 Discuz! X3.5采用模块化设计&#xff0c;主要目录结构如下&#xff1a; discuz_root/ ├─ api/ // API接口目录 ├─ config/ …

【HarmonyOS 5】鸿蒙应用实现发票扫描、文档扫描输出PDF图片或者表格的功能

【HarmonyOS 5】鸿蒙应用实现发票扫描、文档扫描输出PDF图片或者表格的功能 一、前言 图(1-1) HarmonyOS 系统提供的核心场景化视觉服务,旨在帮助开发者快速实现移动端文档数字化功能。 其核心能力包括:扫描合同、票据、会议记录并保存为 PDF 分享。拍摄课堂 PPT、书籍章…