Gahing's blog Gahing's blog
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Gahing / francecil

To be best
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 前端基础

  • 应用框架

    • UI 框架

      • Angular

      • React

      • Solid

      • Svelte

      • Vue

        • Vue 项目中的 data-v-xxx 是怎么生成的
        • Vue之从零编写一个ContextMenu(右键菜单)插件
        • Vue 第一个组件,浏览器后退无法触发beforeRouteLeave的问题与解决
        • Vue问题记录
        • vue 中 updated 的执行时机
        • 源码解析

          • Vue 源码解析-0.前置准备
          • Vue实例化过程
          • Vue响应式原理
            • 前言
            • 响应式实现
            • 后记
            • 参考资料
          • Vue响应式之data
          • Vue响应式之props
          • Vue响应式之computed
          • Vue响应式之watch
          • Vue响应式之异步更新与nextTick
          • Vue响应式之组件更新
      • 框架本质

    • 开发框架

    • 组件库

  • 工程能力

  • 应用基础

  • 专业领域

  • 业务场景

  • 大前端
  • 应用框架
  • UI 框架
  • Vue
  • 源码解析
gahing
2019-11-06
目录

Vue响应式原理

# 前言

本文开始进入 Vue 的核心部分:响应式处理

在看源码之前,我们先试着实现一个响应式系统

# 响应式实现

响应式的关键在于数据如何与视图进行绑定

假设我们有一个对象 data:{name:""} ,以及一个 input 节点,我们如何做到两者的绑定?

我们知道,监测对象数据变化可以用 Object.defineProperty ,监测 input 节点数据变化可以通过监听事件。因此我们可以很快的写出这样的代码

<!DOCTYPE html>
<html>

<head>
</head>

<body>
  <input id="input" />
</body>
<script>
  // 初始化
  var data = {
    name: "test"
  }
  var input = document.getElementById("input")
  input.value = data.name

  // 双向绑定
  input.addEventListener("input", function (e) {
    // input 数据变化 data 跟着变化
    data.name = e.target.value
  })
  function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
      get() {
        return val
      },
      set(newVal) {
        val = newVal
        // data 变化 input 跟着变化
        input.value = val
      }
    })
  }
  defineReactive(data, "name", data.name)

</script>

</html>
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

控制台上设置 name, input 会进行更新, 反过来一样。

实现的关键在于闭包, get/set 实际是对闭包变量 val 进行操作。

但是发现没有,data 数据变更和 dom 更新的处理耦合在一起了,如上input.value = val 只有 data.name 才会这样处理,其他属性变更不需要这个操作,因此 set 方法需要做的通用点。

引入发布订阅设计模式的一个经典例子 EventEmitter

