当前位置:   article > 正文

Vue核心知识点 -Vue2响应式系统是基于什么实现的、以及会产生什么问题和解决方案

Vue核心知识点 -Vue2响应式系统是基于什么实现的、以及会产生什么问题和解决方案

一、概念

        在Vue 2中,响应式系统是基于Object.defineProperty实现的。它通过劫持对象的属性来实现数据的响应式更新。

        当你将一个对象传递给Vue实例的data选项时,Vue会遍历对象的每个属性,并使用Object.defineProperty方法将其转换为getter和setter。这样一来,当你访问或修改这些属性时,Vue能够捕获到这些操作并触发相应的更新。

        具体而言,Object.defineProperty方法用于定义一个对象的新属性,或者修改对象的现有属性。通过在属性上设置getter和setter,我们可以监听属性的读取和修改行为,并在这些行为发生时执行相应的操作。

        Vue的响应式系统利用了这一特性,在getter中收集依赖(即追踪属性的订阅者),在setter中触发更新(通知订阅者进行响应式更新)。

二、产生的问题

        1、Vue的响应式系统仅对已经存在的属性进行劫持,而不会劫持对象的整个原型链。这意味着如果你向一个已经创建的对象直接添加新属性或删除属性,Vue无法检测到这些更改

        2、由于数组本质上是对象,Vue可以通过Object.definePropertyProxy拦截对象的属性访问和修改。但是,对于数组而言,直接修改索引对应的值(如果对应的值是对象数据、指的是修改整条对象数据、即将一整个对象赋值给当前索引的对象数据,修改当前索引值对应的值​​​​​​对象的某个属性是会触发dom更新的)并不会触发属性的setter,因此Vue无法监测到这种变化

三、产生问题的场景和解决方案

场景1:

直接为data里的对象通过赋值的方式添加属性和delete删除属性、控制台打印数据添加和删除属性成功、但是页面未能渲染最新数据、即未触发dom的更新

演示代码

  1. <template>
  2. <div class="home">
  3. <h3>这是vue2测试页面</h3>
  4. <p>用户信息: {{ userMsg }} </p>
  5. <p><button @click="add">添加用户性别为男</button></p>
  6. <p><button @click="change">修改用户年龄为18</button></p>
  7. <p><button @click="deleteH">删除用户身高信息</button></p>
  8. </div>
  9. </template>
  10. <script>
  11. export default {
  12. data() {
  13. return {
  14. // 用户信息
  15. userMsg: {
  16. name: 'zs',
  17. age: '20',
  18. height: '180',
  19. }
  20. }
  21. },
  22. methods: {
  23. // 为对象直接添加数据
  24. add() {
  25. this.userMsg.sex = '男'
  26. console.log('添加性别后')
  27. console.log(this.userMsg)
  28. },
  29. change() {
  30. this.userMsg.age = '18'
  31. console.log('修改数据年龄后')
  32. console.log(this.userMsg)
  33. },
  34. deleteH() {
  35. delete this.userMsg.height
  36. console.log('删除身高数据后')
  37. console.log(this.userMsg)
  38. }
  39. },
  40. }
  41. </script>
  42. <style>
  43. .home{
  44. margin: 0 auto;
  45. text-align: center;
  46. }
  47. </style>

图示

点击了添加用户性别为男按钮-数据已经添加 dom元素未更新

 点击了删除用户身高信息按钮-数据已经修改 dom元素未更新

解决方案

方案一: 浅拷贝要操作对象数据给一个新对象、对新对象操作、然后将新对象赋值给要操作的对象数据。
原理:

        这些操作都是对操作对象进行重新赋值,Vue 可以检测到这种赋值操作,并且会重新渲染相关的 DOM 元素,因此你看到了 DOM 元素能够更新。

