深入Vue的响应式原理

222 阅读2分钟

Vue最具特色的一项特征大概就是其响应式原理,看过Vue官方文档的朋友应该还是有了解的,如果你觉得看了文档理解起来还是很吃力,那么我们就从后往前的学习其响应式原理。

在Vue api文档中, 实例方法和数据部分,我们可以看到有三种实例方法

  • $watch
  • $set
  • $delete

我们只看set方法

-   **参数**    -   `{Object | Array} target`
    -   `{string | number} propertyName/index`
    -   `{any} value`

-   **返回值**:设置的值。

-   **用法**    这是全局 `Vue.set` 的**别名**。

-   **参考**:[Vue.set](https://cn.vuejs.org/v2/api/#Vue-set)

这个set方法其实就是Vue.set的别名,那么,我们再来看看Vue.set

### [Vue.set( target, propertyName/index, value )](https://cn.vuejs.org/v2/api/#Vue-set "Vue.set( target, propertyName/index, value )")

-   **参数**    -   `{Object | Array} target`
    -   `{string | number} propertyName/index`
    -   `{any} value`

-   **返回值**:设置的值。

-   **用法**    向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新 property,因为 Vue 无法探测普通的新增 property (比如 `this.myObject.newProperty = 'hi'`)

    注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象。

不知道你有没有遇到过这种需求,就是动态的向Vue某个组件的实例上增加property.并且需要该property必须是响应式的。

如果你没有碰到过这样的需求,也很正常,毕竟单纯的业务开发的时候,我们可能并用不到这种用法,但是有一种场景,在使用ElementUI进行开发的时候,如果你在设计动态菜单的时候,会用到这种方法

 methods: {
      handleCollapseToggle(value) {
        if (value) {
          this.initPopper();
        } else {
          this.doDestroy();
        }
      },
      addItem(item) {
        this.$set(this.items, item.index, item);
      },
      removeItem(item) {
        delete this.items[item.index];
      },
      addSubmenu(item) {
        this.$set(this.submenus, item.index, item);
      },
      removeSubmenu(item) {
        delete this.submenus[item.index];
      },
      ...
}

到现在,你应该知道如何在vue组件实例中动态增加响应式的属性。那我们从这里开始追踪,到底是如何实现的响应式。

首先在stateMixin方法里,将observer中的set方法赋值给了Vue.prototype.$set. 所以先来看看observer中的set方法

/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

抛开对target的判断,主要部分是

defineReactive(ob.value, key, val)
ob.dep.notify()

我们先来看看defineReactive的功能和实现


/**
 * 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()
      }
      // #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()
    }
  })
}

defineReactive是用来在对象上定义响应式属性的。看看是如何定义的,首先将valobserve做个转换

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
}

为什么要转换为Observer类型呢,这其实是重点之一,Observer类的作用就是将val转换为Observer类型之后,便于附加到target类型之上,通过gettersetters方法来收集依赖和分发更新。

具体实现如下:

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have 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)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  /**
   * Walk through all properties 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])
    }
  }
}

在该类的构造器中,调用walk方法以便将所有的属性都gettersetter化,而这个过程正是用defineReactive来处理的。继续来看defineReactive的内容,看看他是如何gettersetter化的

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()
      }
      // #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()
    }
  })

val对象上,定义了key的便利值命名的属性,该属性值可被枚举,可被修改,并且定义了属性的 getter 函数setter函数,首先看看getter函数,属性的getter函数会在该属性被访问的时候进行调用。

getter方法中,进行depend

depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
}

这个Dep.target是一个Watch实例,全局唯一,

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []

depend其实所做的就是,把新建的dep实例放到全局的监视器Dep.target里,其实就是把dep的id追加到watchdepids数组里。当然如果之前全局的Watch里没有监控该 val那么,还会把该watch放到该depsubs监听订阅列表里。详情如下:

 /**
   * Add a dependency to this directive.
   */
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

Dep的实现如下,其中id 标识该depsubs数组存储了所有监听该属性变化的Watch实例


/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

defineReactive中属性的setter方法就是为了在val发生变化时,设置新的值,然后通过depnotify方法找到对应的Watch实例进行update操作。执行执行的逻辑是Watch实例的run方法


  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          const info = `callback for watcher "${this.expression}"`
          invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

最终调用vm上的cb函数,这个cb就是watch属性中的关乎该key属性的handler.