第 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 版权所有