代码
  1. <template>
  2. <div class="home">
  3. <h3>这是vue2测试页面</h3>
  4. <p>用户信息: {{ userMsg }} </p>
  5. <p><button @click="add">添加用户性别为男</button></p>
  6. <p><button @click="change">修改用户年龄为18</button></p>
  7. <p><button @click="deleteH">删除用户身高信息</button></p>
  8. </div>
  9. </template>
  10. <script>
  11. export default {
  12. data() {
  13. return {
  14. // 用户信息
  15. userMsg: {
  16. name: 'zs',
  17. age: '20',
  18. height: '180',
  19. }
  20. }
  21. },
  22. methods: {
  23. // 为对象直接添加数据
  24. add() {
  25. // this.userMsg.sex = '男'
  26. let newUserMsg = { ...this.userMsg }
  27. newUserMsg.sex = '男'
  28. this.userMsg = newUserMsg
  29. console.log('添加性别后')
  30. console.log(this.userMsg)
  31. },
  32. change() {
  33. this.userMsg.age = '18'
  34. console.log('修改数据年龄后')
  35. console.log(this.userMsg)
  36. },
  37. deleteH() {
  38. let newUserMsg = { ...this.userMsg }
  39. delete newUserMsg.height
  40. this.userMsg = newUserMsg
  41. console.log('删除身高数据后')
  42. console.log(this.userMsg)
  43. }
  44. },
  45. }
  46. </script>
  47. <style>
  48. .home{
  49. margin: 0 auto;
  50. text-align: center;
  51. }
  52. </style>
效果
点击了添加用户性别为男按钮-数据已经添加 dom元素更新

 点击了删除用户身高信息按钮-数据已经修改 dom元素更新
方案二:使用 this.$set(); 来添加对象属性、使用this.$delete();来删除属性。
原理:
  • Vue.set(obj, key, value):将响应式对象 obj 的属性 key 设置为 value,如果 obj 是响应式的,则确保这个属性也是响应式的,并触发视图更新。
  • Vue.delete(obj, key):删除响应式对象 obj 的属性 key,并触发视图更新。
代码
  1. <template>
  2. <div class="home">
  3. <h3>这是vue2测试页面</h3>
  4. <p>用户信息: {{ userMsg }} </p>
  5. <p><button @click="add">添加用户性别为男</button></p>
  6. <p><button @click="change">修改用户年龄为18</button></p>
  7. <p><button @click="deleteH">删除用户身高信息</button></p>
  8. </div>
  9. </template>
  10. <script>
  11. export default {
  12. data() {
  13. return {
  14. // 用户信息
  15. userMsg: {
  16. name: 'zs',
  17. age: '20',
  18. height: '180',
  19. }
  20. }
  21. },
  22. methods: {
  23. // 为对象直接添加数据
  24. add() {
  25. // this.userMsg.sex = '男'
  26. // let newUserMsg = { ...this.userMsg }
  27. // newUserMsg.sex = '男'
  28. // this.userMsg = newUserMsg
  29. this.$set(this.userMsg, 'sex', '男');
  30. console.log('添加性别后')
  31. console.log(this.userMsg)
  32. },
  33. change() {
  34. this.userMsg.age = '18'
  35. console.log('修改数据年龄后')
  36. console.log(this.userMsg)
  37. },
  38. deleteH() {
  39. // let newUserMsg = { ...this.userMsg }
  40. // delete newUserMsg.height
  41. // this.userMsg = newUserMsg
  42. this.$delete(this.userMsg, 'height');
  43. console.log('删除身高数据后')
  44. console.log(this.userMsg)
  45. }
  46. },
  47. }
  48. </script>
  49. <style>
  50. .home{
  51. margin: 0 auto;
  52. text-align: center;
  53. }
  54. </style>
 效果
点击了添加用户性别为男按钮-数据已经添加 dom元素更新

 点击了删除用户身高信息按钮-数据已经修改 dom元素更新

场景2:

直接为data里的数组通过直接修改索引对应的值,页面未能渲染最新数据、即未触发dom的更新

