Vue 响应式原理

Vue


# 观察者模式

vue 响应式系统的实现其实使用了一种称作观察者模式的设计模式。说到观察者模式,会涉及到两个很重要的角色:

  • 被观察目标:上图中的响应式数据对象 Data
  • 观察者:上图中的 Watcher

结合上图,可以将 Vue 的响应式系统的整体流程简单描述为下面的过程:

  1. 组件 render 过程中,会触发响应式数据(Data)的 getter,在这个过程中,就会完成观察者对被观察目标的依赖绑定,也就是常说的依赖收集,而被观察目标就是响应式数据(Data)本身(被观察目标与观察者之间是一对多的关系,因为一个响应式数据可能被多个观察者依赖)
  2. 一旦被观察的目标发生改变(比如重新赋值),会触发响应式数据的 setter,然后在 setter 中会通知(Notify)到所有在上一步中绑定的观察者(Watcher)。观察者收到通知后,会触发组件的重新渲染(Trigger re-render
  3. 组件重新渲染又会调用组件的渲染方法(Component Render Function),这个过程会涉及到上一篇提到的针对新旧组件 vnode 的 diff 以及 vnode 到真实 DOM 的 patch 过程。

要实现上面的整个过程,有几个关键的节点,

  • 响应式数据的创建
  • 观察者与被观察目标的依赖关系绑定,即依赖收集
  • 被观察目标变更后如何通知到所有相关联的观察者,即派发更新

Vue 源码中利用三个类实现了 依赖收集以及派发更新

  1. Observer:辅助的可观测类,数组/对象通过它的转化,就会成为响应式数据
  2. Dep响应式数据的每一个属性都会拥有一个 Dep 类实例,Dep 实例内部有个 subs 队列,保存着依赖本数据的观察者,当本数据变更时,调用 dep.notify() 通知观察者
  3. Watcher:扮演观察者的角色,进行观察者函数的包装处理。如 render()函数,会被包装成一个 Watcher 实例

由此可见,Dep其实是被观察者(Observer)与观察者(Watcher)之间的一座桥梁,

  • 一方面,响应式数据的每一个 key 都会有一个与之绑定的 Dep 实例,该实例可以为响应式数据搜集所对应的观察者,并存放到 Dep 实例的 subs 数组中去
  • 另一方面,当响应式数据发生变化的时候,就会通过与该响应式数据绑定的 Dep 实例,获取到所有相应的观察者,并通知它们进行更新操作

# 响应式数据的创建

在 vue 实例化过程中,有一步是对数据 state 的初始化操作,即 initState

function initState(vm) {
  vm._watchers = []
  var 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)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

以 initData 为例,observe(data)就是将 data 转化为响应式对象的入口,而在 observe 方法中,

  • 首先判断如果传入的 value 不是对象,或者是 vnode 实例,则直接返回
  • 然后根据对象的__ob__属性是否存在,判断对象是否已经是响应式的,是的话,就会直接返回该属性的值
  • 最后就是实例化响应式对象的逻辑 new Observer(value)
function initData(vm) {
  var data = vm.$options.data
  data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
  // ...
  // observe data
  observe(data, true /* asRootData */)
}

function observe(value, asRootData) {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  var ob
  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)
  }
  // ...
  return ob
}
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

# Observer 类

这个类的主要工作,就是遍历对象或数组上的所有属性,并将其设置为响应式的

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
    // 将 __ob__ 属性设置为 当前 observer 实例
    def(value, '__ob__', this)

    // 对数组的处理
    if (Array.isArray(value)) {
      // 如果当前环境支持 `__proto__` 属性
      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++) {
      defineReactive(obj, keys[i])
    }
  }

  // 处理数组中的每一个元素
  observeArray(items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
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

# 依赖收集

在 Observer 类中,对于对象和数组的处理是不一样的,对数组的处理相对会复杂一些,原因想必大家都已经很清楚了。现在就分开看一下

# 处理对象

对于对象的处理过程:利用 walk 方法,遍历对象自身所有可枚举属性(不包括符号属性),依次进行 defineReactive 处理。

其实所谓的响应式对象,就是通过 Object.defineProperty 方法,重新定义了对象属性的 getter 和 setter,其中:

  1. getter:使其能够在该值被读取的时候将依赖该数据的 Watcher 实例添加到 Dep 实例的 subs 数组中去
  2. setter:使得当该值被修改的时候,能够通过 Dep 实例的 notify 方法,通知所有相关的 Watcher 实例完成更新操作
function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 对象的每个 key 都会对应一个 Dep 实例
  const dep = new Dep()

  // 获取属性描述符对象
  const property = Object.getOwnPropertyDescriptor(obj, key)

  // 如果属性不可配置,则跳过
  if (property && property.configurable === false) {
    return
  }

  // AOP 技巧
  const getter = property && property.get
  const setter = property && property.set

  // 如果没有 getter,则通过 obj[key] 的方式获取属性值,并赋值给 val
  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) {
      // ...
    },
  })
}
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

