【Vue源码】14.响应式原理-响应式对象

450 阅读3分钟

响应式原理-响应式对象

在前面的文章中,我们主要介绍了 Vue 的初始化过程,原始的数据是怎么映射到 DOM 并展示在页面上的,但没有涉及到数据变化到 DOM 变化的部分,从本文开始我们就来了解数据变更触发 DOM 变化的流程。

先从一个示例开始:

<div id="app" @click="changeMsg">{{ message }}</div>
var app = new Vue({
  el: "#app",
  data: {
    message: "Hello Vue!",
  },
  methods: {
    changeMsg() {
      this.message = "Hello World!";
    },
  },
});

示例中我们在 HTML 中定义了一个 idapp 的根 DOM,并且绑定了一个 click 事件,里面绑定了 message 变量。 在 JS 中实例化了一个 Vue,并在 data 中定义了 message,在 methods 中定义了 changeMsg 事件。 这些在我们开发中总是熟悉不过的流程,但是当我们去修改 this.message 的时候,模板对应的插值也会渲染成新的数据,这在 Vue 中是如何实现的呢?

Object.defineProperty

没错,首先介绍的是大家熟悉的响应式对象,在 Vue 2+ 中就是利用 Object.defineProperty 方法来实现响应式核心的。

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

Object.defineProperty(obj, prop, descriptor);

他接受三个入参:

  1. obj 是要定义的对象
  2. prop 是要定义或修改的属性的名称
  3. descriptor 是要定义或修改的属性描述符:
  • configurable:为 false 时不可修改和删除该属性
  • enumerable:是否可被枚举
  • value:该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)
  • writable:是否可写
  • get:当访问该属性时,会调用此函数
  • set:当属性值被修改时,会调用此函数

其中 descriptor 中的 gettersetter 比较重要,当我们给属性提供 getter 方法,访问该属性就会触发,在给属性设置值时 setter 同理。当对象拥有 gettersetter 后,我们可以把这个对象称为响应式对象。那么 Vue 把哪些对象变成了响应式对象呢,我们接着看。

initState

Vue 初始化执行 _init 方法时,会执行 initState 方法,它定义在 src/core/instance/state.js 中:

export function initState(vm: Component) {
  vm._watchers = [];
  const opts = vm.$options;
  if (opts.props) initProps(vm, opts.props);
  if (opts.methods) initMethods(vm, opts.methods);
  if (opts.data) {
    initData(vm);
  } else {
    observe((vm._data = {}), true /* asRootData */);
  }
  if (opts.computed) initComputed(vm, opts.computed);
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}

initState 方法主要是对 propsmethodsdatacomputedwatcher 等做初始化操作,在这里我们只关注 initPropsinitData

initProps

定义在 src/core/instance/state.js 中:

function initProps(vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {};
  const props = (vm._props = {});
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = (vm.$options._propKeys = []);
  const isRoot = !vm.$parent;
  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false);
  }
  for (const key in propsOptions) {
    keys.push(key);
    const value = validateProp(key, propsOptions, propsData, vm);
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== "production") {
      // ...
    } else {
      defineReactive(props, key, value);
    }
    // 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);
    }
  }
  toggleObserving(true);
}

initProps 的主要过程就是遍历我们定义的 props 配置,然后做了两件事:

  1. 调用 defineReactive 把每个 prop 对应的值变为响应式,可以通过 vm._props.xxx 访问到定义在 props 中的属性
  2. 通过 proxyvm._props.xxx 代理到 vm.xxx

initData

也是定义在 src/core/instance/state.js 中:

function initData(vm: Component) {
  let data = vm.$options.data;
  data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};
  if (!isPlainObject(data)) {
    data = {};
    // ...
  }
  // proxy data on instance
  const keys = Object.keys(data);
  const props = vm.$options.props;
  const methods = vm.$options.methods;
  let i = keys.length;
  while (i--) {
    const key = keys[i];
    if (process.env.NODE_ENV !== "production") {
      // ...
    }
    if (props && hasOwn(props, key)) {
      // ...
    } else if (!isReserved(key)) {
      // 代理属性到 vm 上
      proxy(vm, `_data`, key);
    }
  }
  // observe data
  // 调用 observe 为 data 对象上的数据设置响应式
  observe(data, true /* asRootData */);
}

