1 数据驱动

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

2 响应式核心Vue2

用Object.defineProperty 作数据劫持,不支持ie8及以下

  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>Document</title>
  7. </head>
  8. <body>
  9. <div id="app"></div>
  10. <script>
  11. // 模拟 vue 中的 data
  12. let data = {
  13. msg: 'hello'
  14. }
  15. // 模拟 Vue 的实例
  16. let vm = {}
  17. // 数据劫持:当访问或者设置 vm 中的成员时,做一些干预操作
  18. Object.defineProperty(vm, 'msg', {
  19. // 可枚举(可遍历)
  20. enumerable: true,
  21. // 可配置(可以使用 delete 删除, 可以通过 defineProperty 重新定义)
  22. configurable: true,
  23. // 当获取值的时候执行
  24. get() {
  25. console.log('get: ', data.msg)
  26. return data.msg
  27. },
  28. set(value) {
  29. console.log('set: ', data.msg)
  30. if(data.msg === value) return
  31. data.msg = value;
  32. // 更新dom
  33. document.querySelector('#app').textContent = data.msg
  34. }
  35. })
  36. vm.msg = 'hello world';
  37. console.log(vm.msg)
  38. </script>
  39. </body>
  40. </html>

如果一个对象中有多个属性要转换,getter/setter怎么处理

  1. ...
  2. <div id="app"></div>
  3. <script>
  4. // 模拟 vue 中的 data
  5. let data = {
  6. msg: 'hello',
  7. count: 20
  8. }
  9. // 模拟 Vue 的实例
  10. let vm = {}
  11. proxyData(data)
  12. function proxyData(data) {
  13. Object.keys(data).forEach(key => {
  14. // 数据劫持:当访问或者设置 vm 中的成员时,做一些干预操作
  15. Object.defineProperty(vm, key, {
  16. // 可枚举(可遍历)
  17. enumerable: true,
  18. // 可配置(可以使用 delete 删除, 可以通过 defineProperty 重新定义)
  19. configurable: true,
  20. // 当获取值的时候执行
  21. get() {
  22. console.log('get: ', data[key])
  23. return data[key]
  24. },
  25. set(value) {
  26. console.log('set: ', data[key])
  27. if(data[key] === value) return
  28. data[key] = value;
  29. // 更新dom
  30. document.querySelector('#app').textContent = data[key]
  31. }
  32. })
  33. })
  34. }
  35. vm.msg = 'hello world';
  36. console.log(vm.msg)
  37. ...

3 响应式核心vue3

用 proxy 对象,直接监听对象而非属性,ES6中新增,ie不支持,性能由浏览器优化

  1. ...
  2. <div id="app"></div>
  3. <script>
  4. // 模拟 vue 中的 data
  5. let data = {
  6. msg: 'hello',
  7. count: 20
  8. }
  9. // 模拟 Vue 的实例
  10. let vm = new Proxy(data, {
  11. // 执行代理行为的函数
  12. get(target, key) {
  13. console.log('get, key:', key, target[key])
  14. return target[key]
  15. },
  16. set(target, key, value) {
  17. console.log('set, key:', key, target[key])
  18. if(target[key] === value) return
  19. target[key] = value
  20. document.querySelector('#app').textContent = target[key]
  21. }
  22. })
  23. vm.msg = 'hello world';
  24. console.log(vm.msg)
  25. ...

4 发布订阅模式

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

假设疫情期间,各地的疾控中心每天统计确诊病例,然后发布到疫情发布平台,我们就可以通过订阅疫情发布平台去获取疫情的信息,从而就可以知道周边有没有确认病例
vue 的自定义事件

  1. // 注册事件
  2. vm.$on('datachange', () => {
  3. console.log('data change')
  4. })
  5. vm.$on('datachange', () => {
  6. console.log('data change1')
  7. })
  8. // 触发事件
  9. vm.$emit('datachange')

兄弟组件的通信过程

  1. // eventBus.js
  2. // 事件中心
  3. let eventHub = new Vue()
  4. // ComponentA.vue
  5. // 发布者
  6. addTodo: function() {
  7. // 发布消息(事件)
  8. eventHub.$emit('add-todo', {text: this.newTodoText})
  9. this.newTodoText = ''
  10. }
  11. // ComponentB.vue
  12. // 订阅者
  13. created: function() {
  14. // 订阅消息(事件)
  15. eventHub.$on('add-todo', this.addTodo)
  16. }