演示代码

  1. <template>
  2. <div class="home">
  3. <h3>这是vue2测试页面</h3>
  4. <!-- <p>用户信息: {{ userMsg }} </p> -->
  5. <p>用户家人信息: {{ userMsg.family }} </p>
  6. <!-- <p><button @click="add">添加用户性别为男</button></p>
  7. <p><button @click="change">修改用户年龄为18</button></p>
  8. <p><button @click="deleteH">删除用户身高信息</button></p> -->
  9. <p><button @click="changeFather">修改父亲信息</button></p>
  10. </div>
  11. </template>
  12. <script>
  13. export default {
  14. data() {
  15. return {
  16. // 用户信息
  17. userMsg: {
  18. name: 'zs',
  19. age: '20',
  20. height: '180',
  21. family: [{ name: 'father', age: '45', sex: '男' }, { name: 'mother', age: '42', sex: '女' }]
  22. }
  23. }
  24. },
  25. methods: {
  26. // 为对象直接添加数据
  27. add() {
  28. // this.userMsg.sex = '男'
  29. // let newUserMsg = { ...this.userMsg }
  30. // newUserMsg.sex = '男'
  31. // this.userMsg = newUserMsg
  32. this.$set(this.userMsg, 'sex', '男');
  33. console.log('添加性别后')
  34. console.log(this.userMsg)
  35. },
  36. change() {
  37. this.userMsg.age = '18'
  38. console.log('修改数据年龄后')
  39. console.log(this.userMsg)
  40. },
  41. deleteH() {
  42. // let newUserMsg = { ...this.userMsg }
  43. // delete newUserMsg.height
  44. // this.userMsg = newUserMsg
  45. this.$delete(this.userMsg, 'height');
  46. console.log('删除身高数据后')
  47. console.log(this.userMsg)
  48. },
  49. // 修改父亲信息
  50. changeFather() {
  51. this.userMsg.family[0] = { name: 'father', age: '40', sex: '男' ,height: '180'}
  52. // this.userMsg.family[0].age = '40' // dom 是会更新的
  53. console.log('修改后的父亲信息')
  54. console.log(this.userMsg.family)
  55. },
  56. },
  57. }
  58. </script>
  59. <style>
  60. .home{
  61. margin: 0 auto;
  62. text-align: center;
  63. }
  64. </style>

图示

点击了修改父亲信息按钮、数据更新成功、但是页面未同步渲染、即为dom未更新

解决方案

和场景一差不多

方案一 浅拷贝数组每条数据到一个新数组 对浅拷贝的数组数据操作 将浅拷贝的数组数据赋值给数组数据

