Vue响应式原理

1/9/2022 Vue

# 数据驱动

学习Vue的高频词:

  • 数据响应式
  • 双向绑定
  • 数据驱动

# 数据响应式

指的是数据模型,数据模型就是普通的 JavaScript对象。

数据响应式的核心是:而当我们修改数据时,视图会进行更新,不用操作DOM。

这和使用jQuery的时候完全不一样,使用jQuery的核心就是DOM操作。而Vue内部帮我们进行了复杂的DOM操作,避免了繁琐的 DOM 操作,同时提高了开发效率。

# 双向绑定

数据改变,视图同时发生改变;视图改变,数据也随之改变。双向绑定包含了数据响应式。

因为双向绑定包含视图变化,所以针对的是可以和用户交互的表单元素。我们可以使用 v-model 在表单元素上创建双向数据绑定。

# 数据驱动

数据驱动是 Vue 最独特的特性之一 开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图上的。

注意:现在主流的MVVM已经帮我们实现了数据响应式和双向绑定。所以之后我们开发时只需要考虑业务本身。不用再考虑如何将数据渲染到DOM上。

# 数据响应式的核心原理

Vue3.0的响应式的实现方式和Vue2相比已经发生变化,逐个了解:

# Vue2.x —— Object.defineProperty()

深入响应式原理 (opens new window)

Vue2.x是使用Object.defineProperty()实现的,我们需要了解 Object.defineProperty()-MDN (opens new window) 的使用。

# Object.defineProperty的基本使用

  • ES5出现,不兼容IE8及以下
  • 单个属性转换如下:
// 模拟 Vue 中的 data 选项 
let data = { 
    msg: 'hello' 
}

// 模拟 Vue 的实例
let vm = {}

// 数据劫持:当访问或者设置 vm 中的成员的时候,做一些干预操作
// 参数一:vm对象
// 参数二:给vm对象添加的属性msg
// 参数三:设置msg的属性描述符
Object.defineProperty(vm, 'msg', {
    // 可枚举(可遍历) 
    enumerable: true, 
    // 可配置(可以使用 delete 删除,可以通过 defineProperty 重新定义) 
    configurable: true,
    // 当获取值的时候执行 
    get () { 
        console.log('get: ', data.msg)
        return data.msg
    },
    // 当设置值的时候执行 
    set (newValue) { 
        console.log('set: ', newValue) 
        // 如果数据未更改,直接返回
        if (newValue === data.msg) { 
            return
        }
        data.msg = newValue
        // 数据更改,更新 DOM 的值 
        document.querySelector('#app').textContent = data.msg
    } 
})

// 测试 
vm.msg = 'Hello World'
console.log(vm.msg)
// 控制台直接改变数据vm.msg = 'xxx',set执行,并将数据改为xxx并更新DOM
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
  • 一个对象中多个属性如何转换getter/setter?

我们需要遍历data的属性,并把每一个都转换成getter/setter

// 模拟 Vue 中的 data 选项
let data = {
  msg: 'hello',
  count: 10
}

// 模拟 Vue 的实例
let vm = {}

proxyData(data)

function proxyData(data) {
  // 遍历 data 对象的所有属性
  Object.keys(data).forEach(key => {
    // 把 data 中的属性,转换成 vm 的 setter/setter
    Object.defineProperty(vm, key, {
      enumerable: true,
      configurable: true,
      get () {
        console.log('get: ', key, data[key])
        return data[key]
      },
      set (newValue) {
        console.log('set: ', key, newValue)
        if (newValue === data[key]) {
          return
        }
        data[key] = newValue
        // 数据更改,更新 DOM 的值
        document.querySelector('#app').textContent = data[key]
      }
    })
  })
}

// 测试
vm.msg = 'Hello World'
console.log(vm.msg)
// 修改msg的值
vm.msg = 'xxx'// set: msg xxx
vm.count = 80 // set: count 80
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

# Vue3.0 —— Proxy

  • Proxy-MDN (opens new window)
  • Vue3.0是基于Proxy实现的,直接监听对象而非属性,因此将多个属性转化成getter/setter的时候不需要循环。
  • ES6中新增,IE不支持,性能由浏览器优化,比Object.defineProperty()要好。

# Proxy的基本使用

// 模拟 Vue 中的 data 选项
let data = {
  msg: 'hello',
  count: 0
}

// 模拟 Vue 实例
// Proxy 是一个构造函数,通过new出来的vm是一个代理对象,我们想访问data的时候,通过vm代理对象去访问
// 想获取msg属性,通过访问vm.msg,会触发代理对象的get方法
// 设置值的时候会触发代理对象的set方法
// 参数一: 代理对象
// 参数二:对象,执行代理行为的函数
let vm = new Proxy(data, {
  // 执行代理行为的函数
  // 当访问 vm 的成员会执行
  // get接收两个参数,target是目标对象,key是访问的属性,这里返回目标函数的值
  get (target, key) {
    console.log('get, key: ', key, target[key])
    return target[key]
  },
  // 当设置 vm 的成员会执行
  // set接收三个参数,target是目标对象,key是设置的属性,newValue是设置的新值
  set (target, key, newValue) {
    console.log('set, key: ', key, newValue)
    // 设置的值没有发生变化直接返回
    if (target[key] === newValue) {
      return
    }
    target[key] = newValue
    document.querySelector('#app').textContent = target[key]
  }
})

