海边的小溪鱼

vuePress-theme-reco 海边的小溪鱼    2017 - 2023
海边的小溪鱼 海边的小溪鱼

Choose mode

  • dark
  • auto
  • light
首页
分类
  • CSS
  • HTTP
  • Axios
  • jQuery
  • NodeJS
  • JavaScript
  • Vue2
  • Server
  • Vue3
标签
时间轴
更多
  • Gitee (opens new window)
Getee (opens new window)
author-avatar

海边的小溪鱼

28

文章

14

标签

首页
分类
  • CSS
  • HTTP
  • Axios
  • jQuery
  • NodeJS
  • JavaScript
  • Vue2
  • Server
  • Vue3
标签
时间轴
更多
  • Gitee (opens new window)
Getee (opens new window)

Vue2 响应式原理(数据劫持 与 发布-订阅者模式)

vuePress-theme-reco 海边的小溪鱼    2017 - 2023

Vue2 响应式原理(数据劫持 与 发布-订阅者模式)

海边的小溪鱼 2017-07-14 Vue2

Vue2 实现响应式的原理,以及存在的一些问题(手写 Vue2 响应式)

您可能需要了解(Vue3的响应式原理)

Vue 响应式指的是:data 中的数据发生变化时,视图也会随之更新

什么是数据驱动视图(MVVM)
数据驱动视图(Model-View-ViewModel):简写形式为"MVVM"
Model:表示数据层
View:表示视图层,通常就是 DOM 层
ViewModel:业务逻辑层,是 Model 与 View 的桥梁(数据变化驱动视图更新,视图变化会驱动数据更新)
Vue2 实现响应式的原理,以及存在的一些问题
image

  • 想完成整个响应,我们需要如下过程:
    • 数据劫持/代理:监听数据的变化
    • 收集依赖:收集视图依赖了哪些数据
    • 发布订阅模式:数据变化时,自动“通知”需要更新的视图部分,并进行更新
  • 响应式原理:采用 数据劫持 结合 发布-订阅者模式的方式,来实现数据的响应式。
    • 在创建 Vue 实例时,Vue 会遍历 data 中的每一个属性,并通过 Object.defineProperty() 给 data 中的每一个属性添加 getter 和 setter 方法
    • 当有人读取数据时,会触发 getter 方法(收集依赖),watcher(观察者)会把接触过的数据记录为依赖(会记录谁用了这个数据)
    • 当有人修改这个数据数据时,会触发 setter 方法(派发更新),通知 watcher(观察者)触发相应的监听回调,生成新的虚拟 DOM 树,视图更新(使关联这个数据的组件重新渲染)
  • Vue2 中操作对象和数组类型数据时,存在的一些问题
    • 对象类型:通过 Object.defineProperty() 数据劫持,来实现对属性的读取和修改
      • 缺点:新增属性、删除属性,界面不会更新
    • 数组类型:通过重写更新数组的一系列方法来实现拦截(对数组的变更方法进行了包裹)
      • 缺点:通过下标修改数组,界面不会更新

注意:每个组件都对应一个 watcher,它会在组件渲染时记录这些属性,并在 setter 触发时重新渲染。

# 1. 数据劫持/拦截:Object.defineProperty()

用于劫持一个对象的属性,通常我们对属性的 getter 和 setter 方法进行劫持,在对象的属性发生变化时进行特定的操作。

Vue2 响应式核心原理:通过 Object.defineProperty() 来劫持 data 里面各个属性的 getter 和 setter方法,来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图。

let person = { name: "张三", age: 18 }
let p = {}

// 格式:Object.defineProperty("对象名称", "属性名称", 配置对象)
Object.defineProperty(p, "name", {

    // get() 方法:当"读取"数据时会调用
    get() {
        console.log("有人读取了 name 属性");
        return person.name
    },

    // set() 方法:当"修改"数据时会调用
    set(value) {
        console.log(`有人修改了 name 属性,新值为${value},我要去更新界面`);
        person.name = value
    }

    // 那么问题来了,我要 添加/删除 一个属性,怎么才能被监听到呢?
    // 那么问题又来了, 我要读取/修改 age 属性,又得要写一个 Object.defineProperty() 方法
    // 所以 Vue2 中的响应式,还是有一些问题存在的
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

注意:只用 Object.defineProperty() 可以实现双向绑定,但是效率非常低,我们还需要结合发布-订阅者模式去更加精准的更新视图

# 2. Vue2 响应式(缺点)

  1. 新增、删除对象中的属性,页面不会更新
<template>
    <div class="container">
        <h2>{{ obj }}</h2>
        <button @click="addObj">添加对象属性</button>
        <button @click="delObj">删除对象属性</button>
    </div>
</template>
<script>
export default {
    data() {
        return {
            obj: { name: "王五", age: 30 }
        }
    },
    methods: {
        // 1. 添加对象属性,界面不会更新
        addObj() {
            this.obj.sex = "男" // 添加对象属性,界面不会更新
            // 解决方法:
            this.$set(this.obj, "sex", "男") // 通过 $set(目标对象, 属性名, 值)
            Vue.set(this.obj, "sex", "男") // 通过 Vue 身上的 set
        },
        // 2. 删除对象属性,界面不会更新
        delObj() {
            delete this.obj.name // 删除对象属性,界面不会更新
            // 解决方法:
            this.$delete(this.obj, "name") // 通过 $delete(目标对象, 属性名)
            Vue.delete(this.obj, "name") // 通过 Vue身上的 delete
        }
    }
}
</script>
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
  1. 通过下标修改数组,页面不会更新
<template>
    <div class="container">
        <h2>{{ arr }}</h2>
        <button @click="changeArr">修改数组元素</button>
    </div>
</template>
<script>
export default {
    data() {
        return {
            arr: ["张三", "李四"],
        }
    },
    methods: {
        // 1. Vue2 中通过下标修改数组,界面不会更新
        changeArr() {
            this.arr[0] = "小张" // 通过下标修改数组,界面不会更新
            // 解决方法:
            this.$set(this.arr, 0, "男") // 通过 $set(目标对象, 索引, 值)
            Vue.set(this.arr, 0, "男") // 通过 Vue 身上的 set
            this.arr.splice(0, 1, "男") // 通过 .splice(开始位置, 结束位置, 值)
        }
    }
}
</script>
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
帮助我们改善此页面! (opens new window)