破防了!原来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得到了解决。
热门推荐
成人高考毕业证与学位证的区别及难易分析
违章停车怎么查询?一文详解查询方式与处罚规定
深入了解离岸贸易:定义、优势及实践指南
木瓜和酸奶可以一起吃吗
浅谈发酵后蛋白的分离纯化技术
房贷利率的计算方法是怎样的
人寿保险退保原因的研究分析
175款产品退保数据大起底:万能险、投连险成重灾区,23款产品全年退保率超20%
整个生命科学的发展历史
《河峪颂》摩崖石刻:关陇古道上的历史见证
第一性原理:马斯克如何颠覆传统,开启创新之路
中本聪身份之谜:神秘面纱下的智慧之光,保护创新源头的纯净力量
监督学习、无监督学习、半监督学习、弱监督学习、强化学习 和 主动学习
进制转换小课堂:二进制如何变身十进制、八进制与十六进制?
微积分-函数与极限3(函数极限)
便常规黄褐色软便正常吗 你的便便颜色透露了哪些健康信号
如何合法拆除违法建筑?容县土地征收类型及补偿标准详解
用爱点亮童年 用心陪伴成长
荧屏警察形象彰显国家法治进程
ISFJ-T名人指南:揭秘太妍、Wendy、布魯斯威利等性格特質
时光相册丨从千篇一律到我型我秀——跟着新华社镜头看服饰变迁
房产税退税条件及办理流程详解,助您轻松返还税款
广州磋商降低低空救援价格 3万元直升机转运急救能接受吗?
何捷:导读课、推进课、交流课,如何设计整本书阅读课?
什么叫矫正视力
济滨高铁济南东特大桥矮塔斜拉桥首对斜拉索成功安装
玻璃杯哪种材质最安全健康?玻璃杯材质等级全解析
税务零申报情况说明书:法律框架、合规要点与实务探讨
成人英语口语在线培训有哪些互动练习?
《ch资产计价》课件