Vue实现数据响应式的原理

Youky ... 2021-8-30 前端
  • Vue
About 5 min

# Vue实现数据响应式的原理

源码版本:2.6.14(增删了一些注释)

响式是指,data中数据改变时自动更新视图。

实现数据的响应式,一共分为四个组成部分:

# Observer

使数据的改变成为可观测的

在Observer类中,用递归的方法通过Object.defineProperty为属性添加getter和setter,使数据变为响应式。

对于数组属性,则进行了额外处理。

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)     // 将该数据的__ob__属性指向该Observer,表示它已是响应式的了
    if (Array.isArray(value)) {    // 对于数组和对象分别处理
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      // 为数据定义响应式的属性,即在setter中进行收集,在getter中触发更新
      defineReactive(obj, keys[i])  
    }
  }

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

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,
    
    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()      // 触发更新
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

# Dep

收集依赖

# 什么是收集依赖?

一个数据可能会在多个地方使用,因此对应一个数据使用一个Dep收集所有依赖该数据的视图。当数据发生变化时,将Dep里的所有视图进行更新即可

# 如何进行?

在Observer中对于数据定义了getter和setter,因此:

  • 在getter中进行依赖收集
  • 在setter中通知依赖更新

在Dep类中,用一个subs数组存放Watcher,并定义了对subs进行增删操作的函数。

# Watcher

谁用了数据,谁就是“依赖”,就为谁创建一个Watcher

进行依赖收集的过程:

  1. 在构造函数中调用get方法,最终通过pushTarget将自身赋值给Dep.target
  get () {
    pushTarget(this)    // 给Dep.target赋值
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  1. 通过this.getter获取被依赖的数据,在数据的getter中会触发其dep.depend方法,收集依赖。并在收集完成后将Dep.target释放
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
},
1
2
3
4
5
6
7
8
9
10
11
12
13
  1. 数据变化时,触发数据的setter,在其中调用dep.notify方法,触发subs数组中的所有Watcher实例的update方法
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()
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13

# 对于数组的处理

我们知道,对于数组直接进行修改无法触发响应式更新,但可以通过Array的七个API进行修改。

原因是对于这七个API进行了拦截,方法是通过更改数组的原型链

  1. 首先,定义一个新的原型
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
1
2
  1. 定义需要拦截的方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)   // 调用原有方法,达到预期效果
    const ob = this.__ob__
    let inserted    // 新插入了的元素
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted) // 将新添加的元素定义为响应式
    // notify change
    ob.dep.notify()     // 通知数据的变更
    return result
  })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

为什么这里对push、unshift、splice做了判断?

这三种方法会添加新元素。

  1. 在Observer的构造函数中,更改数组的__proto__属性
if (Array.isArray(value)) {
  if (hasProto) { // 如果可以使用__proto__属性
    protoAugment(value, arrayMethods)   // 更改原型链
  } else {
    copyAugment(value, arrayMethods, arrayKeys) // 如果不支持,重新定义这七种方法
  }
  this.observeArray(value)
}

function protoAugment (target, src: Object) {
  target.__proto__ = src
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 总结

响应式原理图

  • Observer类通过Object.defineProperty将数据对象转换为可观测的
  • 为每一个依赖创建一个Watcher,会添加到对应的Dep中。当数据变化时,Dep会通知Watcher,然后Watcher通知依赖进行更新
  • 用Dep类实现了对依赖的收集,当数据改变时通知其中的所有依赖

# 为什么$set可以保持响应式

set方法的定义源码:

export function set (target: Array<any> | Object, key: any, val: any): any {
  // 在非生产环境下,如果传入的target是null、undefined、原始数据类型,则给出警告
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {   
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  
  // 如果target是数组,用splice这个API进行更改
  if (Array.isArray(target) && isValidArrayIndex(key)) {    
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }

  // 如果key是target中已存在的属性,则直接更改即可
  if (key in target && !(key in Object.prototype)) { 
    target[key] = val   // 这里的修改会触发该属性的setter,并通知更新
    return val
  }
  
  const ob = (target: any).__ob__ // 如果target已进行响应式处理,则ob是其Observer实例,否则不存在

  // 如果target是Vue实例或Vue实例的data对象,抛出警告
  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
  }

  // ob不存在,说明target不是响应式的,因此直接添加属性即可
  if (!ob) {
    target[key] = val
    return val
  }

  // target是响应式的,则通过defineReactive响应式的定义该属性,并通知更新
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

通过源码可得知:

  • 如果目标是数组,首先判断索引是否大于当前的length,若大于则先修改length。然后调用splice方法
  • 如果是对象,则判断属性是否存在、该对象是否是响应式的:
    • 如果该属性已经存在,或该对象不是响应式的,则直接赋值
    • 否则:调用内部defineReactive方法,响应式的添加属性,并通知更新
Last update: October 11, 2021 16:57
Contributors: youky7