xingbofeng.github.io icon indicating copy to clipboard operation
xingbofeng.github.io copied to clipboard

Vue源码学习笔记之observer与变异方法

Open xingbofeng opened this issue 6 years ago • 2 comments

这一篇主要是相对src/core/observer/这一块进行剖析。主要作用为MVVM框架在数据层面的观察,之后通知DOM刷新的部分。

文档里面对于这一部分的原理是这么说的:

受现代JavaScript的限制(以及废弃 Object.observe),Vue 不能检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化过程,所以属性必须在 data 对象上存在才能让 Vue 转换它,这样才能让它是响应的。

所以observer需要做的事情是:

遍历一个对象/数组

是否应该遍历它?

虽然我们希望数据都是响应式的。然而,在某些情况下(v-for循环中或props传递的数据),我们并不希望人为改变其中的数据(通常Vue会给出一个警告)。因而需要在遍历一个对象之前,设定这个状态是否应该被响应式触发:

/**
 * By default, when a reactive property is set, the new value is
 * also converted to become reactive. However when passing down props,
 * we don't want to force conversion because the value may be a nested value
 * under a frozen data structure. Converting it would defeat the optimization.
 */
export const observerState = {
  shouldConvert: true,
  isSettingProps: false
}

如何遍历它?

遍历对象/数组通常采用的是递归遍历,这一点众所周知。在Observer里有两个方法:walkobserveArray。他们分别用于遍历对象和数组。

walk (obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i], obj[keys[i]])
  }
}
observeArray (items: Array<any>) {
  for (let i = 0, l = items.length; i < l; i++) {
    observe(items[i])
  }
}

为何我们要遍历它?

文档里面对这一部分解释得很清楚:

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

变异方法

什么是变异方法

之前初学Vue的时候踩过一个坑,相信这个坑,所有初学Vue读者也都经历过:更改数组的某一项,并不会触发视图更新?这是为什么?

文档上边对此解释为变异方法

其实Vue所做的事情是改写数组的pushpop等方法,让他们在执行之后通知Vue,因而如果不使用变异方法进行数组更新,这样的改变是不会被Vue所监听得到的!在使用数组的变异方法时,除了触发本方法,还会触发一个回调:通知Vue,我已更新!

src/core/observer/index.js最开始引入数组的变异方法:

import { arrayMethods } from './array'
变异方法的改写

下面来分析变异方法的改写:

/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */

import { def } from '../util/index'

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

/**
 * Intercept mutating methods and emit events
 */
;[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  // 在这里拿到数组的原型
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator () {
    // 之后对传入的参数放入args数组内
    let i = arguments.length
    const args = new Array(i)
    while (i--) {
      args[i] = arguments[i]
    }
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
        inserted = args
        break
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

我们这里要做到的是:对于数组每个元素的变化,我们都要做到让它是可响应的,这一点是至关重要的。

而对于数组的变异方法pushpopshiftunshiftsplicesortreverse。只有pushunshiftsplice是有新的元素添加入了原数组。因此我们需要对其做特殊处理!

特殊处理就是:对于每一项,我们都对它执行observeArray方法,使得Vue能够响应它自身的变化(也就是通过Object.defineProperty为之添加getter/setter方法)。

对于splice方法,其添加的参数在第二位。因而inserted = args.slice(2)

至此,变异方法的分析大概就到这里。

如何做到响应?

Observer的constructor

我们先分析这样一段代码:

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)
  }
}

Observer的作用是使得传入的对象/数组是相应的,这样我们才能够去实现VueVMModel之间的双向绑定。

如何observer一个数组?

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

下面来看observe方法:

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value)) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    observerState.shouldConvert &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

observe先对传入的数组作一个判断:如果不是引用类型,则返回,如果其原型上已有__ob__实例(即其已经被observe过了),则返回。否则就去做递归,使得其每一子项都是可观察的:ob = new Observer(value)

如何observer一个对象?

Vue执行this.walk(value)时,会对其对象每一项进行递归遍历,并对每一项执行defineReactive(obj, keys[i], obj[keys[i]]),使得对象是可响应的。

下面来看defineReactive的实现方式,由于量比较大,直接贴一段我写过注释的代码:

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: Function
) {
  // Dep有两个属性:id和subs(一个数组,用于存放它的观察者),
  // 通过发布 — 订阅模式实现双向绑定:
  // dep为发布者,他的subs属性用来存放了订阅他的观察者
  const dep = new Dep()

  // getOwnPropertyDescriptor返回指定对象上一个自有属性对应的属性描述符。
  //(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性,如getter/setter)
  // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor

  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 如果它已经被赋予了getter/setter,或者它是一个不可响应的数据(如props中的数据),则直接返回
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  // 之后我们为其创建getter/setter
  const getter = property && property.get
  const setter = property && property.set
  // 递归遍历,并对每一项执行defineReactive,使得对象是可响应的。
  let childOb = observe(val)
  // 定义其原型上的get和set函数,这是Vue设计思想的关键:使之可相应,同时通知视图层的刷新
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 为何要判断是否具有getter/setter?
      // 这里如果没有getter,其实这个值是一个基本类型,我们直接返回这个值就好
      // 否则我们执行其原型上的getter
      const value = getter ? getter.call(obj) : val
      // target其实是Vue内部的数组改变触发的getter,如果不是Vue内部数据改变导致的(如手动的DOM刷新)
      // 这时直接返回value就好
      if (Dep.target) {
        // 执行depend的目的是将其添加到订阅dep的观察者中
        // 一旦此数据改变,getter到这个数据的Dep.target也知晓了这一个变化!
        // 发布这个改变,订阅dep的观察者也会改变!
        dep.depend()
        if (childOb) {
          // 如果其为对象,则让其也depend
          childOb.dep.depend()
        }
        if (Array.isArray(value)) {
          // 如果其为数组,则执行dependArray
          dependArray(value)
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      // 判断是否相等,如果相等我们根本没必要浪费性能去触发一次setter!
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        // 如果是生产环境则触发另一种setter(不是dev环境,节省性能)
        customSetter()
      }
      if (setter) {
        // 如果其子元素有setter(不是基本类型),则触发setter
        setter.call(obj, newVal)
      } else {
        // 是基本类型返回就好
        val = newVal
      }
      // 观察其子元素
      childOb = observe(newVal)
      // notify通知订阅这个数据的元素也要发生改变!
      dep.notify()
    }
  })
}

总结

其实整个过程也正如文档里面所说的,递归,为每个引用类型添加getter/setter,对于数组的profill是为其添加变异方法来进行相应其数据的变化。对于对象我们只能做直接替换!(不能做某一项的改变,我们要刷一个对象的方式,文档里也有写到,只能类似于redux改变数据的方式,使用Object.assign,当然你也可以使用immutable)。

对于getter/setter,同样也需要对每一项进行递归的发布 - 订阅其主要为依赖于Dep对象的发布 - 订阅模式,对于getter,一旦订阅到这一个变化,还会去发布一个自身已经改变的状态给订阅其的数据。即源码中的dep.depend()。对于setter,一旦一个数据触发其set方法,Vue便会发布消息,通知订阅这个数据的元素也要发生改变。即源码中的dep.notify()

xingbofeng avatar Jul 23 '17 11:07 xingbofeng

请问 let ob: Observer | void 这段函数 是什么意思???有些不明白这个写法

ym754870370 avatar Aug 15 '19 12:08 ym754870370

splice 其添加的参数在第二位? 不应该是 第三位才是添加的元素么

ym754870370 avatar Aug 16 '19 03:08 ym754870370