现在是时候深入一下了!Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。在这个章节,我们将研究一下 Vue 响应式系统的底层的细节。

Vue 响应式原理(简单概述)

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setterObject.defineProperty ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

Object.defineProperty

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

  1. Object.defineProperty(obj, prop, descriptor);

obj 是要在其上定义属性的对象。prop 是要定义或修改的属性的名称。descriptor 是将被定义或修改的属性描述符。

比较核心的是 descriptor,它有很多可选键值,具体的可以去参阅它的文档。这里我们最关心的是 get setget 是一个给属性提供的 getter 方法,当我们访问了该属性的时候会触发 getter 方法;set 是一个给属性提供的 setter 方法,当我们对该属性做修改的时候会触发 setter 方法。

一旦对象拥有了 getter setter,我们可以简单地把这个对象称为响应式对象。那么 Vue 把哪些对象变成了响应式对象了呢,接下来我们从源码层面分析。

initState

Vue 首次渲染过程中,也就是 new Vue 的时候,会调用 Vue 构造函数中 _init 方法中的 initState(vm),我们在回顾一下 initState 的代码。

  1. export function initState(vm: Component) {
  2. vm._watchers = [];
  3. const opts = vm.$options;
  4. if (opts.props) initProps(vm, opts.props);
  5. if (opts.methods) initMethods(vm, opts.methods);
  6. if (opts.data) {
  7. initData(vm);
  8. } else {
  9. observe((vm._data = {}), true /* asRootData */);
  10. }
  11. if (opts.computed) initComputed(vm, opts.computed);
  12. if (opts.watch && opts.watch !== nativeWatch) {
  13. initWatch(vm, opts.watch);
  14. }
  15. }

initState 方法主要是对 props、methods、data、computedwathcer 等属性做了初始化操作。这里我们重点分析 props data,对于其它属性的初始化我们之后再详细分析。

initProps

这边主要列举核心部分的代码,其他就不一 一列举了。

  1. function initProps(vm: Component, propsOptions: Object) {
  2. const propsData = vm.$options.propsData || {};
  3. const props = (vm._props = {});
  4. for (const key in propsOptions) {
  5. const value = validateProp(key, propsOptions, propsData, vm);
  6. defineReactive(props, key, value);
  7. if (!(key in vm)) {
  8. proxy(vm, `_props`, key);
  9. }
  10. }
  11. }

props 初始化过程主要就是遍历用户传进来的 props,校验 propsvalue 类型是否匹配(如果不匹配则触发警告),然后通过 defineReactiveprops 的每个值都设置成响应式对象。

另外就是判断当前 props key 是否已经在 vm 实例上,如果不在则通过 proxy 代理 _props 的成员到 vm 实例上,原本需要通过 vm._props.xxx 访问的成员,现在可以通过 vm.xxx 进行访问。

initData

  1. function initData(vm: Component) {
  2. let data = vm.$options.data;
  3. data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {};
  4. // proxy data on instance
  5. const keys = Object.keys(data);
  6. const props = vm.$options.props;
  7. const methods = vm.$options.methods;
  8. let i = keys.length;
  9. while (i--) {
  10. const key = keys[i];
  11. if (process.env.NODE_ENV !== 'production') {
  12. if (methods && hasOwn(methods, key)) {
  13. warn(`Method "${key}" has already been defined as a data property.`, vm);
  14. }
  15. }
  16. if (props && hasOwn(props, key)) {
  17. process.env.NODE_ENV !== 'production' && warn(`The data property "${key}" is already declared as a prop. ` + `Use prop default value instead.`, vm);
  18. } else if (!isReserved(key)) {
  19. proxy(vm, `_data`, key);
  20. }
  21. }
  22. // observe data
  23. observe(data, true /* asRootData */);
  24. }

props 初始化过程主要就是遍历用户传进来的选项 data,通过 proxy 代理 _data 的成员到 vm 实例上,原本需要通过 vm._data.xxx 访问的成员,现在可以通过 vm.xxx 进行访问。

因为三者 props 、methods、data 都会把成员代理到 vm 上面,所以必然会有名称冲突,所以 Vue 还进行了一些判断,在 while 循环中还对 method_s 和 _props 进行了判断,如果他们其中也定义了 data 中的某个 key 属性,则触发警告,不能重复定义同名变量。最后通过 observe 设置 data 为响应式对象,并且监听 data 对象的变化。

proxy

  1. const sharedPropertyDefinition = {
  2. enumerable: true,
  3. configurable: true,
  4. get: noop,
  5. set: noop,
  6. };
  7. export function proxy(target: Object, sourceKey: string, key: string) {
  8. sharedPropertyDefinition.get = function proxyGetter() {
  9. return this[sourceKey][key];
  10. };
  11. sharedPropertyDefinition.set = function proxySetter(val) {
  12. this[sourceKey][key] = val;
  13. };
  14. Object.defineProperty(target, key, sharedPropertyDefinition);
  15. }