class EventEmitter {
  constructor() {
    this.events = {}
  }
  on(key, callback) {
    this.events[key] = this.events[key] || []
    this.events[key].push(callback)
  }
  emit(key, val) {
    let events = this.events[key]

    if (!events) {
      return
    }
    for (let i = 0, m = events.length; i < m; i++) {
      events[i].call(null, val)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

其他代码修改如下

// 初始化
let data = {
  name: "test"
}
let input = document.getElementById("input")
let eventEmitter = new EventEmitter()

// 双向绑定
input.addEventListener("input", function (e) {
  // input 数据变化 data 跟着变化
  data.name = e.target.value
})


function defineReactive (obj, key, val) {
  Object.defineProperty(obj, key, {
    get () {
      return val
    },
    set (newVal) {
      val = newVal
      // 通知变更
      eventEmitter.emit(key, newVal)
    }
  })
}
defineReactive(data, "name", data.name)
eventEmitter.on("name", function (val) {
  console.log("data.name 值更新为", val)
  input.value = val
})
eventEmitter.emit("name", data.name)
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

新增一个绑定 name 的 dom 元素,进行 eventEmitter.on("name",callback) 即可

看上去好像可以用了,那这么写有什么弊端呢?

这里用了属性作为key,如果属性值为对象,该对象的属性被监听需要带上之前的属性值,举例

let data = {
  user:{
    name:""
  },
  ["user.name"]: ""
}
eventEmitter.on("user.name") // 这里监听的是哪个属性??
1
2
3
4
5
6
7

可能这可以通过寻找一个不常用的字符或者提示用户 data 不支持这么设置来解决。

另外,这里每个监听的回调应该由外部定义,方便后续取消监听。

我们改用观察者模式【详见后记,应该是发布-订阅模式】,每个属性是一个观察目标,一个目标可以有多个与之相依赖的观察者,这里观察者就是绑定属性更新视图或者 vue 的 compute 属性等的封装对象。当目标状态发生改变,通知所有的观察者进行更新。

维护观察目标依赖关系的实体类 Dep

// 依赖收集
class Dep {
  constructor() {
    // 观察者集合
    this.subs = []
  }
  // 添加观察者
  addSub (sub) {
    this.subs.push(sub)
  }
  // 移除观察者
  removeSub (sub) {
    this.subs.splice(this.subs.findIndex(watcher => watcher === sub), 1)
  }
  // 处理依赖
  depend () {
    // 调用观察者添加该依赖
    const watcher = Dep.target
    watcher.addDep(this)
    // 为该依赖添加观察者
    this.addSub(watcher)
  }
  // 通知观察者更新
  notify () {
    this.subs.forEach(watcher => {
      watcher.update()
    })
  }
}

// 临时存放观察者
Dep.target = null
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

观察者实体类 Watcher

// 观察者
class Watcher {
  /**
   * @param {Object} vm 上下文
   * @param {Function} getter 被监听属性的getter函数,通过 Object.getOwnPropertyDescriptor 获取
   * @param {Function} callback 触发更新时执行的操作
   */
  constructor(vm, getter, callback) {
    this.vm = vm
    this.deps = []
    this.getter = getter
    this.callback = callback
    // 手动触发 getter 绑定依赖
    this.get()
  }
  // 添加依赖
  addDep (dep) {
    this.deps.push(dep)
  }
  // 获取被监听属性的值
  get () {
    Dep.target = this
    // 触发被监听属性的 getter, 此时 Dep.target 非空,进行依赖处理
    let value = this.getter.call(this.vm)
    Dep.target = null
    return value
  }
  // 更新视图
  update () {
    let value = this.getter.call(this.vm)
    this.callback.call(this.vm, value)
  }
}
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

其他处理修改如下:

// 初始化
let vm = {
  data: {
    name: "test"
  }
}
let input = document.getElementById("input")

input.addEventListener("input", function (e) {
  // input 数据变化 data 跟着变化
  vm.data.name = e.target.value
})

// 观察目标监听
function defineReactive (obj, key, val) {
  // 创建一个闭包变量,处理观察目标 key 的依赖关系
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    get () {
      if (Dep.target) {
        dep.depend()
      }
      return val
    },
    set (newVal) {
      if (val === newVal) {
        return
      }
      val = newVal
      // 通知观察者更新
      dep.notify()
    }
  })
}

// 简单起见,这里我们只遍历一层
function observer (data) {
  if (!data || (typeof data !== 'object')) { return }
  Object.keys(data).forEach((key) => {
    defineReactive(data, key, data[key])
  })
}
observer(vm.data)
// 创建观察者
new Watcher(vm, Object.getOwnPropertyDescriptor(vm.data, "name").get, function (val) {
  console.log("data.name 更新:", val)
  input.value = val
})
// 初始化 input 的值
input.value = vm.data.name
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

完整版代码如下:

<!DOCTYPE html>
<html>

<head>
</head>

<body>
  <input id="input" />
  <input id="input1" />
</body>
<script>



  // 依赖收集
  class Dep {
    constructor() {
      // 观察者集合
      this.subs = []
    }
    // 添加观察者
    addSub(sub) {
      this.subs.push(sub)
    }
    // 移除观察者
    removeSub(sub) {
      this.subs.splice(this.subs.findIndex(watcher => watcher === sub), 1)
    }
    // 处理依赖
    depend() {
      // 调用观察者添加该依赖
      const watcher = Dep.target
      watcher.addDep(this)
      // 为该依赖添加观察者
      this.addSub(watcher)
    }
    // 通知观察者更新
    notify() {
      this.subs.forEach(watcher => {
        watcher.update()
      })
    }
  }

