相关概念

数据驱动

学习 Vue 的时候经常能看到三个词:数据响应式、双向绑定、数据驱动。

  • 数据响应式
    • 数据模型仅仅是普通的 JavaScript 对象,而当我们改变数据的时候,视图会进行更新,避免了繁琐的 DOM 操作,提高开发效率。
  • 双向绑定
    • 数据改变,视图改变;视图改变,数据改变。
    • 可以使用 v-model 在表单元素上创建双向数据绑定。
  • 数据驱动
    • Vue 最独特的特性之一,开发过程仅需要关注数据本身,不需要关心数据是如何渲染到视图的。

数据响应式的核心原理

Vue2.x

官方介绍:当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
用 Object.defineProperty 定义的对象的属性,如果实现它的 getter 函数,在访问该属性时,会调用此函数;如果实现它的 setter 函数,当属性值被修改时,会调用此函数,函数被接收新值作为参数。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  7. <title>defineProperty 多个成员</title>
  8. </head>
  9. <body>
  10. <div id="app">
  11. hello
  12. </div>
  13. <script>
  14. // 模拟 Vue 中的 data 选项
  15. let data = {
  16. msg: 'hello',
  17. count: 10
  18. }
  19. // 模拟 Vue 的实例
  20. let vm = {}
  21. proxyData(data)
  22. function proxyData(data) {
  23. // 遍历 data 对象的所有属性
  24. Object.keys(data).forEach(key => {
  25. // 把 data 中的属性,转换成 vm 的 setter/setter
  26. Object.defineProperty(vm, key, {
  27. enumerable: true,
  28. configurable: true,
  29. get () {
  30. console.log('get: ', key, data[key])
  31. return data[key]
  32. },
  33. set (newValue) {
  34. console.log('set: ', key, newValue)
  35. if (newValue === data[key]) {
  36. return
  37. }
  38. data[key] = newValue
  39. // 数据更改,更新 DOM 的值
  40. document.querySelector('#app').textContent = data[key]
  41. }
  42. })
  43. })
  44. }
  45. // 测试
  46. vm.msg = 'Hello World'
  47. console.log(vm.msg)
  48. </script>
  49. </body>
  50. </html>

Vue3

官方介绍:当我们从一个组件的 data 函数中返回一个普通的 JavaScript 对象时,Vue 会将该对象包裹在一个带有 get 和 set 处理程序的 Proxy 中。Proxy 是在 ES6 中引入的,它使 Vue 3 避免了 Vue 早期版本中存在的一些响应性问题。Proxy 是一个对象,它包装了另一个对象,并允许你拦截对该对象的任何交互。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  7. <title>Proxy</title>
  8. </head>
  9. <body>
  10. <div id="app">
  11. hello
  12. </div>
  13. <script>
  14. // 模拟 Vue 中的 data 选项
  15. let data = {
  16. msg: 'hello',
  17. count: 0
  18. }
  19. // 模拟 Vue 实例
  20. let vm = new Proxy(data, {
  21. // 执行代理行为的函数
  22. // 当访问 vm 的成员会执行
  23. get (target, key) {
  24. console.log('get, key: ', key, target[key])
  25. return target[key]
  26. },
  27. // 当设置 vm 的成员会执行
  28. set (target, key, newValue) {
  29. console.log('set, key: ', key, newValue)
  30. if (target[key] === newValue) {
  31. return
  32. }
  33. target[key] = newValue
  34. document.querySelector('#app').textContent = target[key]
  35. }
  36. })
  37. // 测试
  38. vm.msg = 'Hello World'
  39. console.log(vm.msg)
  40. </script>
  41. </body>
  42. </html>

发布/订阅模式和观察者模式

发布/订阅模式

发布订阅模式有三个角色:发布者、订阅者、信号中心。发布者不会直接把信息发给订阅者,而是传给信号中心,由信号中心按需发给订阅者。

  1. <!DOCTYPE html>
  2. <html lang="cn">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>发布订阅模式</title>
  7. </head>
  8. <body>
  9. <script>
  10. // 事件触发器
  11. class EventEmitter {
  12. constructor () {
  13. // { 'click': [fn1, fn2], 'change': [fn] }
  14. this.subs = Object.create(null)
  15. }
  16. // 注册事件(订阅消息) 的方法
  17. $on (eventType, handler) {
  18. this.subs[eventType] = this.subs[eventType] || []
  19. this.subs[eventType].push(handler)
  20. }
  21. // 触发事件(发布消息) 的方法
  22. $emit (eventType) {
  23. if (this.subs[eventType]) {
  24. this.subs[eventType].forEach(handler => {
  25. handler()
  26. })
  27. }
  28. }
  29. }
  30. // 测试
  31. let em = new EventEmitter()
  32. em.$on('click', () => {
  33. console.log('click1')
  34. })
  35. em.$on('click', () => {
  36. console.log('click2')
  37. })
  38. em.$emit('click') // click1 click2
  39. </script>
  40. </body>
  41. </html>

