响应式对象

    Object.defineProperty 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象,先来看一下它的语法:

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

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

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

    initState

    在 Vue 的初始化阶段,_init 方法执行的时候,会执行 initState(vm) 方法,它的定义在 src/core/instance/state.js 中。

    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 方法主要是对 propsmethodsdatacomputedwathcer 等属性做了初始化操作。这里我们重点分析 propsdata,对于其它属性的初始化我们之后再详细分析。

    • initProps
    1. function initProps (vm: Component, propsOptions: Object) {
    2. const propsData = vm.$options.propsData || {}
    3. const props = vm._props = {}
    4. // cache prop keys so that future props updates can iterate using Array
    5. // instead of dynamic object key enumeration.
    6. const keys = vm.$options._propKeys = []
    7. const isRoot = !vm.$parent
    8. // root instance props should be converted
    9. if (!isRoot) {
    10. toggleObserving(false)
    11. }
    12. for (const key in propsOptions) {
    13. keys.push(key)
    14. const value = validateProp(key, propsOptions, propsData, vm)
    15. /* istanbul ignore else */
    16. if (process.env.NODE_ENV !== 'production') {
    17. const hyphenatedKey = hyphenate(key)
    18. if (isReservedAttribute(hyphenatedKey) ||
    19. config.isReservedAttr(hyphenatedKey)) {
    20. warn(
    21. `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
    22. vm
    23. )
    24. }
    25. defineReactive(props, key, value, () => {
    26. if (vm.$parent && !isUpdatingChildComponent) {
    27. warn(
    28. `Avoid mutating a prop directly since the value will be ` +
    29. `Instead, use a data or computed property based on the prop's ` +
    30. `value. Prop being mutated: "${key}"`,
    31. vm
    32. )
    33. }
    34. })
    35. } else {
    36. defineReactive(props, key, value)
    37. }
    38. // static props are already proxied on the component's prototype
    39. // during Vue.extend(). We only need to proxy props defined at
    40. // instantiation here.
    41. if (!(key in vm)) {
    42. proxy(vm, `_props`, key)
    43. }
    44. }
    45. toggleObserving(true)
    46. }
    • initData

    data 的初始化主要过程也是做两件事,一个是对定义 data 函数返回对象的遍历,通过 proxy 把每一个值 vm._data.xxx 都代理到 vm.xxx 上;另一个是调用 observe 方法观测整个 data 的变化,把 data 也变成响应式,可以通过 vm._data.xxx 访问到定义 data 返回函数中对应的属性,observe 我们稍后会介绍。

    可以看到,无论是 props 或是 data 的初始化都是把它们变成响应式对象,这个过程我们接触到几个函数,接下来我们来详细分析它们。

    首先介绍一下代理,代理的作用是把 propsdata 上的属性代理到 vm 实例上,这也就是为什么比如我们定义了如下 props,却可以通过 vm 实例访问到它。

    1. let comP = {
    2. props: {
    3. msg: 'hello'
    4. },
    5. methods: {
    6. say() {
    7. console.log(this.msg)
    8. }
    9. }
    10. }

    我们可以在 say 函数中通过 this.msg 访问到我们定义在 props 中的 msg,这个过程发生在 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.definePropertytarget[sourceKey][key] 的读写变成了对 target[key] 的读写。所以对于 props 而言,对 vm._props.xxx 的读写变成了 vm.xxx 的读写,而对于 vm._props.xxx 我们可以访问到定义在 props 中的属性,所以我们就可以通过 vm.xxx 访问到定义在 props 中的 xxx 属性了。同理,对于 data 而言,对 vm._data.xxxx 的读写变成了对 vm.xxxx 的读写,而对于 vm._data.xxxx 我们可以访问到定义在 data 函数返回对象中的属性,所以我们就可以通过 vm.xxxx 访问到定义在 data 函数返回对象中的 xxxx 属性了。

    observe

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

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

    1. /**
    2. * Observer class that is attached to each observed
    3. * object. Once attached, the observer converts the target
    4. * collect dependencies and dispatch updates.
    5. */
    6. value: any;
    7. dep: Dep;
    8. vmCount: number; // number of vms that has this object as root $data
    9. constructor (value: any) {
    10. this.value = value
    11. this.dep = new Dep()
    12. this.vmCount = 0
    13. def(value, '__ob__', this)
    14. if (Array.isArray(value)) {
    15. const augment = hasProto
    16. ? protoAugment
    17. : copyAugment
    18. augment(value, arrayMethods, arrayKeys)
    19. this.observeArray(value)
    20. } else {
    21. this.walk(value)
    22. }
    23. }
    24. /**
    25. * Walk through each property and convert them into
    26. * getter/setters. This method should only be called when
    27. * value type is Object.
    28. */
    29. walk (obj: Object) {
    30. const keys = Object.keys(obj)
    31. for (let i = 0; i < keys.length; i++) {
    32. defineReactive(obj, keys[i])
    33. }
    34. }
    35. /**
    36. * Observe a list of Array items.
    37. */
    38. observeArray (items: Array<any>) {
    39. for (let i = 0, l = items.length; i < l; i++) {
    40. observe(items[i])
    41. }
    42. }
    43. }

    Observer 的构造函数逻辑很简单,首先实例化 Dep 对象,这块稍后会介绍,接着通过执行 def 函数把自身实例添加到数据对象 valueob 属性上,def 的定义在 src/core/util/lang.js 中:

    1. /**
    2. * Define a property.
    3. */
    4. export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
    5. Object.defineProperty(obj, key, {
    6. value: val,
    7. enumerable: !!enumerable,
    8. writable: true,
    9. configurable: true
    10. })

    def 函数是一个非常简单的Object.defineProperty 的封装,这就是为什么我在开发中输出 data 上对象类型的数据,会发现该对象多了一个 ob 的属性。

    回到 Observer 的构造函数,接下来会对 value 做判断,对于数组会调用 observeArray 方法,否则对纯对象调用 walk 方法。可以看到 observeArray 是遍历数组再次调用 observe 方法,而 walk 方法是遍历对象的 key 调用 defineReactive 方法,那么我们来看一下这个方法是做什么的。

    defineReactive

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

    defineReactive 函数最开始初始化 Dep 对象的实例,接着拿到 obj 的属性描述符,然后对子对象递归调用 observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj 中一个嵌套较深的属性,也能触发 getter 和 setter。最后利用 Object.defineProperty 去给 obj 的属性 添加 getter 和 setter。而关于 getter 和 setter 的具体实现,我们会在之后介绍。