从 Vue2 源码探究响应式原理

413 阅读4分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。 这是源码共读的第23期,链接:为什么 Vue2 this 能够直接获取到 data 和 methods

断点调试要领:

  • 赋值语句可以一步按F10跳过,看返回值即可,后续详细再看。

  • 函数执行需要断点按F11跟着看,也可以结合注释和上下文倒推这个函数做了什么。

  • 有些不需要细看的,直接按F8走向下一个断点

  • 刷新重新调试按F5

详见这篇文章《前端容易忽略的 debugger 调试技巧》,截图标注的很详细。

1. 准备调试

编辑器: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 在浏览器打开调试

2. 数据侦听

  function Vue (options) {
    if (!(this instanceof Vue)
    ) {
      warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
  }

  initMixin(Vue);
  stateMixin(Vue);
  eventsMixin(Vue);
  lifecycleMixin(Vue);
  renderMixin(Vue);

2.1 initMixin

进入 _initinitMixin

  function initMixin (Vue) {
    Vue.prototype._init = function (options) {
      var vm = this;
      // a uid
      vm._uid = uid$3++;

      var startTag, endTag;
      /* istanbul ignore if */
      if (config.performance && mark) {
        startTag = "vue-perf-start:" + (vm._uid);
        endTag = "vue-perf-end:" + (vm._uid);
        mark(startTag);
      }

      // a flag to avoid this being observed
      vm._isVue = true;
      // merge options
      if (options && options._isComponent) {
        // optimize internal component instantiation
        // since dynamic options merging is pretty slow, and none of the
        // internal component options needs special treatment.
        initInternalComponent(vm, options);
      } else {
        vm.$options = mergeOptions(
          resolveConstructorOptions(vm.constructor),
          options || {},
          vm
        );
      }
      /* istanbul ignore else */
      {
        initProxy(vm);
      }
      // expose real self
      vm._self = vm;
      initLifecycle(vm); //
      initEvents(vm);
      initRender(vm);
      callHook(vm, 'beforeCreate');
      initInjections(vm); // resolve injections before data/props
      initState(vm);
      initProvide(vm); // resolve provide after data/props
      callHook(vm, 'created');

      /* istanbul ignore if */
      if (config.performance && mark) {
        vm._name = formatComponentName(vm, false);
        mark(endTag);
        measure(("vue " + (vm._name) + " init"), startTag, endTag);
      }

      if (vm.$options.el) {
        vm.$mount(vm.$options.el);
      }
    };
  }

可以看到在 _init 方法中:先调用了 mergeOptions() 方法合并实例构造函数和传入的options,感兴趣的童鞋可以自己再调试这部分源码。调用完后返回的 vm.$options 如下图:

image.png

接下来初始化生命周期,methods、data、watch、computed等。

下面详细看看 data 是如何定义的?

进入 initState,再进入 initData

image.png

这里 proxy工具方法代理,使用户通过 this.name 访问数据时,相当于 vm._data.name

image.png

2.2 Observer

往下进入 observe 方法

image.png

F11new Observer(value) 进入首字母大写的 Observe 构造函数,传入 data

image.png

  var Observer = function Observer (value) { // 传入data对象
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    def(value, '__ob__', this);
    if (Array.isArray(value)) { // 数组
      if (hasProto) {
        protoAugment(value, arrayMethods);
      } else {
        copyAugment(value, arrayMethods, arrayKeys);
      }
      this.observeArray(value);
    } else { // 对象
      this.walk(value);
    }
  };

2.3 walk

这里我们可以看到对数组和对象分别进行数据侦听。如果是对象,则进入 Observer 原型对象的 walk 方法

image.png

  Observer.prototype.walk = function walk (obj) {
    var keys = Object.keys(obj);
    for (var i = 0; i < keys.length; i++) {
      defineReactive$$1(obj, keys[i]);
    }
  };

2.4 defineReactive$$1

再进入 defineReactive$$1,在这里我们看到大家熟悉的 Object.defineProperty

  /**
   * Define a reactive property on an Object.
   */
  function defineReactive$$1 (
    obj,
    key,
    val,
    customSetter,
    shallow
  ) {
    var dep = new Dep();

    var property = Object.getOwnPropertyDescriptor(obj, key);
    if (property && property.configurable === false) {
      return
    }

    // cater for pre-defined getter/setters
    var getter = property && property.get;
    var setter = property && property.set;
    if ((!getter || setter) && arguments.length === 2) {
      val = obj[key];
    }

    // 递归子属性 observe 内部 new Observer()
    var childOb = !shallow && observe(val); 
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
          dep.depend();
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) {
              dependArray(value);
            }
          }
        }
        return value
      },
      set: function reactiveSetter (newVal) {
        var value = getter ? getter.call(obj) : val;
        /* eslint-disable no-self-compare */
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
        /* eslint-enable no-self-compare */
        if (customSetter) {
          customSetter();
        }
        // #7981: for accessor properties without setter
        if (getter && !setter) { return }
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        childOb = !shallow && observe(newVal);
        dep.notify();
      }
    });
  }

new Dep() 时,在当前 dep 实例上创建 subs 数组,存放依赖项

image.png

并为对象中每个属性创建 getset

2.5 observeArray

遍历到data中的arr数组 image.png

Observer 中调用 protoAugment 通过使用__proto__拦截原型链来增加目标对象或数组,即重写数组 pop/push/reverse/shift/sort/splice/unshift 方法,通过 ob.dep.notify() 派发依赖