观察者模式

观察者模式没有中间的事件中心,只有发布者和观察者,并且发布者需要知道观察者的存在。所以发布者要存储观察者,当发布信息时,调用方法通知观察者。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>观察者模式</title>
  7. </head>
  8. <body>
  9. <script>
  10. // 发布者-目标
  11. class Dep {
  12. constructor () {
  13. // 记录所有的订阅者
  14. this.subs = []
  15. }
  16. // 添加订阅者
  17. addSub (sub) {
  18. if (sub && sub.update) {
  19. this.subs.push(sub)
  20. }
  21. }
  22. // 发布通知
  23. notify () {
  24. this.subs.forEach(sub => {
  25. sub.update()
  26. })
  27. }
  28. }
  29. // 订阅者-观察者
  30. class Watcher {
  31. update () {
  32. console.log('update')
  33. }
  34. }
  35. // 测试
  36. let dep = new Dep()
  37. let watcher = new Watcher()
  38. dep.addSub(watcher)
  39. dep.notify()
  40. </script>
  41. </body>
  42. </html>

总结

  • 观察者模式是由具体目标调度,比如当事件触发,Dep 就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的。
  • 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。

image.png

分析

Vue 的基本结构

首先 new 一个 Vue 实例,构造函数接收一个对象,这个对象有 el 和 data 属性。

  1. <!DOCTYPE html>
  2. <html lang="cn">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  7. <title>Vue 基础结构</title>
  8. </head>
  9. <body>
  10. <div id="app">
  11. <h1>插值表达式</h1>
  12. <h3>{{ msg }}</h3>
  13. <h3>{{ count }}</h3>
  14. <h1>v-text</h1>
  15. <div v-text="msg"></div>
  16. <h1>v-model</h1>
  17. <input type="text" v-model="msg">
  18. <input type="text" v-model="count">
  19. </div>
  20. <script src="./js/vue.js"></script>
  21. <script>
  22. let vm = new Vue({
  23. el: '#app',
  24. data: {
  25. msg: 'Hello Vue',
  26. count: 20,
  27. items: ['a', 'b', 'c']
  28. }
  29. })
  30. </script>
  31. </body>
  32. </html>

打印出这个 Vue 实例,我们要实现的是在实例上挂载一些属性:

  1. data中的成员以及它们的 getter 和 setter 函数。
  2. $data:这个对象里面存放了原来 data 中的成员以及它们的 getter 和 setter,这里的 setter 是真正监视数据变化的函数。
  3. $options:记录传入 Vue 构造函数的参数。
  4. $el:传入的参数中的 el 可以是选择器,也可以是 DOM 对象。$el 是一个 DOM 对象,如果传入的是选择器,需要获取对应的 DOM 对象。

一个最小版本的 Vue 的整体结构:

image.png

  • Vue
    • 把 data 中的成员注入到 Vue 实例,并且把 data 中的成员转成 getter/setter
  • Observer
    • 监听 data 中的所有属性,如有变动可拿到最新值并通知 Dep
  • Compiler
    • 解析每个元素中的指令/插值表达式,并替换成相应的数据
  • Dep
    • 添加观察者,当数据变化时通知观察者
  • Watcher
    • 数据变化时更新视图

Vue

功能

  • 负责接收初始化的参数(选项)
  • 负责把 data 的属性注入到 Vue 实例中,转换成 getter/setter
  • 负责调用 observer 监听 data 中所有属性的变化
  • 负责调用 compiler 解析指令/插值表达式

结构

  1. Vue
  2. ------------
  3. + $options
  4. + $el
  5. + $data
  6. -------------
  7. - _proxyData()

实现

  1. class Vue {
  2. constructor (options) {
  3. // 1. 通过属性保存选项的数据
  4. this.$options = options || {}
  5. this.$data = options.data || {}
  6. this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
  7. // 2. 把data中的成员转换成getter和setter,并注入到vue实例中
  8. this._proxyData(this.$data)
  9. // 3. 调用observer对象,监听数据的变化
  10. new Observer(this.$data)
  11. // 4. 调用compiler对象,解析指令和差值表达式
  12. new Compiler(this)
  13. }
  14. _proxyData (data) {
  15. // 遍历data中的所有属性
  16. Object.keys(data).forEach(key => {
  17. // 把data的属性注入到vue实例中
  18. Object.defineProperty(this, key, {
  19. enumerable: true,
  20. configurable: true,
  21. get () {
  22. return data[key]
  23. },
  24. set (newValue) {
  25. if (newValue === data[key]) {
  26. return
  27. }
  28. data[key] = newValue
  29. }
  30. })
  31. })
  32. }
  33. }