// 测试
vm.msg = 'Hello World'
console.log(vm.msg)
vm.msg = 'XXX' //set, key: msg xxx
vm.count = 90 // set,key: count 90
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

# 发布订阅模式 & 观察者模式

这是两种设计模式,在Vue中有不同的应用场景。这两种模式本质相同,但是是有区别的。

# 发布/订阅模式

  • 订阅者
  • 发布者
  • 信号中心

我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern)

Vue的自定义事件和Node中的事件机制都是基于发布/订阅模式的。

# Vue的自定义事件

# 基本用法
// Vue 自定义事件,首先创建一个Vue实例,不需要传任何参数。
let vm = new Vue()
// { 'click': [fn1, fn2], 'change': [fn] }

// vm.$on注册事件(订阅消息),可以多次调用传多个处理函数
// 参数一:dataChange事件名称
// 参数二:事件处理函数
vm.$on('dataChange', () => {
  console.log('dataChange')
})

vm.$on('dataChange', () => {
  console.log('dataChange1')
})

// 特定时机可以调用$emit触发事件(发布消息)
vm.$emit('dataChange')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

在上述代码中,很难去分辨出来那个是订阅者、发布者和订阅中心,其实这三者都是vm,可以通过兄弟组件通信过程看

  • 调用$emit的就是发布者
  • 调用$on的就是订阅者
// eventBus.js 
// 事件中心:创建一个Vue实例,不需要传任何参数
let eventHub = new Vue() 
// ComponentA.vue 
// 发布者:
addTodo: function () { 
    // 发布消息(事件)
    eventHub.$emit('add-todo', { text: this.newTodoText }) 
    this.newTodoText = '' 
}
// ComponentB.vue
// 订阅者 
created: function () { 
    // 订阅消息(事件)
    eventHub.$on('add-todo', this.addTodo) 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 模拟实现
  • $on中只是订阅事件,并没有触发,所以内部需要一个变量存放事件名称以及其处理函数。
  • 注册的时候可以注册多个名称也可以注册多个处理函数,采用键值对的形式,键是事件名称,值是对应处理函数。
  • 同一个名称可以对应多个处理函数,所以值的类型是数组{ 'click': [fn1, fn2], 'change': [fn] }
  • $emit通过对应的名称去便令里面找对应属性值,依次执行函数
  • 目前不考虑执行函数有参数情况
// 事件触发器
class EventEmitter {
  // 定义属性subs,是一个对象,存储事件以及事件对应的处理函数
  constructor () {
    // 参数是null表示这个对象没有原型属性
    this.subs = Object.create(null)
  }

  // 注册事件
  // 参数一:事件类型
  // 参数二:事件处理函数
  $on (eventType, handler) {
    this.subs[eventType] = this.subs[eventType] || []
    this.subs[eventType].push(handler)
  }

  // 触发事件
  // 参数一:事件类型
  $emit (eventType) {
    // 如果有值就遍历数组执行,如果没有值不做操作
    if (this.subs[eventType]) {
      this.subs[eventType].forEach(handler => {
        handler()
      })
    }
  }
}

// 测试,创建一个信号中心
let em = new EventEmitter()
em.$on('click', () => {
  console.log('click1')
})
em.$on('click', () => {
  console.log('click2')
})

em.$emit('click')
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

# 观察者模式

Vue的响应式机制中使用了观察者模式。

  • 观察者(订阅者) -- Watcher
    • update():所有的观察者都有的方法,当事件发生时,会调用所有订阅者的update方法,Vue的相应机制中,当数据发生变化的时候会调用观察者的update方法,update内部去更新视图,此update方法是目标调用的。
  • 目标(发布者) -- Dep
    • 在发布者内部记录所有的观察者,当事件发生的时候,目标通知观察者,目标内部有一个属性,存放所有订阅者,下面是需要的方法:
    • subs 数组:存储所有的观察者,所有依赖目标的观察者都在这里
    • addSub():添加观察者到数组中
    • notify():当事件发生,调用所有观察者的 update() 方法
  • 没有事件中心
  • 忽略传参情况
// 发布者-目标
class Dep {
  constructor () {
    // 记录所有的订阅者
    this.subs = []
  }
  // 添加订阅者
  // 参数:订阅者
  addSub (sub) {
    // 判断这个对象存在且有update方法才为观察者
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }
  // 发布通知
  notify () {
    // 遍历所有观察者调用其update方法
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}
// 订阅者-观察者
class Watcher {
  update () {
    console.log('update')
  }
}

// 测试
let dep = new Dep()
let watcher = new Watcher()
// 将观察者添加到目标中
dep.addSub(watcher)
// 事件发生的时候调用目标的notify方法
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

# 发布订阅模式 VS 观察者模式

观察者模式 发布订阅模式
不存在事件中心,观察者和目标相互依赖 存在事件中心,发布者和订阅者不互相依赖
目标要知道观察者的存在,并且由目标调用观察者的update方法 发布者和订阅者互相不知道存在,由事件中心触发事件
更新时间: 2022-01-10 09:52