image.png 如果是数组,则遍历数组中的每个属性,进行 observe(items[i]) 监听

  Observer.prototype.observeArray = function observeArray (items) {
    for (var i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
  };

遍历到第一个元素 {a:0}

image.png

进入 defineReactive$$1,使用 Object.defineProperty 侦听对象

2.5.1 思考

思考:为什么在data中定义的数组arr,使用 this.arr[0] = {a:1} 这种下标修改的方式不生效?

初始化页面: 在控制台打印 vm.arr,可以看到arr数组两个元素的 a 属性都是 Observer 响应式被侦听的,有 getset 方法。见下图:

image.png

接下来在控制台输入 vm.arr[0].a = 1,页面也响应式变化了,如下图:

image.png

但是如果我们直接用下标赋值的方式修改数据 vm.arr[0] = {a:1},页面不会响应式变化,且arr数组第一个元素丢失响应式

image.png

下面这段代码就能给出回答:因为 observeArray 方法中, observe(items[i]) 中监听的数组的每个属性值,对于数组本身是无法监听到的。通过 vm.arr[0] = {a:1} 表面上是修改了第一元素值,实际上 {} 相当于重新创建了一个新对象,和之前的对象内存地址不是同一个,他没有被observe侦听到,所以丢失响应式

  Observer.prototype.observeArray = function observeArray (items) {
    for (var i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
  };

这实际上也是 Object.defineProperty 的不足之处:

  • 每次只能劫持一个属性,对于嵌套数组和对象,只能递归遍历;
  • 对于对象的新增和修改属性无法监听的到,得用setset和delete手动增加响应式;
  • 对于数组也只能重写pop,push,reverse等方法。

3. 依赖收集

有三个问题: 依赖何时收集?依赖收集到哪里?依赖是谁? 下面一一解答。

Vue.prototype._init 方法中调用 vm.$mount(vm.$options.el);,返回 mountComponent 方法,在该方法中

    new Watcher(vm, updateComponent, noop, {
      before: function before () {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, 'beforeUpdate');
        }
      }
    }, true /* isRenderWatcher */);

3.1 Watcher

创建 Watcher

image.png

    this.value = this.lazy
      ? undefined
      : this.get();

Watcher 中调用 get 方法,求值getter,并重新收集依赖项

  Watcher.prototype.get = function get () {
    pushTarget(this);
    var value;
    var vm = this.vm; //Vue实例
    try {
      value = this.getter.call(vm, vm);
    } catch (e) {
      if (this.user) {
        handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      // “touch”每个属性,使它们都被跟踪为对深度观察的依赖
      if (this.deep) {
        traverse(value);
      }
      popTarget();
      this.cleanupDeps();
    }
    return value
  };

pushTarget(this)this 指向当前 Watcher 实例,Dep.target 全局唯一。这里的 Watcher实例 就是我们收集的依赖

image.png

Watcher.prototype.get 再往下走,这里很巧妙,使用 this.getter.call(vm, vm) 读取值,触发getter,从而将this即Watcher实例添加到dep中

image.png

3.2 defineReactive$$1

我们看 defineReactive$$1方法,代码详解看注释

  function defineReactive$$1 (
    obj,
    key,
    val,
    customSetter,
    shallow
  ) {
    
    var dep = new Dep(); // 用 subs 数组存放依赖

    var property = Object.getOwnPropertyDescriptor(obj, key);
    if (property && property.configurable === false) {
      return
    }

    // cater for pre-defined getter/setters
    var getter = property && property.get;
    var setter = property && property.set;
    if ((!getter || setter) && arguments.length === 2) {
      val = obj[key];
    }

    // 递归子属性 observe 内部new Observer()
    var childOb = !shallow && observe(val); 
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
          dep.depend(); // 依赖收集
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) {
              dependArray(value);
            }
          }
        }
        return value
      },
      set: function reactiveSetter (newVal) {
        var value = getter ? getter.call(obj) : val;
        /* eslint-disable no-self-compare */
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
        /* eslint-enable no-self-compare */
        if (customSetter) {
          customSetter();
        }
        // #7981: for accessor properties without setter
        if (getter && !setter) { return }
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        childOb = !shallow && observe(newVal);
        dep.notify(); // 派发依赖
      }
    });
  }

3.3 depend

dep.depend() 方法:

  Dep.prototype.depend = function depend () {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  };

进入 addDep 方法

image.png

触发 addSub 方法,这里最终将 Watcher 添加到 subs 队列。这里 Watcher 起到一个桥梁纽带作用,当数据变化时通知 Watcher ,再由 Watcher 统一通知其他依赖。

image.png

执行完 mounted 钩子函数,模板编译完成,dom 树挂载。 _init 执行完毕

所以上面三个问题可以回答了。

  • 依赖在 get 数据时收集,即使用数据时,无论是模板中还是方法中;
  • 依赖收集在 Dep 里面;
  • 依赖就是 Watcher 实例。

4. 派发更新

控制台输入 vm.name = 'world',回车,进入 set

image.png

F11 进入 dep.notify()

image.png

遍历subs,即上面收集的依赖队列,调用 update(),告知依赖Watcher 数据发生变化

image.png

后续进行新旧Vnode比较,重新渲染。

总结

本文通过学习源码调试方法,对响应式原理的相关代码进行逐行逐段调试,能够增强原理的理解和掌握,遇到问题更容易debug问题所在点,并融会贯通其他框架源码的调试方法。

针对 vue2Object.defineProperty 的缺陷,vue3Proxy 一招解决所有问题,放弃了对低版本浏览器的兼容,从而达到更优的效果。