  // 临时存放观察者
  Dep.target = null

  // 观察者
  class Watcher {
    /**
     * @param {Object} vm 上下文
     * @param {Function} getter 被监听属性的getter函数,通过 Object.getOwnPropertyDescriptor 获取
     * @param {Function} callback 触发更新时执行的操作
     */
    constructor(vm, getter, callback) {
      this.vm = vm
      this.deps = []
      this.getter = getter
      this.callback = callback
      // 手动触发 getter 绑定依赖
      this.get()
    }
    // 添加依赖
    addDep(dep) {
      this.deps.push(dep)
    }
    // 获取被监听属性的值
    get() {
      Dep.target = this
      // 触发被监听属性的 getter, 此时 Dep.target 非空,进行依赖处理
      let value = this.getter.call(this.vm)
      Dep.target = null
      return value
    }
    // 更新视图
    update() {
      let value = this.getter.call(this.vm)
      this.callback.call(this.vm, value)
    }
  }

  // 初始化
  let vm = {
    data: {
      name: "test"
    }
  }
  let input = document.getElementById("input")

  input.addEventListener("input", function (e) {
    // input 数据变化 data 跟着变化
    vm.data.name = e.target.value
  })

  // 观察目标监听
  function defineReactive(obj, key, val) {
    // 创建一个闭包变量,处理观察目标 key 的依赖关系
    const dep = new Dep()
    Object.defineProperty(obj, key, {
      get() {
        if (Dep.target) {
          dep.depend()
        }
        return val
      },
      set(newVal) {
        if (val === newVal) {
          return
        }
        val = newVal
        // 通知观察者更新
        dep.notify()
      }
    })
  }

  // 简单起见,这里我们只遍历一层
  function observer(data) {
    if (!data || (typeof data !== 'object')) { return }
    Object.keys(data).forEach((key) => {
      defineReactive(data, key, data[key])
    })
  }
  observer(vm.data)
  // 创建观察者
  new Watcher(vm, Object.getOwnPropertyDescriptor(vm.data, "name").get, function (val) {
    console.log("data.name 更新:", val)
    input.value = val
  })
  // 初始化 input 的值
  input.value = vm.data.name


</script>

</html>
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
  1. 监听 data.name 的 get/set , 并在其中创建依赖收集 dep
  2. 创建观察者 watcher , 设置 Dep.target = watcher, 触发 data.name 的 get, 此时 Dep.target 有值, watcher 绑定依赖 dep
  3. 当 data.name 的 set 方法被调用时,dep 通知 watcher 进行更新,而 watcher 会去更新视图,如 input 的值

整个过程可以用一张图来描述

每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

这个就是 vue 响应式实现的简化版,下一篇我们从 data 源码入手,分析下 vue 的源码都做了哪些优化

# 后记

发布-订阅模式和观察者模式的最主要区别在于,当新增订阅者(观察者)的时候,发布者(观察目标)是否需要进行处理,前者是不进行处理的。

而我们根据上文的例子,响应式属性作为发布者(观察目标),当新增订阅者(观察者)Watcher 的时候,响应式属性并不需要进行变更,这一切都是通过 Dep 这个第三方进行的。

因此这整套设计是发布-订阅模式

# 参考资料

  1. 观察者模式 (opens new window)
  2. 深入响应式原理 (opens new window)
编辑 (opens new window)
上次更新: 2024/09/01, 23:56:56
Vue实例化过程
Vue响应式之data

← Vue实例化过程 Vue响应式之data→

最近更新
01
浅谈代码质量与量化指标
08-27
02
快速理解 JS 装饰器
08-26
03
Vue 项目中的 data-v-xxx 是怎么生成的
09-19
更多文章>
Theme by Vdoing | Copyright © 2016-2024 Gahing | 闽ICP备19024221号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式