Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue - 响应式对象 #20

Open
VenenoFSD opened this issue Apr 24, 2019 · 0 comments
Open

Vue - 响应式对象 #20

VenenoFSD opened this issue Apr 24, 2019 · 0 comments
Labels
important review remember to review Vue Vue.js source code

Comments

@VenenoFSD
Copy link
Owner

VenenoFSD commented Apr 24, 2019

前言

在第二章,我们学习了数据驱动的概念,分析了 Vue 的初始化过程以及数据是如何渲染到 DOM 上的整个过程,但是还没有分析数据变化后是如何驱动视图更新的。

这一章我们就来分析由于用户交互或者其他方面导致数据发生变化引起页面重新渲染的原理,下面先看一个例子:

<div id="app" @click="changeMsg">{{ msg }}</div>
<script>
  new Vue({
    el: '#app',
    data() {
      return {
        msg: 'Hello'
      }
    },
    methods: {
      changeMsg() {
        this.msg = 'HelloWorld';
      }
    }
  })
</script>

在这个例子中,点击 div 会修改 msg 的值从而引发视图更新,那在 Vue 中是怎么实现的呢,接下来就来研究 Vue 响应式原理的源码实现。

响应式对象

根据 Vue 官方文档 的介绍,当我们把一个普通的 js 对象传入 Vue 实例作为 data 选项时,Vue 会遍历此对象的所有属性,并使用 Object.defineProperty 给这些属性添加 getter/setter ,使之成为一个响应式对象。

Object.defineProperty

我曾经在 我的博客 介绍过 Object.defineProperty,这里过一遍核心的概念。Object.defineProperty 这个方法可以在一个对象上定义属性或修改现有属性,第三个参数 属性描述符 是一个对象,顾名思义是对属性的描述,有 数据描述符存取描述符 两种。存取描述符 可以设置 getset 两个方法,这样当属性被读取时就会自动调用 get 方法,属性被修改就会自动调用 set 方法。

举个例子说明:

let obj = {
  a: 1
};
Object.defineProperty(obj, 'a', {
  get() {
    console.log('a 被访问了');
  },
  set() {
    console.log('a 被修改了');
  }
});
let b = obj.a; // a 被访问了
obj.a = 2; // a 被修改了

Vue 正是使用了上述 Object.defineProperty 的用法来实现响应式对象的,接下来我们从源码的角度来分析。

initState

我们知道在 _init 函数中调用了一系列 init 方法做初始化工作,其中有一个方法 initState ,它的作用是初始化 propsdatamethods 等属性,定义在 src/core/instance/state.js 文件中:

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

可以看到这里针对不同的属性调用了不同的函数来处理,这一节我们只分析针对 propsdata 的处理,先来看 initProps

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) {
    // ...
  }
  toggleObserving(true)
}

initProps 函数一开始拿的 vm.$options.propsData 指的是组件外部传入组件的值,要注意和参数 propsOptions 的区别。可以看到这里在 for 循环前调用了 toggleObserving(false) ,在循环后又调用了 toggleObserving(true) ,这么做的原因稍后会分析。然后是 for 循环中的内容:

for (const key in propsOptions) {
  keys.push(key)
  const value = validateProp(key, propsOptions, propsData, vm)
  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production') {
    const 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, () => {
      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
        )
      }
    })
  } 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)
  }
}

for 循环首先将 propsOptions 中的 key push 到 vm.$options._propKeys 中,然后调用了 validateProp 函数,现在我们只需要知道 validateProp 函数的作用是校验每个 key 给定的 prop 数据是否符合预期的类型,并返回相应 prop 的值(或默认值)。之后在 if else 逻辑都调用了 defineReactive 函数,这个函数作用是把 prop 中的每个 key 都变成响应式的,最后执行的 proxy 函数作用是把对 vm._props.xxx 的访问代理到 vm.xxx 上,这两个函数都会在稍后具体分析。

这样我们大概了解了 initProps 函数后,再来看 initData 函数:

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // 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 (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

initData 通过 vm.$options.data 拿到 data 后,如果 data 是一个函数则将它返回的对象赋值给 datavm._data 。然后会遍历 data 的属性,先检查 data 属性和 methodsprops 是否重名,然后调用 proxyvm._data.xxx 代理到 vm.xxx 上。最后调用 observe 函数来观察 data

在大概浏览了 initPropsinitData 的执行流程后,我们了解到了这两个函数最主要的作用是将 propsdata 转换为响应式对象,接下来就逐个来分析 initPropsinitData 中调用的函数。

proxy

本来 data 的属性是保存在 vm._data 上的, props 的属性是保存在 vm._props 上的,但我们在实际使用 Vue 时却可以在 vm 上访问这些属性,这是因为内部调用 proxy 函数做了一层访问代理,我们来看 proxy 函数是怎么实现的:

// /src/core/instance/state.js

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
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)
}