模拟vue的事件机制

  1. class EventEmitter {
  2. constructor() {
  3. this.subs = Object.create(null)
  4. }
  5. $on(eventType, handler) {
  6. this.sub[eventType] = this.sub[eventType] || [];
  7. this.sub[eventType].push(handler)
  8. }
  9. $emit(eventType) {
  10. if(this.sub[eventType]) {
  11. this.sub[eventType].forEach( handler => {
  12. handler()
  13. })
  14. } else {
  15. console.log('事件未挂载')
  16. }
  17. }
  18. }
  19. let vm = new EventEmitter()
  20. vm.$on('click', function(){
  21. console.log('click1')
  22. })
  23. vm.$on('click', function(){
  24. console.log('click2')
  25. })
  26. vm.$emit('click')

5 观察者模式

  • 观察者(订阅者) watcher

update(): 当事件发生时,具体要做的事情

  • 目标(发布者)Dep
    • subs数组:存储所有的观察者
    • addSub(): 添加观察者
    • notify(): 当事件发生时,调用所有观察者的update 方法
  • 没有事件中心 ```javascript // 目标-发布者 class Dep { constructor() { this.subs = [] }

    addSub(sub) { if(sub && sub.update) {

    1. this.subs.push(sub)

    } }

    notify() { this.subs.forEach( sub => {

    1. sub.update()

    }) } }

// 观察者-订阅者 class Watcher { update() { console.log(‘update’) } }

let dep = new Dep() let watcher = new Watcher()

dep.addSub(watcher) dep.notify();

  1. 总结<br />观察者模式:由具体目标调度,比如当事件触发,Dep 就会去调用观察者的方法,所以观察者模式的发布者和订阅者是存在依赖关系的<br />发布订阅模式:由统一的调度中心调用,因此发布者和订阅者不需要知道对方的存在
  2. <a name="8uxVr"></a>
  3. #### 6 Vue 响应式原理模拟
  4. Vue类<br /> 功能
  5. - 负责接收初始化的参数
  6. - 负责把 data 中的属性注入到 Vue 的实例,转换成 getter/setter
  7. - 负责调用 observer 监听 data 中所有属性的变化
  8. - 负责调用 compiler 解析指令/插值表达式
  9. 结构
  10. - 类名 vue
  11. - 属性 $options $el $data
  12. - 方法 _proxyData()
  13. ```javascript
  14. // vue.js
  15. class Vue {
  16. constructor(options) {
  17. // 1 通过属性保存选项的数据
  18. this.$options = options || {}
  19. this.$data = options.data || {}
  20. this.$el = typeof options.el === 'string'? document.querySelector(options.el) : options.el
  21. // 2 把data 中的成员转换成getter/setter, 注入到 vue 的实例中
  22. this._proxyData(this.$data)
  23. // 3 调用 observer 对象,监听数据的变化
  24. // 4 调用 compiler 对象,解析指令和插值表达式
  25. }
  26. _proxyData(data){
  27. // 遍历data 中的所有属性
  28. Object.keys(data).forEach(key => {
  29. // 把 data 的属性注入到 vue 实例中
  30. Object.defineProperty(this, key, {
  31. enumerable: true,
  32. configurable: true,
  33. get () {
  34. return data[key]
  35. },
  36. set(newValue) {
  37. if(newValue === data[key]) {
  38. return
  39. }
  40. data[key] = newValue
  41. }
  42. })
  43. })
  44. }
  45. }

Observer类
功能

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