上面提到,在 getter 中会将与对象某个属性相关的 Watcher 实例添加到 Dep 依赖中,那要怎么才能知道究竟是哪个 Watcher 实例依赖了这个属性的值呢?这里用到的就是 Dep 类上的一个静态属性 target。还是以 App 组件为例,

  • App 组件的渲染 Watcher 实例
class Watcher {
  constructor() {
    // ...
    this.value = this.get()
  }
  get() {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      // ...
    } finally {
      // ...
      popTarget()
    }
    return value
  }
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  1. 当 App 组件 mount 过程中,会创建一个组件的 渲染 Watcher 实例,因此构造器中 get 方法会被触发,所以 get 方法内部就会通过 pushTarget(this) 将当前 Watcher 实例挂到 Dep.target 上

  2. 紧接着在获取 value 的过程中,this.getter 的执行,就会触发 App 组件的渲染函数 render。而 render 函数中,就会触发响应式数据的 getter,比如这里的 _vm.name

  • App 组件的数据对象 data 以及 render 函数
{
  data: function data() {
    return {
      name: 'App'
    };
  },
  render: function() {
    var _vm = this
    var _h = _vm.$createElement
    var _c = _vm._self._c || _h
    return _c("div", { staticClass: "content-wrapper" }, [
      _vm._v(_vm._s(_vm.name))
    ])
  },
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

由于此时 Dep.target 上已经存在值,所以就会将当前的 渲染 Watcher 实例添加进跟 _vm.name 这个属性绑定的 Dep 实例的 subs 数组中,这就完成了依赖收集的过程。

# 处理数组

访问对象属性,其取值与赋值操作,都能被 Object.defineProperty() 成功拦截,但是 Object.defineProperty() 在处理数组上存在如下问题:

对于数组变异方法pop、push、shift、unshift、reverse、sort、spliceObject.defineProperty()无法监听到变更

针对这个问题,vue 是怎么做的呢?

答案:还是运用了 AOP 的编程思想,重写了 Array 原型上的数组变异方法

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

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse',
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]

  // 利用 AOP 技巧,注入自定义功能
  Object.defineProperty(arrayMethods, method, {
    value: function (...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
    },
    enumerable: !!enumerable,
    writable: true,
    configurable: true,
  })
})
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

# Object.defineProperty()的缺陷

上述的问题可以通过 hack 的方式来解决,但还有一些问题是没有办法通过 hack 的方式解决的,

# 对于对象

Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

# 对于数组

Vue 不能检测以下数组的变动:

  • 当利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  • 当修改数组的长度时,例如:vm.items.length = newLength

不过针对上面的问题,vue 官方文档也给了相应的解决方案 (opens new window),这里不细说了,只看下 Vue.set 方法的实现

# Vue.set(target, key, val)

function set(target: Array<any> | Object, key: any, val: any): 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 (!ob) {
    target[key] = val
    return val
  }

  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

# 派发更新

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter() {
    //  ...
  },
  set: function reactiveSetter(newVal) {
    var value = getter ? getter.call(obj) : val
    if (newVal === value || (newVal !== newVal && value !== value)) return
    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

可以看到,派发更新的过程就是调用 dep.notify 的过程

// ======= Dep ======== //
class Dep {
  static target: ?Watcher

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

  // ...

  notify() {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    // ...
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// ======= Watcher ======== //
class Watcher {
  // ...
  update() {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      // 需要立即响应变更
      this.run()
    } else {
      // 放到队列中等待下一次事件循环时,统一做出响应
      queueWatcher(this)
    }
  }
}
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

Watcher 实例的 update 方法中,对不同的 watcher 实例有不同的处理,用户定义 watcher 时,如果不特别指定,lazy、sync 属性的值默认都为 false,所以一般都会走到 queueWatcher 的逻辑中去。 queueWatcher 的流程中,会涉及到 nextTick 相关的内容,下一篇再说,这篇就先到这里。

Last Updated: 10/21/2024, 4:15:17 PM