浅析Vue的响应式原理对象篇
一、对象响应式数据的创建
在组件的初始化阶段,将对传入的状态进行初始化,以下以data
为例,会将传入的数据包装为响应式的数据。
// main.js
new Vue({ // 根组件
render: h => h(App)
})
// app.vue
<template>
<div>{{info.name}}</div> // 只用了info.name属性
</template>
export default {
data() {
return {
info: {
name: 'cc',
sex: 'man' // 即使是响应式数据,没被使用就不会进行依赖收集
}
}
}
}
我们将以上面代码为例分析,这种结构其实是一个嵌套组件,只不过根组件一般定义的参数比较少而已,理解这个还是很重要的。
在组件new Vue()
后的执行vm._init()
初始化过程中,当执行到initState(vm)
时就会对内部使用到的一些状态,如props
、data
、computed
、watch
、methods
分别进行初始化,再对data
进行初始化的最后有这么一句:
function initData(vm) { //初始化data
...
observe(data) // info:{name:'cc',sex:'man'}
}
这个observe
就是将用户定义的data
变成响应式的数据,接下来看下它的创建过程
export function observe(value) {
if(!isObject(value)) { // 不是数组或对象,return
return
}
return new Observer(value)
}
简单理解这个observe
方法就是Observer
这个类的工厂方法,所以还是要看下Observer
这个类的定义
export class Observer {
constructor(value) {
this.value = value
this.walk(value) // 遍历value
}
walk(obj) {
const keys = Object.keys(obj)
for(let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]) // 只传入了两个参数
}
}
}
当执行new Observer
时,首先将传入的对象挂载到当前this
下,然后遍历当前对象的每一项,执行defineReactive
这个方法,看下它的定义:
export function defineReactive(obj, key, val) { const dep = new Dep() // 依赖管理器
val = obj[key] // 计算出对应key的值 observe(val) // 递归包装对象的嵌套属性
Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { ... 收集依赖 }, set(newVal) { ... 派发更新 } }) }
Object.defineProperty
创建响应式数据。首先根据传入的obj
和key
计算出val
具体的值;如果val
还是对象,那就使用observe
方法进行递归创建(如果不是就直接return了),在递归的过程中使用Object.defineProperty
将对象的每一个属性都变成响应式数据
data() {
return {
info: {
name: 'cc',
sex: 'man'
}
}
}
// 这段代码就会有三个响应式数据:
// info, info.name, info.sex
Object.defineProperty
内的get
方法,它的作用就是谁访问到当前key
的值就用defineReactive
内的dep
将它收集起来,也就是依赖收集的意思。set
方法的作用就是当前key
的值被赋值了,就通知dep
内收集到的依赖项,key
的值发生了变更,视图请变更吧 这个时候get
和set
只是定义了,并不会触发。什么是依赖我们接下来说明,首先还是用一张图帮大家理清响应式数据的创建过程
二、依赖收集
什么是依赖呢?我们看下mountComponent
的定义
function mountComponent(vm, el) {
...
const updateComponent = function() {
vm._update(vm._render())
}
new Watcher(vm, updateComponent, noop, { // 渲染watcher
...
}, true) // true为标志,表示是否是渲染watcher
...
}
我们首先说明下这个Watcher
类,它类似与之前的VNode
类,根据传入的参数不同,可以分别实例化出三种不同的Watcher
实例,它们分别是用户watcher
,计算watcher
以及渲染watcher
:
1、用户(user) watcher
:也就是用户自己定义的
2、计算(computed) watcher:是当定义计算属性实例化出来的一种
3、渲染(render) watcher:
只是用做视图渲染而定义的Watcher
实例,再组件执行vm.$mount
的最后会实例化Watcher
类,这个时候就是以渲染watcher
的格式定义的,收集的就是当前渲染watcher
的实例
只要一执行就会执行当前组件实例上的vm._update(vm._render())
将render
函数转为VNode
,这个时候如果render
函数内有使用到data
中已经转为了响应式的数据,就会触发get
方法进行依赖的收集,补全之前依赖收集的逻辑:
export function defineReactive(obj, key, val) {
const dep = new Dep() // 依赖管理器
val = obj[key] // 计算出对应key的值
observe(val) // 递归的转化对象的嵌套属性
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() { // 触发依赖收集
if(Dep.target) { // 之前赋值的当前watcher实例
dep.depend() // 收集起来,放入到上面的dep依赖管理器内
...
}
return val
},
set(newVal) {
... 派发更新
}
})
}
这时我们知道watcher
是个什么东西了,简单理解就是数据和组件之间一个通信工具的封装,当某个数据被组件读取时,就将依赖数据的组件使用Dep
这个类给收集起来。
当前例子data
内的属性是只有一个渲染watcher
的,因为没有被其他组件所使用。但如果该属性被其他组件使用到,也会将使用它的组件收集起来,例如作为了props
传递给了子组件,再dep
的数组内就会存在多个渲染watcher
三、派发更新
如果只是收集依赖,那其实是没任何意义的,将收集到的依赖在数据发生变化时通知到并引起视图变化,这样才有意义。如现在我们对数据重新赋值,这个时候就会触发创建响应式数据时的set
方法了,我们再补全那里的逻辑
export function defineReactive(obj, key, val) {
const dep = new Dep() // 依赖管理器
val = obj[key] // 计算出对应key的值
observe(val) // 递归转化对象的嵌套属性
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
... 依赖收集
},
set(newVal) { // 派发更新
if(newVal === val) { // 相同
return
}
val = newVal // 赋值
observer(newVal) // 如果新值是对象也递归包装
dep.notify() // 通知更新
}
})
}
当赋值触发set
时,首先会检测新值和旧值,不能相同;然后将新值赋值给旧值;如果新值是对象则将它变成响应式的;最后让对应属性的依赖管理器使用dep.notify
发出更新视图的通知。我们看下它的实现
let uid = 0
class Dep{
constructor() {
this.id = uid++
this.subs = []
}
notify() { // 通知
const subs = this.subs.slice()
for(let i = 0, i < subs.length; i++) {
subs[i].update() // 挨个触发watcher的update方法
}
}
}
这里做的事情只有一件,将收集起来的watcher
挨个遍历触发update
方法:
class Watcher{
...
update() {
queueWatcher(this)
}
}
const queue = []
let has = {}
function queueWatcher(watcher) {
const id = watcher.id
if(has[id] == null) { // 如果某个watcher没有被推入队列
...
has[id] = true // 已经推入
queue.push(watcher) // 推入到队列
}
...
nextTick(flushSchedulerQueue) // 下一个tick更新
}
update
方法时将当前watcher
实例传入到定义的queueWatcher
方法内,这个方法的作用是把将要执行更新的watcher
收集到一个队列queue
之内,保证如果同一个watcher
内触发了多次更新,只会更新一次对应的watcher
。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律