破防了!原来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'
}
})
- 初始化阶段:
- Vue实例化时,对data中的message属性调用defineReactive
- 这会为message创建一个Dep实例,并设置getter/setter
- 依赖收集阶段:
- 当渲染模板{{ message }}时,会创建一个渲染Watcher
- 在渲染过程中,会读取message属性,触发其getter
- getter中发现有当前Watcher (Dep.target),于是调用dep.depend()收集依赖
- 这样,message的Dep就收集了渲染Watcher作为依赖
- 更新阶段:
- 当我们执行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得到了解决。
热门推荐
抽纸选购完全指南:从材质到品牌,一文读懂如何挑选优质抽纸
找到“降血脂”的新方法了,让你的坏胆固醇跟着粪便一起排出
空调遥控器故障维修全攻略:从原因分析到解决方案
皮肤自我修复的节奏:夜间修复的黄金时间
晚上睡前抹什么护肤品
《原神》爱可菲培养攻略
司马迁的历史巨著、体例及其影响是什么
AI 对人类社会经济、科技和生活的影响:快餐文化时代的深度剖析
突然打嗝又止不住?医生给出了3种科学方法
突然胸闷、气短伴濒死感?小心,别把“惊恐发作”当成“心脏病”
CPU导热泥:提升散热性能,有什么独特之处?
长期喝蒲公英茶的好处和坏处
绿豆汤的营养价值及功效与禁忌
“人工智能+”教育行动,开启教育数字化转型新篇章
临床试验表明,每日1克omega-3可有效延缓生物年龄
警惕!这些异常出血,或是癌症早期预警信号
深入解析 API 類型與架構 API 的多元世界
春分养生指南:饮食睡眠双兼顾,开启健康好时节
德国Venta空气清洗机常见问题和使用指南(官方翻译版本)
海肠VS沙虫:一场海洋食材的全方位较量与美食文化探寻
重复经颅磁刺激(rTMS)在抑郁症中的应用
经颅磁刺激(TMS)相关科普第二篇
碳如何点燃文明,又影响未来
如何查询周边5G信号覆盖情况:详解手机设置中的方法和步骤
佳士得专家指南:陨石收藏
为什么詹姆斯打球给人一种没技术的感觉?有两点原因
道家追求清净无为,为什么又要追求永生呢?
电缆敷设的主要方式
手机电池的未来,探索革新与进步
硬盘的基本知识与选购指南