Observer

功能

  • 负责把 data 选项中的属性转换成响应式数据
  • data 中的某个属性也是对象,把该属性也转换成响应式数据
  • 数据变化发送通知

结构

  1. Observer
  2. ---------------
  3. + walk(data)
  4. + defineReactive(data, key, value)

实现

  1. class Observer {
  2. constructor (data) {
  3. this.walk(data)
  4. }
  5. walk (data) {
  6. // 1. 判断data是否是对象
  7. if (!data || typeof data !== 'object') {
  8. return
  9. }
  10. // 2. 遍历data对象的所有属性
  11. Object.keys(data).forEach(key => {
  12. this.defineReactive(data, key, data[key])
  13. })
  14. }
  15. defineReactive (obj, key, val) {
  16. let that = this
  17. // 负责收集依赖,并发送通知
  18. let dep = new Dep()
  19. // 如果val是对象,把val内部的属性转换成响应式数据
  20. this.walk(val)
  21. Object.defineProperty(obj, key, {
  22. enumerable: true,
  23. configurable: true,
  24. get () {
  25. // 收集依赖
  26. Dep.target && dep.addSub(Dep.target)
  27. return val
  28. },
  29. set (newValue) {
  30. if (newValue === val) {
  31. return
  32. }
  33. val = newValue
  34. that.walk(newValue) // 1. 这里直接使用this指向的是data 2.当值从一个基础类型改成对象,对象里的属性也要转换 getter/setter
  35. // 发送通知
  36. dep.notify()
  37. }
  38. })
  39. }
  40. }

Compiler

功能

  • 负责编译模板,解析指令/插值表达式
  • 负责页面的首次渲染
  • 当数据变化后重新渲染视图

结构

  1. Compiler
  2. ---------------
  3. + el
  4. + vm
  5. --------------
  6. + compile(el)
  7. + compileElement(node)
  8. + compileText(node)
  9. + isDirective(attrName)
  10. + isTextNode(node)
  11. + isElementNode(node)

实现

  1. class Compiler {
  2. constructor (vm) {
  3. this.el = vm.$el
  4. this.vm = vm
  5. this.compile(this.el)
  6. }
  7. // 编译模板,处理文本节点和元素节点
  8. compile (el) {
  9. let childNodes = el.childNodes
  10. Array.from(childNodes).forEach(node => {
  11. // 处理文本节点
  12. if (this.isTextNode(node)) {
  13. this.compileText(node)
  14. } else if (this.isElementNode(node)) {
  15. // 处理元素节点
  16. this.compileElement(node)
  17. }
  18. // 判断node节点,是否有子节点,如果有子节点,要递归调用compile
  19. if (node.childNodes && node.childNodes.length) {
  20. this.compile(node)
  21. }
  22. })
  23. }
  24. // 编译元素节点,处理指令
  25. compileElement (node) {
  26. // console.log(node.attributes)
  27. // 遍历所有的属性节点
  28. Array.from(node.attributes).forEach(attr => {
  29. // 判断是否是指令
  30. let attrName = attr.name
  31. if (this.isDirective(attrName)) {
  32. // v-text --> text
  33. attrName = attrName.substr(2)
  34. let key = attr.value
  35. this.update(node, key, attrName)
  36. }
  37. })
  38. }
  39. update (node, key, attrName) {
  40. let updateFn = this[attrName + 'Updater']
  41. updateFn && updateFn.call(this, node, this.vm[key], key)
  42. }
  43. // 处理 v-text 指令
  44. textUpdater (node, value, key) {
  45. node.textContent = value
  46. new Watcher(this.vm, key, (newValue) => {
  47. node.textContent = newValue
  48. })
  49. }
  50. // v-model
  51. modelUpdater (node, value, key) {
  52. node.value = value
  53. new Watcher(this.vm, key, (newValue) => {
  54. node.value = newValue
  55. })
  56. // 双向绑定
  57. node.addEventListener('input', () => {
  58. this.vm[key] = node.value
  59. })
  60. }
  61. // 编译文本节点,处理差值表达式
  62. compileText (node) {
  63. // console.dir(node)
  64. // {{ msg }}
  65. let reg = /\{\{(.+?)\}\}/
  66. let value = node.textContent
  67. if (reg.test(value)) {
  68. let key = RegExp.$1.trim()
  69. node.textContent = value.replace(reg, this.vm[key])
  70. // 创建watcher对象,当数据改变更新视图
  71. new Watcher(this.vm, key, (newValue) => {
  72. node.textContent = newValue
  73. })
  74. }
  75. }
  76. // 判断元素属性是否是指令
  77. isDirective (attrName) {
  78. return attrName.startsWith('v-')
  79. }
  80. // 判断节点是否是文本节点
  81. isTextNode (node) {
  82. return node.nodeType === 3
  83. }
  84. // 判断节点是否是元素节点
  85. isElementNode (node) {
  86. return node.nodeType === 1
  87. }
  88. }

