问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

破防了!原来Vue响应式设计的如此巧妙

创作时间:
作者:
@小白创作中心

破防了!原来Vue响应式设计的如此巧妙

引用
CSDN
1.
https://blog.csdn.net/zz_jesse/article/details/146356605

Vue的响应式系统是其最核心的特性之一,它让我们通过简单地声明数据与视图的关系,而不需要手动操作DOM。本文通过对Vue2的源码的学习和理解来串一下整体设计。

响应式系统的基本原理

Vue2的响应式系统基于JS的Object.defineProperty API实现。他的系统设计目标很简单:当数据变化时,自动更新与之相关的视图。

核心模块

Vue的响应式系统主要由三个核心部分组成:

  • Observer(观察者):负责将普通JavaScript对象转换为响应式对象
  • Dep(依赖收集器):用于收集和管理依赖
  • Watcher(监听器):代表一个依赖,当数据变化时被通知

源码解析

1. Observer - 数据的"间谍"

Observer类位于src/core/observer/index.js,它的主要工作是将普通对象的属性转换为getter/setter:

// 简化版 Observer 类
export class Observer {
  constructor(value) {
    this.value = value
    this.dep = new Dep()
    
    // 给对象添加 __ob__ 属性,指向 Observer 实例
    def(value, '__ob__', this)
    
    if (Array.isArray(value)) {
      // 数组的特殊处理...
    } else {
      // 对象的处理:遍历对象的每个属性,转换为 getter/setter
      this.walk(value)
    }
  }
  walk(obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}

2.defineReactive - 响应式转换的核心

defineReactive函数是实现响应式的核心,它使用Object.defineProperty重新定义对象的属性:

// 简化版 defineReactive
export function defineReactive(obj, key, val) {
  // 每个属性都有自己的依赖收集器
  const dep = new Dep()
  
  // 获取属性的当前值
  val = val || obj[key]
  
  // 如果值是对象,递归使其响应式
  let childOb = observe(val)
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    
    // getter: 读取属性时收集依赖
    get: function reactiveGetter() {
      // 如果有当前正在计算的 watcher,就收集它作为依赖
      if (Dep.target) {
        dep.depend()  // 收集依赖
        if (childOb) {
          childOb.dep.depend()  // 对象本身也作为依赖
        }
      }
      return val
    },
    
    // setter: 修改属性时通知依赖更新
    set: function reactiveSetter(newVal) {
      if (newVal === val) return
      val = newVal
      // 如果新值是对象,也要使其响应式
      childOb = observe(newVal)
      // 通知所有依赖进行更新
      dep.notify()
    }
  })
}

这段代码就像给每个属性安装了"感应器":当你读取属性时,Vue会记录谁在使用这个属性;当你修改属性时,Vue会通知所有用到这个属性的地方进行更新。

3. Dep - 依赖收集器

Dep类位于src/core/observer/dep.js,它的职责是收集和管理依赖,以及在数据变化时通知这些依赖:

// 简化版 Dep
export default class Dep {
  constructor() {
    this.subs = []  // 存储依赖的数组
  }
  
  // 添加依赖
  addSub(sub) {
    this.subs.push(sub)
  }
  
  // 移除依赖
  removeSub(sub) {
    const index = this.subs.indexOf(sub)
    if (index > -1) {
      this.subs.splice(index, 1)
    }
  }
  
  // 收集当前 watcher 作为依赖
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  
  // 通知所有依赖更新
  notify() {
    // 复制一份依赖数组,防止更新过程中数组变化
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()  // 通知每个 watcher 更新
    }
  }
}
// 全局唯一的当前正在计算的 watcher
Dep.target = null

Dep就像一个通讯录,记录了谁对某个数据感兴趣,当数据变化时,它会逐个通知这些"订阅者"。

4. Watcher - 变化的观察者

Watcher类位于src/core/observer/watcher.js,它代表一个依赖,可以是组件的渲染函数、计算属性或用户使用$watch创建的观察者:

// 简化版 Watcher 
export default class Watcher {
  constructor(vm, expOrFn, cb, options) {
    this.vm = vm
    // 表达式或函数,用于获取监听的值
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)  // 解析表达式
    }
    this.cb = cb  // 值变化时的回调函数
    this.deps = []  // 记录这个 watcher 依赖了哪些 dep
    this.depIds = new Set()
    // 立即执行一次 getter,收集依赖
    this.value = this.get()
  }
  
  // 获取值并收集依赖
  get() {
    // 设置当前正在计算的 watcher
    Dep.target = this
    let value
    try {
      // 执行 getter,这会触发属性的 getter,从而收集依赖
      value = this.getter.call(this.vm, this.vm)
    } finally {
      // 清除当前 watcher
      Dep.target = null
    }
    return value
  }
  
  // 添加依赖
  addDep(dep) {
    const id = dep.id
    if (!this.depIds.has(id)) {
      this.depIds.add(id)
      this.deps.push(dep)
      dep.addSub(this)
    }
  }
  
  // 更新
  update() {
    // 异步更新队列
    queueWatcher(this)
  }
  
  // 实际执行更新的方法
  run() {
    const oldValue = this.value
    const newValue = this.get()
    if (newValue !== oldValue) {
      // 调用回调函数
      this.cb.call(this.vm, newValue, oldValue)
    }
  }
}

Watcher就像是一个"侦探",负责监视数据的变化,并在变化时执行特定的操作,如更新视图。

响应式系统的工作流程

现在用一个简单的例子来说下整个流程:

new Vue({
  el: '#app',
  data: {
    message: 'Hello'
  }
})
  1. 初始化阶段:
  • Vue实例化时,对data中的message属性调用defineReactive
  • 这会为message创建一个Dep实例,并设置getter/setter
  1. 依赖收集阶段:
  • 当渲染模板
    {{ message }}
    时,会创建一个渲染Watcher
  • 在渲染过程中,会读取message属性,触发其getter
  • getter中发现有当前Watcher (Dep.target),于是调用dep.depend()收集依赖
  • 这样,message的Dep就收集了渲染Watcher作为依赖
  1. 更新阶段:
  • 当我们执行vm.message = 'Hi'时,触发message的setter
  • setter中调用dep.notify()通知依赖更新
  • Dep通知所有收集的Watcher执行update方法
  • 渲染Watcher重新渲染组件,更新视图

响应式系统的巧妙之处

Vue响应式系统的巧妙之处在于:

  • 自动依赖收集:不需要手动声明依赖关系,Vue会在数据被访问时自动建立依赖关系
  • 精确更新:只有真正依赖某个数据的视图才会在该数据变化时更新,而不是整个应用重新渲染
  • 深度响应:对象的嵌套属性也能被监听,因为Observer会递归地将嵌套对象也变成响应式的

响应式系统的局限性

Vue 2响应式系统也有一些局限:

  • 不能检测对象属性的添加或删除:这是因为Object.defineProperty只能劫持已经存在的属性
  • 数组变化的部分限制:Vue重写了数组的变异方法(如push、pop等),但不能检测通过索引设置数组项或修改数组长度的变化
    这些限制在Vue 3中通过使用ES6的Proxy代替Object.defineProperty得到了解决。
© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号