proxy 其实就是对 Object.defineProperty 的封装。原本需要对 target[sourceKey][key] 的读写变成了对 target[key] 的读写。

observe

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

  1. /**
  2. * Attempt to create an observer instance for a value,
  3. * returns the new observer if successfully observed,
  4. * or the existing observer if the value already has one.
  5. */
  6. export function observe(value: any, asRootData: ?boolean): Observer | void {
  7. if (!isObject(value) || value instanceof VNode) {
  8. return;
  9. }
  10. let ob: Observer | void;
  11. if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
  12. ob = value.__ob__;
  13. } else if (shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue) {
  14. ob = new Observer(value);
  15. }
  16. return ob;
  17. }

主要展示了一些关键部分的代码,observe 方法的作用就是给非 VNode 的对象类型数据添加一个 Observer,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个 Observer 对象实例。接下来我们来看一下 Observer 的作用。

Observer

Observer 是一个类,它的作用是给对象的属性添加 gettersetter,用于依赖收集和派发更新。

  1. /**
  2. * Observer class that is attached to each observed
  3. * object. Once attached, the observer converts the target
  4. * object's property keys into getter/setters that
  5. * collect dependencies and dispatch updates.
  6. */
  7. export class Observer {
  8. value: any;
  9. dep: Dep;
  10. vmCount: number; // number of vms that have this object as root $data
  11. constructor(value: any) {
  12. this.value = value;
  13. this.dep = new Dep();
  14. this.vmCount = 0;
  15. def(value, '__ob__', this);
  16. if (Array.isArray(value)) {
  17. if (hasProto) {
  18. protoAugment(value, arrayMethods);
  19. } else {
  20. copyAugment(value, arrayMethods, arrayKeys);
  21. }
  22. this.observeArray(value);
  23. } else {
  24. this.walk(value);
  25. }
  26. }
  27. walk(obj: Object) {}
  28. observeArray(items: Array<any>) {}
  29. }

Observer 的构造函数逻辑很简单,首先实例化 Dep 对象,这块稍后会介绍,接着通过执行 def 函数把自身实例添加到数据对象 value ob 属性上,def 函数是一个 Object.defineProperty 的封装。接下来会对 value 做判断,对于数组会调用 observeArray 方法,否则对纯对象调用 walk 方法。

  1. /**
  2. * Observe a list of Array items.
  3. */
  4. observeArray(items: Array<any>) {
  5. for (let i = 0, l = items.length; i < l; i++) {
  6. observe(items[i]);
  7. }
  8. }

observeArray 是遍历数组再次调用 observe 方法。

  1. /**
  2. * Walk through all properties and convert them into
  3. * getter/setters. This method should only be called when
  4. * value type is Object.
  5. */
  6. walk(obj: Object) {
  7. const keys = Object.keys(obj);
  8. for (let i = 0; i < keys.length; i++) {
  9. defineReactive(obj, keys[i]);
  10. }
  11. }

walk 方法是遍历对象的 key 调用 defineReactive 方法。接下来主要分析一下 defineReactive 实现了什么功能。

defineReactive

defineReactive 的功能就是定义一个响应式对象,给对象动态添加 gettersetter,它的定义在src/core/observer/index.js 中。

  1. /**
  2. * Define a reactive property on an Object.
  3. */
  4. export function defineReactive(obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean) {
  5. const dep = new Dep();
  6. const property = Object.getOwnPropertyDescriptor(obj, key);
  7. if (property && property.configurable === false) {
  8. return;
  9. }
  10. // cater for pre-defined getter/setters
  11. const getter = property && property.get;
  12. const setter = property && property.set;
  13. if ((!getter || setter) && arguments.length === 2) {
  14. val = obj[key];
  15. }
  16. let childOb = !shallow && observe(val);
  17. Object.defineProperty(obj, key, {
  18. enumerable: true,
  19. configurable: true,
  20. get: function reactiveGetter() {},
  21. set: function reactiveSetter(newVal) {},
  22. });
  23. }

defineReactive 函数最开始初始化 Dep 对象的实例,接着拿到 obj 的属性描述符,然后对子对象递归调用 observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj 中一个嵌套较深的属性,也能触发 getter setter

最后利用 Object.defineProperty 去给 obj 的属性 key 添加 getter setter。而关于 getter setter 的具体实现,我们会在之后介绍。

这一节我们介绍了响应式对象,核心就是利用 Object.defineProperty 给数据添加了 gettersetter,目的就是为了在我们访问数据以及写数据的时候能自动执行一些逻辑:getter 做的事情是依赖收集,setter 做的事情是派发更新,那么在接下来的章节我们会重点对这两个过程分析。