Dep

image.png

功能

  • 收集依赖,添加观察者(watcher)
  • 通知所有观察者

结构

  1. Dep
  2. ---------------
  3. + subs
  4. --------------
  5. + addSub(sub)
  6. + notify()

实现

  1. class Dep {
  2. constructor () {
  3. // 存储所有的观察者
  4. this.subs = []
  5. }
  6. // 添加观察者
  7. addSub (sub) {
  8. if (sub && sub.update) {
  9. this.subs.push(sub)
  10. }
  11. }
  12. // 发送通知
  13. notify () {
  14. this.subs.forEach(sub => {
  15. sub.update()
  16. })
  17. }
  18. }

Wacther

image.png

功能

  • 当数据变化触发依赖,dep 通知所有的 Watcher 实例更新视图
  • 自身实例化的时候往 dep 对象中添加自己

结构

  1. Watcher
  2. ---------------
  3. + vm
  4. + key
  5. + cb
  6. + oldValue
  7. --------------
  8. + update()


实现

  1. class Watcher {
  2. constructor (vm, key, cb) {
  3. this.vm = vm
  4. // data中的属性名称
  5. this.key = key
  6. // 回调函数负责更新视图
  7. this.cb = cb
  8. // 把watcher对象记录到Dep类的静态属性target
  9. Dep.target = this
  10. // 触发get方法,在get方法中会调用addSub
  11. this.oldValue = vm[key]
  12. Dep.target = null
  13. }
  14. // 当数据发生变化的时候更新视图
  15. update () {
  16. let newValue = this.vm[this.key]
  17. if (this.oldValue === newValue) {
  18. return
  19. }
  20. this.cb(newValue)
  21. }
  22. }

总结

image.png

  • Vue
    • 记录传入的选项,设置 $data/$el
    • 把 data 的成员注入到 Vue 实例
    • 负责调用 Observer 实现数据响应式处理(数据劫持)
    • 负责调用 Compiler 编译指令/插值表达式等
  • Observer
    • 数据劫持
      • 负责把 data 中的成员转换成 getter/setter
      • 负责把多层属性转换成 getter/setter
      • 如果给属性赋值为新对象,把新对象的成员设置为 getter/setter
    • 添加 Dep 和 Watcher 的依赖关系
    • 数据变化发送通知
  • Compiler
    • 负责编译模板,解析指令/插值表达式
    • 负责页面的首次渲染过程
    • 当数据变化后重新渲染
  • Dep
    • 收集依赖,添加订阅者(watcher)
    • 通知所有订阅者
  • Watcher
    • 自身实例化的时候往dep对象中添加自己
    • 当数据变化dep通知所有的 Watcher 实例更新视图

Demo

  1. <!DOCTYPE html>
  2. <html lang="cn">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  7. <title>Mini Vue</title>
  8. </head>
  9. <body>
  10. <div id="app">
  11. <h1>差值表达式</h1>
  12. <h3>{{ msg }}</h3>
  13. <h3>{{ count }}</h3>
  14. <h1>v-text</h1>
  15. <div v-text="msg"></div>
  16. <h1>v-model</h1>
  17. <input type="text" v-model="msg">
  18. <input type="text" v-model="count">
  19. </div>
  20. <script src="./js/dep.js"></script>
  21. <script src="./js/watcher.js"></script>
  22. <script src="./js/compiler.js"></script>
  23. <script src="./js/observer.js"></script>
  24. <script src="./js/vue.js"></script>
  25. <script>
  26. let vm = new Vue({
  27. el: '#app',
  28. data: {
  29. msg: 'Hello Vue',
  30. count: 100,
  31. person: { name: 'zs' }
  32. }
  33. })
  34. console.log(vm.msg)
  35. // vm.msg = { test: 'Hello' }
  36. vm.test = 'abc'
  37. </script>
  38. </body>
  39. </html>