可以看到,这里先定义了一个全局的属性描述符 sharedPropertyDefinition ,在调用 proxy 函数的时候(以 proxy(vm, '_data', key) 为例),会在 vm 定义一个 key 属性,通过 gettersetter ,使得访问 vm.key 时会返回 vm._data.key ,修改 vm.key 时实际会修改 vm._data.key ,相当于做了一层访问代理。

observe

initData 最后调用了 observe 函数来观察 dataobserve 函数定义在 src/core/observer/index.js 文件中:

// /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 函数的作用是给传入的 value 创建一个观察者实例并返回(如果已经有了则直接返回)。函数首先对 value 的类型做了限制,必须是一个非 VNode 类型的对象,然后通过 __ob__ 检查观察者实例是否已存在,存在则直接返回,不存在的话会判断是否要创建一个实例。创建观察者实例需要满足的第一个条件是 shouldObserve ,在 src/core/observer/index.js 文件中有这么几行代码:

export let shouldObserve: boolean = true

export function toggleObserving (value: boolean) {
  shouldObserve = value
}

可以看到 ,shouldObserve 相当于一个开关,决定是否要观察,而 toggleObserving 函数控制 shouldObserve 的开和关,回顾 initProps 函数:

function initProps (vm: Component, propsOptions: Object) {
  // ...
  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    // ...
  }
  toggleObserving(true)
}

可以看到,是否应该被观察取决于当前实例是不是根实例。回到 observe ,创建观察者实例还需要满足以下条件:

  • 非服务端渲染
  • value 是一个数组或者是纯对象
  • value 是可扩展的(可添加属性)
  • value 不是 Vue 实例

满足这些条件后就会调用 Observer 创建一个观察者实例并返回出去。Observer 定义如下:

// /src/core/observer/index.js

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

Observerconstructor 中,初始化了几个属性的值,其中 dep 会在下一节详细介绍,接着执行 def(value, '__ob__', this) ,来看 def 函数的定义:

// /src/core/util/lang.js

export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

可以看到,def 函数就是调用 Object.defineProperty 函数给 value(对应到 initData 函数 就是 data 对象)添加一个 __ob__ 属性,属性值为这个观察者实例。这里为什么不直接 value.__proto__ = this 而要调用 def 函数呢,带着这个疑问我们继续往下分析。

接下来针对不同类型的 value 做不同处理:

  • value 是一个数组,则调用 observeArray 函数。observeArray 函数的逻辑很简单,就是遍历 value 的每一项递归调用 observe 函数。
  • value 是一个纯对象,则调用 walk 函数,walk 函数会遍历 value 对象的属性调用 defineReactive 函数。

可以看到无论是 observeArray 还是 walk 函数都会遍历 value ,这就是为什么要调用 def 函数来设置 __ob__ 属性,通过设置 __ob__ 属性描述符的 enumerable = false(不可枚举),这样可以避免 __ob__ 属性被遍历。

Observer 类中调用了 defineReactive 函数,在 initProps 中也有调用,这个函数其实就是给对象属性添加 gettersetter 。下面就来分析 defineReactive 函数。

defineReactive

defineReactive 函数定义在 src/core/observer/index.js 中:

// /src/core/observer/index.js

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 函数首先会通过 Object.getOwnPropertyDescriptor 拿到对象指定属性的属性描述符,如果属性描述符的 configurable 属性(表示该属性是否可配置)为 falsereturn 结束函数。之后会尝试获取属性描述符的 gettersetter ,如果没有传入 value 参数并且没有 getter 或者有 setter(因为 getter 可能会影响取值),则主动通过 obj[key] 拿到 value

接着递归调用 observe(val) ,因为 val 本身可能也是一个对象,要递归调用实现深度观察。这样 data 下的所有非 VNode 对象属性都会有 __ob__ 属性,所以可以通过 __ob__ 属性来判断一个对象是不是响应式对象,这个在后面章节也会有所涉及。最后就是调用 Object.defineProperty 重新定义属性 key ,配置存取描述符:包括可枚举,可配置,定义 gettersettergetter 要做的就是依赖收集setter 要做的是派发更新,这两个函数的具体逻辑会放在下一节详细介绍。

总结

这一节我们学习了 Vue 是如何深度观察 dataprops 对象的,它的核心就是调用 Object.defineProperty 给对象的属性添加 gettersetter 使之成为一个响应式对象,并且如果属性也是对象则递归调用 observe 函数实现深度观察。

这一节我们还没有分析 gettersetter 的具体逻辑,下一节我们先来分析 getter ,也就是 依赖收集 的原理。

@VenenoFSD VenenoFSD added Vue Vue.js source code review remember to review important labels Apr 24, 2019
@VenenoFSD VenenoFSD added this to 四、深入响应式原理 in Learing Vue.js source code Apr 24, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
important review remember to review Vue Vue.js source code
Projects
Learing Vue.js source code
四、深入响应式原理
Development

No branches or pull requests

1 participant