结构

  • walk(data) 方法,遍历属性
  • defineReactive(data,key,value) 通过调用Object.defineProperty方法把属性转换成getter、setter ```javascript class Observer { constructor(data) {

    1. this.walk(data);

    }

    walk(data) { // 1 判断data 是否对象 if(!data || typeof data !== ‘object’) {

    1. return

    } // 2 遍历对象的所有属性 Object.keys(data).forEach( key => {

    1. this.defineReactive(data, key, data[key])

    }) }

    defineReactive(obj, key, value) { let that = this // 如果 value 是对象,把对象的属性转换为响应式数据 this.walk(value) Object.defineProperty(obj, key, {

    1. enumerable: true,
    2. configurable: true,
    3. get () {
    4. // 此处不返回obj[key]的原因是 在vue.js中对obj有get方法,这样就会导致次递归循环调用,堆栈溢出
    5. // 此处在外部有调用就会形成闭包,所有可以拿到值
    6. return value
    7. },
    8. set (newValue) {
    9. if(newValue === value) {
    10. return
    11. }
    12. value = newValue
    13. // 赋值的是对象,把对象的属性也转换成响应式数据
    14. that.walk(newValue)
    15. // 发送通知
    16. }

    }) } }

  1. <a name="bXsDE"></a>
  2. #### 7 Compiler 类
  3. 功能
  4. - 负责编译模板、解析指令、插值表达式
  5. - 负责页面的首次渲染
  6. - 当数据变化时重新渲染视图
  7. 结构
  8. - el 存储模板
  9. - vm 存储实例
  10. - compiler(el)方法 编译模板、处理文本节点和元素节点
  11. - compilerElement(el)方法 编译元素节点、处理指令
  12. - compilerText(el)方法 编译文本节点、处理插值表达式
  13. - isDirective(attrName)方法 判断元素是否是指令
  14. - isTextNode(node)方法 判断节点是否是文本节点
  15. - isElementNode(node)方法 判断节点是否是元素节点
  16. 在new Compiler 的时候,传入了 vue 的实例,在compiler 构造函数内部存储实例和实例的$el,然后调用编译的方法,在编译的方法中根据传入的$el,去遍历它的所有子节点,然后根据节点类型是元素节点还是文本节点去执行对应的编译方法,执行完毕后判断遍历的当前节点是否有子节点,再递归调用。编译文本节点,通过正则表达式,获取到插值表达式的name,然后根据name去找实例的属性对应的值,替换进节点中;编译元素节点,遍历节点的所有属性,判断属性是否是指令,根据不同指令执行不同的编译方法。
  17. ```javascript
  18. class Complier {
  19. constructor(vm) {
  20. this.el = vm.$el
  21. this.vm = vm
  22. this.complier(this.el)
  23. }
  24. // 编译模板,处理文本节点和元素节点
  25. complier (el) {
  26. let childNodes = el.childNodes
  27. Array.from(childNodes).forEach( node => {
  28. if(this.isTextNode(node)) {
  29. this.compilerText(node)
  30. } else if(this.isElementNode(node)) {
  31. this.complierElement(node)
  32. }
  33. // 判断node 节点是否有子节点,如果有子节点,要递归调用compiler
  34. if(node.childNodes && node.childNodes.length) {
  35. this.complier(node)
  36. }
  37. })
  38. }
  39. // 编译元素节点,处理指令
  40. complierElement (node) {
  41. // console.log(node.attributes)
  42. // 遍历所有属性节点
  43. Array.from(node.attributes).forEach( attr => {
  44. // 判断是否是指令
  45. let attrName = attr.name
  46. if(this.isDirective(attrName)) {
  47. // v-text => text
  48. attrName = attrName.substr(2)
  49. let key = attr.value
  50. this.update(node, key, attrName)
  51. }
  52. })
  53. }
  54. // 调用指令的方法
  55. update (node, key, attrName) {
  56. let updateFn = this[attrName + 'Updater']
  57. updateFn && updateFn(node, this.vm[key])
  58. }
  59. // 处理 v-text 指令
  60. textUpdater (node, value) {
  61. node.textContent = value
  62. }
  63. // 处理 v-model 指令
  64. modelUpdater (node, value) {
  65. node.value = value
  66. }
  67. // 编译文本节点,处理插值表达式
  68. compilerText (node) {
  69. // console.dir(node)
  70. let reg = /\{\{(.+?)\}\}/
  71. let value = node.textContent
  72. if(reg.test(value)) {
  73. let key = RegExp.$1.trim()
  74. node.textContent = value.replace(reg, this.vm[key])
  75. }
  76. }
  77. // 判断元素属性是指令
  78. isDirective (attrName) {
  79. return attrName.startsWith('v-')
  80. }
  81. // 判断节点是否是文本节点
  82. isTextNode (node) {
  83. return node.nodeType === 3
  84. }
  85. // 判断节点是否是元素节点
  86. isElementNode (node) {
  87. return node.nodeType === 1
  88. }
  89. }

8 Dep(Dependency)类

功能

  • 收集依赖,添加观察者
  • 通知所有观察者
  • subs 存储所有观察者 watcher
  • addSub(sub)方法 添加watcher
  • notify() 数据变化时通知所有的观察者

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

    9 Watcher 类

    功能

  • 当数据变化触发依赖, dep 通知所有的 watcher 实例更新视图

  • 自身实例化的时候往 dep 对象中添加自己