initData 的主要过程还是遍历我们定义的 data 配置,然后做了两件事:

  1. 通过 proxy 把每一个值 vm.data.xxx 都代理到 vm.xxx
  2. 调用 observe 方法监测 整个 data 的变化,把 data 也变成响应式,可以通过 vm._data.xxx 访问到 data 中定义的属性

proxy

initPropsinitData 中都调用了 proxy 方法,他的作用是把 propsdata 上的属性都代理到 vm 实例上,这也就是我们明明在 props 中定义的属性,在 this.xx 就可以访问到:

let comP = {
  props: {
    msg: "hello",
  },
  methods: {
    say() {
      console.log(this.msg);
    },
  },
};
export function proxy(target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter() {
    return this[sourceKey][key];
  };
  sharedPropertyDefinition.set = function proxySetter(val) {
    this[sourceKey][key] = val;
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

proxy 方法内部逻辑很简单,就是通过 Object.definePropertytarget[sourceKey][key] 的读写变成了对 target[key] 的读写。 比如 props,对 vm._props.xxx 的读写变成了 vm.xxx 的读写,而 vm._props.xxx 又已经代理到 props 中的属性上了。所以我们就是相当于 vm.xxx = props.xxx。 对于 data 同理。

observe

observe 的功能就是用来监测数据的变化,它的定义在 src/core/observer/index.js 中:

/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
export function observe(value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return;
  }
  let ob: Observer | void;
  if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value);
  }
  if (asRootData && ob) {
    ob.vmCount++;
  }
  return ob;
}

observe 主要做了下面几件事:

  1. 判断不是对象或者是 VNode 类型,则什么也不做,直接返回
  2. 如果对象拥有 __ob__属性,则直接返回 value.__ob__(十秒之后会讲到这个)
  3. 如果对象未拥有 __ob__属性,则在满足一定条件时,实例化一个 Observer 实例并返回

Observer

Observer 是一个类,作用是给对象的属性添加 gettersetter,用于依赖收集和派发更新,定义在 src/core/observer/index.js 中:

/**
 * Observer class that is attached to each observed
 * object. Once attached, the observer converts the target
 * object's property keys into getter/setters that
 * collect dependencies and dispatch updates.
 */
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor(value: any) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    def(value, "__ob__", this);
    if (Array.isArray(value)) {
      const augment = hasProto ? protoAugment : copyAugment;
      augment(value, arrayMethods, arrayKeys);
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  }

  /**
   * Walk through each property and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk(obj: Object) {
    const keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i]);
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray(items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
  }
}

Observer 主要做了下面几件事:

  1. 首先实例化一个 Dep 实例
  2. 接着执行 def 方法,把自身实例添加到数据对象 value__ob__ 属性上,如果 value.__ob__ 存在的话说明是已经绑定过的,所以在 observe 中会有对 __ob__ 的判断
  3. 接下来对 value 做判断,如果是数组则执行 observeArray,否则执行 walk 方法

observeArray 是遍历执行 observe,而 walk 方法是遍历对象的 key 调用 defineReactive 方法。

defineReactive

defineReactive 的作用就是定义一个响应式对象,给对象动态增加 gettersetter,定义在 src/core/observer/index.js 中:

/**
 * Define a reactive property on an Object.
 */
export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep();

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

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

  let childOb = !shallow && observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const 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) {
      const 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 (process.env.NODE_ENV !== "production" && customSetter) {
        customSetter();
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      dep.notify();
    },
  });
}

defineReactive 先实例化一个 Dep 实例,接着拿 obj 的属性描述符,对子对象递归调用 observe 方法,这样就保证无论 obj 层次多复杂,所有的子属性也都能变成响应式。当我们访问或修改 obj 对象中一个属性时,就能触发 gettersetter。 关于 gettersetter 具体实现,我们后文在讲。

总结

14.响应式原理-响应式对象.png

这里主要介绍了响应式对象,核心是利用 Object.defineProperty 给数据添加 gettersetter,目的是让我们在写入或读取数据时自动执行逻辑。 getter 负责依赖收集,setter 负责派发更新。

源码分析 GitHub 地址

参考:Vue.js 技术揭秘