方案二 this.$set() 方法

  1. <template>
  2. <div class="home">
  3. <h3>这是vue2测试页面</h3>
  4. <!-- <p>用户信息: {{ userMsg }} </p> -->
  5. <p>用户家人信息: {{ userMsg.family }} </p>
  6. <!-- <p><button @click="add">添加用户性别为男</button></p>
  7. <p><button @click="change">修改用户年龄为18</button></p>
  8. <p><button @click="deleteH">删除用户身高信息</button></p> -->
  9. <p><button @click="changeFather">修改父亲信息</button></p>
  10. </div>
  11. </template>
  12. <script>
  13. export default {
  14. data() {
  15. return {
  16. // 用户信息
  17. userMsg: {
  18. name: 'zs',
  19. age: '20',
  20. height: '180',
  21. family: [{ name: 'father', age: '45', sex: '男' }, { name: 'mother', age: '42', sex: '女' }]
  22. },
  23. }
  24. },
  25. methods: {
  26. // 为对象直接添加数据
  27. add() {
  28. // this.userMsg.sex = '男'
  29. // let newUserMsg = { ...this.userMsg }
  30. // newUserMsg.sex = '男'
  31. // this.userMsg = newUserMsg
  32. this.$set(this.userMsg, 'sex', '男');
  33. console.log('添加性别后')
  34. console.log(this.userMsg)
  35. },
  36. change() {
  37. this.userMsg.age = '18'
  38. console.log('修改数据年龄后')
  39. console.log(this.userMsg)
  40. },
  41. deleteH() {
  42. // let newUserMsg = { ...this.userMsg }
  43. // delete newUserMsg.height
  44. // this.userMsg = newUserMsg
  45. this.$delete(this.userMsg, 'height');
  46. console.log('删除身高数据后')
  47. console.log(this.userMsg)
  48. },
  49. // 修改父亲信息
  50. changeFather() {
  51. // this.userMsg.family[0] = { name: 'father', age: '40', sex: '男' ,height: '180'}
  52. // this.userMsg.family[0].age = '40' // dom 是会更新的
  53. // 方案一 浅拷贝数组每条数据到一个新数组 对浅拷贝的数组数据操作 将浅拷贝的数组数据赋值给数组数据
  54. let newArr = [ ...this.userMsg.family ]
  55. newArr[0] = { name: 'father', age: '40', sex: '男' ,height: '180'}
  56. this.userMsg.family = newArr
  57. console.log(this.userMsg)
  58. // 方案二 this.$set() 方法
  59. // this.$set(this.userMsg.family, 0, { name: 'father', age: '40', sex: '男' });
  60. },
  61. },
  62. }
  63. </script>
  64. <style>
  65. .home{
  66. margin: 0 auto;
  67. text-align: center;
  68. }
  69. </style>

四、简化版的数据劫持源码

  1. // 定义一个构造函数 Dep,用于收集依赖和通知更新
  2. function Dep() {
  3. this.subs = [];
  4. }
  5. Dep.prototype = {
  6. addSub(sub) {
  7. this.subs.push(sub);
  8. },
  9. notify() {
  10. this.subs.forEach(sub => {
  11. sub.update();
  12. });
  13. }
  14. };
  15. // 定义一个监听器 Observer,用于劫持对象的属性
  16. function Observer(data) {
  17. this.data = data;
  18. this.walk(data);
  19. }
  20. Observer.prototype = {
  21. walk(data) {
  22. Object.keys(data).forEach(key => {
  23. this.defineReactive(data, key, data[key]);
  24. });
  25. },
  26. defineReactive(data, key, val) {
  27. const dep = new Dep();
  28. Object.defineProperty(data, key, {
  29. enumerable: true,
  30. configurable: true,
  31. get() {
  32. if (Dep.target) {
  33. dep.addSub(Dep.target);
  34. }
  35. return val;
  36. },
  37. set(newVal) {
  38. if (val === newVal) {
  39. return;
  40. }
  41. val = newVal;
  42. dep.notify();
  43. }
  44. });
  45. }
  46. };
  47. // 定义一个 Watcher,用于进行依赖收集和更新
  48. function Watcher(vm, exp, cb) {
  49. this.vm = vm;
  50. this.exp = exp;
  51. this.cb = cb;
  52. this.value = this.get();
  53. }
  54. Watcher.prototype = {
  55. get() {
  56. Dep.target = this;
  57. const value = this.vm[this.exp];
  58. Dep.target = null;
  59. return value;
  60. },
  61. update() {
  62. const value = this.vm[this.exp];
  63. this.cb.call(this.vm, value);
  64. }
  65. };
  66. // 宮户 Vue 构造函数
  67. function Vue(options) {
  68. this.data = options.data;
  69. new Observer(this.data);
  70. // 初始化一个 Watcher 对象,用于触发依赖收集
  71. new Watcher(this, 'data', () => {
  72. console.log('数据更新了');
  73. });
  74. // 其他 Vue 相关逻辑...
  75. }
  76. // 创建一个 Vue 实例
  77. var vm = new Vue({
  78. data: {
  79. message: 'Hello, Vue!'
  80. }
  81. });
  82. // 修改数据,触发更新
  83. vm.data.message = 'Hello, New Vue!';

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/266057
推荐阅读
相关标签
  

闽ICP备14008679号