结构

  • vm 实例
  • key 属性名称
  • cb 回调函数,如何更新视图
  • oldValue 记录数据变化之前的值
  • 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. // 把target 设为空,防止重复渲染
    13. Dep.target = null
    14. }
    15. // 当数据发生变化的时候更新视图
    16. update () {
    17. let newValue = this.vm[this.key]
    18. if(this.oldValue === newValue) {
    19. return
    20. }
    21. this.cb(newValue)
    22. }
    23. }

    添加 watcher 到文本节点编译方法

    1. // compiler.js
    2. ...
    3. compilerText (node) {
    4. ...
    5. // 创建 watcher 对象,数据改变时更新视图
    6. new Watcher(this.vm, key, newValue => {
    7. node.textContent = newValue
    8. })
    9. ...
    10. }
    11. ...

    添加 watcher 到元素节点

    1. ...
    2. // 调用指令的方法
    3. update (node, key, attrName) {
    4. let updateFn = this[attrName + 'Updater']
    5. updateFn && updateFn.call(this, node, this.vm[key], key)
    6. }
    7. // 处理 v-text 指令
    8. textUpdater (node, value, key) {
    9. node.textContent = value
    10. new Watcher(this.vm, key, newValue => {
    11. node.textContent = newValue
    12. })
    13. }
    14. // 处理 v-model 指令
    15. modelUpdater (node, value, key) {
    16. node.value = value
    17. new Watcher(this.vm, key, newValue => {
    18. node.value = newValue
    19. })
    20. }
    21. ...

    10 双向绑定

    通过给model指令处理函数绑定事件来更新 data 数据

    1. ...
    2. // 处理 v-model 指令
    3. modelUpdater (node, value, key) {
    4. ...
    5. // 双向绑定
    6. node.addEventListener('input', () => {
    7. this.vm[key] = node.value
    8. })
    9. }
    10. ...

    11 调试首次渲染

    1 new Vue 处断点,进入vue.js 的构造函数中,存储选项中到属性,遍历data 设置为setter/getter存储到属性中
    2 vue.js 实例化 Observer 对象,构造函数中传入data,observer 中遍历data 对象所有属性设为getter/setter,在每个属性遍历的时候新建 dep 实例,在getter 中如果 Dep 有target 就把 Dep 的target 添加到 dep 实例中的subs 中,在setter 中,递归遍历属性,并且调用 dep 的通知方法,使每个 subs 中的 watcher 都调用 update;
    3 vue.js 实例化 Compiler 对象,构造函数中传入 vue 实例,调用 compiler 方法编译模板,根据 vue 实例的 el,递归遍历所有子节点,根据不同的节点类型执行不同的编译函数;
    4 compiler.js 在节点编译函数中新建 Water 的实例,Watcher 的构造函数中传入当前节点,编译的 data 对应的 key,和数据更新后的回调函数;在 Watcher 构造函数中,把 watcher 的实例挂载到 Dep 对象的target 上,然后在存储旧值时,触发 vue 中data 某属性的 getter,接着会触发 observer 中 data 某属性的 getter;在 observer 的get 中,此时 Dep 有target,就会把 target 上的 watcher 放入 Dep实例的 subs 数组中; watcher 实例在构造函数的做完这些后销毁 target;
    5 至此,首次渲染完成

    12 调试数据改变

    1 在 observer 中的set 方法中 dep 发送通知的地方断点,此时dep 中的 notify 执行,变量 dep 实例中的watcher 组成的 subs 数组,调用每个 watcher 的 update 方法
    2 watcher.js 中的update 方法会判断旧值和新值是否相等,不相等则调用watcher 的回调函数,回调函数是在 compiler 中定义的
    3 compiler 中定义的回调函数执行,更新节点
    4 至此,数据变化视图变化的逻辑完成

    13 总结

    问题:

  • 给属性重新赋值成对象,是否是响应式的

属性重新赋值会走 observer 的 set 方法,set 方法中对于新的值会递归设置成响应式的数据

  • 给Vue 实例新增一个成员是否是响应式的

vue实例成员变成响应式是在new 一个 vue 实例时做的,在new 完实例以后,再去新增成员显然不会变成响应式的。在vue 官网文档中提及,vue 不允许添加根级别的响应式属性,但是可以使用 Vue.set(vm.someObject, ‘b’, 2)来添加嵌套对象的响应式属性