第 44 期 - Vue2 响应式原理源码调试探究
摘要
本文介绍了参与源码共读活动调试 Vue2 源码探究响应式原理的过程,包括数据侦听、依赖收集、派发更新等内容,还提及了 Object.defineProperty 的缺陷与 Vue3 的改进。
一、调试准备
在这部分内容中,作者首先介绍了本文是参加由公众号@若川视野发起的每周源码共读活动。
- 调试要领
- 赋值语句按 F10 跳过看返回值;函数执行按 F11 跟着看,也可结合注释和上下文倒推;不需要细看的按 F8 走向下一个断点;刷新重新调试按 F5。还推荐了《前端容易忽略的 debugger 调试技巧》这篇文章。
- 具体准备
- 编辑器:使用 VSCode。
- vue2 源码地址:可以是 unpkg.com/vue@2.6.14/…,作者这里是直接下载到本地以便调试。
- 准备文件 index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device - width, initial - scale = 1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<div>name:{{name}}</div>
<div>arr:{{JSON.stringify(arr)}}</div>
<div>obj:{{JSON.stringify(obj)}}</div>
</div>
<script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
<!-- <script src="./vue@2.6.14.js"></script> -->
<script>
const vm = new Vue({
el: '# app',
data: {
name: 'Hello',
obj: { a: 1 },
arr: [{ a: 0 }, { a: 2 }]
},
methods: {
sayName() {
console.log(this.name);
},
changeName() {
this.name = 'world'
console.log(this.name);
},
},
});
console.log(vm.name);
</script>
</body>
</html>
- 在 VSCode 中下载 Live Server 插件,右击 index.html 文件,选 Open with Live Server 在浏览器打开调试。
二、数据侦听
- Vue 函数
- 首先定义了
Vue
函数,在Vue
函数中如果this
不是Vue
的实例会给出警告,然后调用_init
方法。 - 接着通过
initMixin
、stateMixin
、eventsMixin
、lifecycleMixin
、renderMixin
对Vue
进行扩展。
- 首先定义了
- initMixin
- 在
initMixin
中的_init
方法里,先调用mergeOptions
方法合并实例构造函数和传入的options
,然后进行一系列初始化操作,如初始化生命周期、methods
、data
、watch
、computed
等。这里还提到如果options
是内部组件则进行优化实例化,否则合并选项。
- 在
- Observer
- 进入
initState
,再进入initData
,有proxy
工具方法代理,使用户通过this.name
访问数据时相当于vm._data.name
。 - 然后进入
observe
方法,Observer
构造函数中,如果传入的值是数组,会根据是否有proto
进行不同的操作(protoAugment
或copyAugment
)然后调用observeArray
方法;如果是对象则调用walk
方法。
- 进入
- walk
- 在
Observer
原型对象的walk
方法中,通过Object.keys
获取对象的所有键,然后对每个键调用defineReactive$$1
方法。
- 在
- defineReactive$$1
- 在
defineReactive$$1
方法中,创建Dep
实例,处理Object.getOwnPropertyDescriptor
获取的属性描述符。如果没有getter
或者setter
且参数长度为 2 时获取属性值。 - 递归子属性进行
observe
操作。通过Object.defineProperty
为对象属性创建get
和set
方法,在get
方法中进行依赖收集(dep.depend
),在set
方法中进行派发依赖(dep.notify
)。
- 在
- observeArray
- 在
Observer
的observeArray
方法中,遍历数组中的每个元素调用observe
方法进行监听。这里还提到了在data
中定义的数组arr
,使用this.arr[0] = {a:1}
这种下标修改的方式不生效的原因,是因为observeArray
方法监听的是数组元素的值,而不是数组本身,这种方式相当于重新创建了一个新对象,没有被observe
侦听到,所以丢失响应式。并且指出这是Object.defineProperty
的不足之处,如每次只能劫持一个属性,对于嵌套数组和对象只能递归遍历,对于对象新增和修改属性无法监听等。
- 在
三、依赖收集
- Watcher
- 在
Vue.prototype._init
方法中调用vm.$mount(vm.$options.el)
返回mountComponent
方法,在该方法中创建Watcher
实例。 Watcher
实例的get
方法中,先调用pushTarget(this)
(this
指向当前Watcher
实例,Dep.target
全局唯一),然后求值getter
,在求值过程中触发get
方法(例如在defineReactive$$1
中的get
方法)从而将Watcher
实例添加到dep
中进行依赖收集,最后执行popTarget
和this.cleanupDeps
。
- 在
- defineReactive$$1 中的依赖收集
- 在
defineReactive$$1
方法中的get
函数里,如果Dep.target
存在,会调用dep.depend
进行依赖收集,这里会进一步调用Dep.prototype.depend
方法,该方法中如果Dep.target
存在会调用Dep.target.addDep(this)
,最终将Watcher
添加到subs
队列。
- 在
- 依赖相关问题解答
- 依赖在获取数据时收集(无论是模板中还是方法中使用数据时);依赖收集在
Dep
里面;依赖就是Watcher
实例。
- 依赖在获取数据时收集(无论是模板中还是方法中使用数据时);依赖收集在
四、派发更新
- 当在控制台输入
vm.name = 'world'
时,会进入set
方法,然后 F11 进入dep.notify
方法,该方法会遍历subs
(即上面收集的依赖队列),调用update
方法告知依赖Watcher
数据发生变化,后续进行新旧Vnode
比较,重新渲染。
五、总结
- 通过学习源码调试方法对
Vue2
响应式原理的相关代码进行调试,能增强对原理的理解和掌握,也更容易debug
问题所在点,还能融会贯通其他框架源码的调试方法。同时提到Vue3
用Proxy
解决了Vue2
中Object.defineProperty
的缺陷,但放弃了对低版本浏览器的兼容。
扩展阅读
Made by 捣鼓键盘的小麦 / © 2025 Front Talk 版权所有