我理解的Vue2源码shouldObserve作用

61 阅读2分钟

在 Vue2 的响应式系统里,shouldObserve 是一个控制开关,其作用是决定是否要对对象进行响应式转换。当 shouldObserve 为 true 时,调用 observe 或者 defineReactive 函数会把对象的属性转换为 getter/setter,从而使对象变为响应式的;当 shouldObserve 为 false 时,就不会进行响应式转换。

何时设为 false 以及原因

初始化注入、初始化非根实例的 props和 更新子组件的 props

1. initInjections 函数

function initInjections (vm) {
  var result = resolveInject(vm.$options.inject, vm);
  if (result) {
    toggleObserving(false);
    Object.keys(result).forEach(function (key) {
      /* istanbul ignore else */
      {
        defineReactive(vm, key, result[key], function () {
          warn(
            "Avoid mutating an injected value directly since the changes will be " +
            "overwritten whenever the provided component re-renders. " +
            "injection being mutated: "" + key + """,
            vm
          );
        });
      }
    });
    toggleObserving(true);
  }
}
  • 设置为 false 的时机:在初始化注入(injections)时,也就是调用 defineReactive 为注入的属性添加响应式时。
  • 原因:注入(injections)通常是从父组件传递下来的,这些值会在父组件重新渲染时被覆盖。所以不希望这些注入的值被响应式转换,防止不必要的依赖收集和更新操作,进而提升性能。
注入(Injections)

一种组件间通信的方式,它允许父组件向其所有子孙组件传递数据,而不需要在中间的每个组件都手动传递。下面通过一个简单的代码示例来说明注入的用法和应用场景,以及为什么注入的值会在父组件重新渲染时被覆盖。

  • 应用场景

    • 全局配置:当应用中有一些全局的配置信息,比如主题颜色、语言设置等,需要在多个组件中使用时,可以通过注入来传递这些信息。这样,无论组件嵌套有多深,都能方便地获取到这些全局配置。
    • 共享数据:多个子孙组件需要共享一些数据,而这些数据的传递如果通过 props 在多层组件之间传递会很繁琐,使用注入可以简化这个过程,让子孙组件直接从父组件获取共享数据。
为什么会在父组件重新渲染时被覆盖

因为注入的数据本质上是父组件提供的,当父组件重新渲染时,provide函数会再次被调用,生成新的注入数据。如果父组件的provide中返回的对象发生了变化,那么子组件中注入的值也会相应地更新。

  • provide函数的调用时机
    • 组件创建时:在父组件实例化过程中,当执行到provide选项时,会调用provide函数来提供数据。例如,在一个根组件中定义了provide选项,那么在根组件创建时,provide函数就会被调用,为其所有后代组件提供数据4。
    • 父组件重新渲染时:当父组件由于数据更新、状态变化等原因触发重新渲染时,provide函数会再次被调用。这是因为父组件可能会根据新的状态或数据来提供不同的值给后代组件。例如,父组件中有一个响应式数据count,在provide函数中返回{ count: this.count },当count的值发生变化导致父组件重新渲染时,provide函数会再次执行,将新的count值提供给后代组件4。
    • 组件动态切换时:如果父组件是一个动态组件,根据不同的条件切换显示不同的子组件,那么在每次切换父组件时,provide函数也会被调用。这是为了确保新显示的子组件能够获取到正确的provide数据。

2. updateChildComponent 函数

