Vue 的数据绑定机制

相关问题

  • Vue 是如何实现数据劫持的

  • Vue 是如何实现双向绑定的

  • MVVM 是什么

回答关键点

响应式对象 数据劫持 双向绑定 MVVM(Model-View-ViewModel) 发布/订阅模式(publish-subscribe pattern)

响应式对象

Vue2 通过 Object.defineProperty,Vue3 通过 Proxy 来劫持 state 中各个属性的 setter、getter,通过 getter 收集依赖。当 state 中的数据发生变动之后发布通知给订阅者更新数据。

双向绑定

Vue 通过 v-model 实现双向绑定。v-model 实际是 v-bind:xxx 和 v-on:xxx 的语法糖。当触发元素对应的事件(如 input、change 等)时更新数据(ViewModel),当数据(ViewModel)更新时同步更新到元素的对应属性(View)上。

MVVM(Model-View-ViewModel)

MVVM 模式是一种软件架构模式,相比 MVC 模式多了一个 ViewModel 层。有助于将图形用户界面的开发与业务逻辑或后端逻辑(数据模型)的开发分离开来。

  • Model:模型层,负责处理业务逻辑以及和服务器端进行交互。

  • View:视图层,将数据通过 UI 展现出来。

  • ViewModel:视图模型层,连接 Model 层和 View 层。

知识点深入

1. 前置概念

在详细说明原理之前,需要对以下概念有一定的认知:

  1. Dep:实现发布订阅模式的模块。

  2. Watcher:订阅更新和触发视图更新的模块。

2. 实现原理

image

上图是 Vue 官网描述 Vue 内数据变化与发布更新的流程,我们以响应式对象、依赖收集、数据更新的顺序详细说明整个过程。

2.1 响应式对象

Vue2 通过 Object.defineProperty,Vue3 通过 Proxy 来劫持 state 中各个属性的 getter、setter。其中 getter 中主要是通过 Dep 收集依赖这个属性的订阅者,setter 中则是在属性变化后通知 Dep 收集到的订阅者,派发更新。

以下是 Dep 的伪代码:

1export default class Dep {
2  static target?: Watcher;
3  subs: Array<Watcher>;
4
5  constructor () {
6    this.subs = []
7  }
8
9  /**
10   * 添加订阅者(watcher)
11   **/
12  addSub (sub: Watcher) {
13    this.subs.push(sub)
14  }
15
16  /**
17   * 移除订阅者(watcher)
18   **/
19  removeSub (sub: Watcher) {
20    remove(this.subs, sub)
21  }
22
23  /**
24   * 添加订阅(调用订阅者上的 addDep 方法)
25   **/
26  depend () {
27    if (Dep.target) {
28      Dep.target.addDep(this)
29    }
30  }
31
32  /**
33   * 遍历通知订阅者更新
34   **/
35  notify () {
36    const subs = this.subs.slice()
37    for (let i = 0, l = subs.length; i < l; i++) {
38      subs[i].update()
39    }
40  }
41}
42
43Dep.target = null

以下是生成响应式对象的伪代码:

1/**
2 * 生成响应式对象
3 * 为了方便理解,以下代码略有修改,省略了部分不相关内容
4 */
5export function defineReactive (
6  obj: Object,
7  key: string,
8  val: any,
9) {
10  const dep = new Dep()
11  Object.defineProperty(obj, key, {
12    enumerable: true,
13    configurable: true,
14    get: function reactiveGetter () {
15      // 每次 get 时如果有订阅者则添加订阅
16      if (Dep.target) {
17        dep.depend()
18      }
19      return val
20    },
21    set: function reactiveSetter (newVal) {
22      val = newVal
23      // 每次更新数据之后广播更新
24      dep.notify()
25    }
26  })
27}

2.2 依赖收集

Vue 会在需要使用到属性的地方新建一个 Watcher 的实例 watcher,watcher 实例化时会读取对应属性的内容,从而触发 1.1 中的 getter,将 watcher 注册进 Dep 中。

以下是 Watcher 的伪代码:

1/**
2 * 为了方便理解,以下代码略有修改,省略了部分不相关内容
3 */
4export default class Watcher {
5  vm: Component;
6  getter: Function;
7  value: any;
8  cb: Function;
9
10  constructor (
11    vm: Component,
12    exp: string
13  ) {
14    this.vm = vm
15    this.cb = cb
16    // 获取表达式对应的属性的 getter
17    this.getter = parsePath(exp)
18    this.value = this.get()
19  }
20
21  /**
22   * 获取最新的值
23   **/
24  get () {
25    // 将 Dep 的当前订阅者指向当前 watcher
26    Dep.target = this
27    let value
28    const vm = this.vm
29    // 获取对应属性值
30    value = this.getter.call(vm, vm)
31    // 清空 Dep 当前订阅者
32    Dep.target = null
33    return value
34  }
35
36  /**
37   * 订阅
38   **/
39  addDep (dep: Dep) {
40    // 将当前 watcher 添加到 Dep 的订阅者列表中
41    dep.addSub(this)
42  }
43
44  /**
45   * 更新视图
46   **/
47  update () {
48    const value = this.get()
49    const oldValue = this.value
50    // 调用 callback 更新视图
51    this.cb.call(this.vm, value, oldValue)
52  }
53}

2.3 数据更新

state 属性更新时会触发属性的 setter,setter 中会触发 Dep 的更新,Dep 通知 1.2 中收集到的 watcher 更新,watcher 获取到更新的数据之后触发更新视图。

3. 无法监听到的变化

由于受到 JavaScript 设计的限制,Vue2 使用的 Object.defineProperty 并不能完全劫持所有数据的变化,以下是几种无法正常劫持的变化:

  • 无法劫持新创建的属性,为了解决这个问题,Vue 提供了 Vue.set 以创建新属性。

  • 无法劫持数组的变化,为了解决这个问题,Vue 对数组原生方法进行了劫持。

  • 无法劫持利用索引修改数组元素,这个问题同样可以用 Vue.set 解决。

4. Vue2 与 Vue3 的差异

Vue2 与 Vue3 数据绑定机制的主要差异是劫持方式。Vue2 使用的是 Object.defineProperty 而 Vue3 使用的是 Proxy。Proxy 可以创建一个对象的代理,从而实现对这个对象基本操作的拦截和自定义。

特性 defineProperty Proxy
劫持新创建属性 否 是
劫持数组变化 否 是
劫持索引修改数组元素 否 是
兼容性 IE8及以上 不支持IE

由于 Vue 3 中改用 Proxy 实现数据劫持,Vue 2 中的 Vue.set/vm.$set 在 Vue 3 中被移除。

参考资料

  1. Vuejs 官网
  2. Vuejs 源码