第 44 期 - Vue2 响应式原理源码调试探究
logoFRONTALK AI/12月6日 16:31/阅读原文

摘要

本文介绍了参与源码共读活动调试 Vue2 源码探究响应式原理的过程,包括数据侦听、依赖收集、派发更新等内容,还提及了 Object.defineProperty 的缺陷与 Vue3 的改进。

一、调试准备

在这部分内容中,作者首先介绍了本文是参加由公众号@若川视野发起的每周源码共读活动。

  1. 调试要领
    • 赋值语句按 F10 跳过看返回值;函数执行按 F11 跟着看,也可结合注释和上下文倒推;不需要细看的按 F8 走向下一个断点;刷新重新调试按 F5。还推荐了《前端容易忽略的 debugger 调试技巧》这篇文章。
  2. 具体准备
    • 编辑器:使用 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>

二、数据侦听

  1. Vue 函数
    • 首先定义了Vue函数,在Vue函数中如果this不是Vue的实例会给出警告,然后调用_init方法。
    • 接着通过initMixinstateMixineventsMixinlifecycleMixinrenderMixinVue进行扩展。
  2. initMixin
    • initMixin中的_init方法里,先调用mergeOptions方法合并实例构造函数和传入的options,然后进行一系列初始化操作,如初始化生命周期、methodsdatawatchcomputed等。这里还提到如果options是内部组件则进行优化实例化,否则合并选项。
  3. Observer
    • 进入initState,再进入initData,有proxy工具方法代理,使用户通过this.name访问数据时相当于vm._data.name
    • 然后进入observe方法,Observer构造函数中,如果传入的值是数组,会根据是否有proto进行不同的操作(protoAugmentcopyAugment)然后调用observeArray方法;如果是对象则调用walk方法。
  4. walk
    • Observer原型对象的walk方法中,通过Object.keys获取对象的所有键,然后对每个键调用defineReactive$$1方法。
  5. defineReactive$$1
    • defineReactive$$1方法中,创建Dep实例,处理Object.getOwnPropertyDescriptor获取的属性描述符。如果没有getter或者setter且参数长度为 2 时获取属性值。
    • 递归子属性进行observe操作。通过Object.defineProperty为对象属性创建getset方法,在get方法中进行依赖收集(dep.depend),在set方法中进行派发依赖(dep.notify)。
  6. observeArray
    • ObserverobserveArray方法中,遍历数组中的每个元素调用observe方法进行监听。这里还提到了在data中定义的数组arr,使用this.arr[0] = {a:1}这种下标修改的方式不生效的原因,是因为observeArray方法监听的是数组元素的值,而不是数组本身,这种方式相当于重新创建了一个新对象,没有被observe侦听到,所以丢失响应式。并且指出这是Object.defineProperty的不足之处,如每次只能劫持一个属性,对于嵌套数组和对象只能递归遍历,对于对象新增和修改属性无法监听等。

三、依赖收集

  1. 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中进行依赖收集,最后执行popTargetthis.cleanupDeps
  2. defineReactive$$1 中的依赖收集
    • defineReactive$$1方法中的get函数里,如果Dep.target存在,会调用dep.depend进行依赖收集,这里会进一步调用Dep.prototype.depend方法,该方法中如果Dep.target存在会调用Dep.target.addDep(this),最终将Watcher添加到subs队列。
  3. 依赖相关问题解答
    • 依赖在获取数据时收集(无论是模板中还是方法中使用数据时);依赖收集在Dep里面;依赖就是Watcher实例。

四、派发更新

五、总结

 

扩展阅读

Made by 捣鼓键盘的小麦 / © 2025 Front Talk 版权所有