if (propsData && vm.$options.props) {
  toggleObserving(false);
  var props = vm._props;
  var propKeys = vm.$options._propKeys || [];
  for (var i = 0; i < propKeys.length; i++) {
    var key = propKeys[i];
    var propOptions = vm.$options.props; // wtf flow?
    props[key] = validateProp(key, propOptions, propsData, vm);
  }
  toggleObserving(true);
  // keep a copy of raw propsData
  vm.$options.propsData = propsData;
}
  • 设置为 false 的时机:在更新子组件的 props 时。
  • 原因:在更新 props 时,需要重新验证和设置 props 的值。因为 props 是单向数据流,父组件的更新会覆盖子组件的 props 值。所以在这个过程中暂时关闭响应式转换,避免不必要的依赖收集和更新操作,提高性能。
    • “单向数据流” 意味着数据只能从父组件流向子组件,子组件不能直接修改从父组件接收到的props数据。如果尝试在子组件中直接修改 props,则会触发警告。子组件中对数据的任何操作都不会影响父组件的数据。

    • 当传递对象或数组等复杂数据类型时,虽然不能直接修改props,但可以修改对象或数组内部的属性。这可能会导致一些意外的行为,因此在传递复杂数据类型时要特别小心,必要时可以使用深拷贝来确保数据的独立性。

    • “单向数据流”设计原因

      1. 数据一致性和可维护性:单向数据流确保了数据的流向清晰,所有数据的源头都在父组件。如果子组件可以随意修改props,那么数据的变化会变得难以追踪和调试,增加了维护的难度。通过单向数据流,数据的变化路径更加明确,出现问题时更容易定位。
      2. 避免数据冲突:在大型应用中,可能有多个子组件依赖同一个props。如果子组件可以随意修改props,那么不同子组件之间可能会产生数据冲突,导致应用出现不可预料的行为。单向数据流避免了这种情况的发生,保证了数据的一致性。
      3. 组件的独立性和复用性:子组件只负责接收和展示数据,不负责修改数据,这样子组件可以更加独立,只关注自身的业务逻辑和展示效果。同时,这样的设计也使得子组件更容易被复用,因为它不依赖于外部数据的修改方式。

3. initProps 函数

var isRoot = !vm.$parent;
// root instance props should be converted
if (!isRoot) {
  toggleObserving(false);
}
var loop = function ( key ) {
  keys.push(key);
  var value = validateProp(key, propsOptions, propsData, vm);
  /* istanbul ignore else */
  {
    var hyphenatedKey = hyphenate(key);
    if (isReservedAttribute(hyphenatedKey) ||
        config.isReservedAttr(hyphenatedKey)) {
      warn(
        (""" + hyphenatedKey + "" is a reserved attribute and cannot be used as component prop."),
        vm
      );
    }
    defineReactive(props, key, value, function () {
      if (!isRoot && !isUpdatingChildComponent) {
        warn(
          "Avoid mutating a prop directly since the value will be " +
          "overwritten whenever the parent component re-renders. " +
          "Instead, use a data or computed property based on the prop's " +
          "value. Prop being mutated: "" + key + """,
          vm
        );
      }
    });
  }
  // static props are already proxied on the component's prototype
  // during Vue.extend(). We only need to proxy props defined at
  // instantiation here.
  if (!(key in vm)) {
    proxy(vm, "_props", key);
  }
};

for (var key in propsOptions) loop( key );
toggleObserving(true);
  • 设置为 false 的时机:在初始化非根实例的 props 时。
  • 原因:非根实例的 props 是由父组件传递的,同样是单向数据流,父组件的更新会覆盖子组件的 props 值。所以在初始化 props 时暂时关闭响应式转换,避免不必要的依赖收集和更新操作,提高性能。
根实例 props
  • 根实例是整个 Vue 应用的入口组件,它可以接收外部传递进来的 props
  • 不过,与子组件不同的是,根实例通常不会有父组件来重新传递 props,所以在根实例的 props 初始化过程中,与子组件有所区别
  • 非根实例的props在子组件初始化和更新时不要观察,是因为此时存在父组件,由父组件进行观察就好了。而根实例props因为不存在父组件,所以就需要观察。根实例的props往往是整个组件树中数据传递的起点。如果根实例的props不被观察,那么当这些props变化时,子组件无法接收到最新的数据,这会导致组件树中的数据不一致。

总结

把 shouldObserve 设置为 false 主要是为了避免在某些场景下进行不必要的响应式转换和依赖收集,以此来提升性能。这些场景通常涉及单向数据流(如 props 和 injections),因为这些值会被父组件的更新覆盖,所以不需要对子组件内的这些值进行响应式处理。