经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » Vue.js » 查看文章
浅析vue侦测数据的变化之基本实现
来源:jb51  时间:2021/6/15 11:28:22  对本文有异议

一、Object的变化侦测

下面我们就来模拟侦测数据变化的逻辑。

强调一下我们要做的事情:数据变化,通知到外界(外界再做一些自己的逻辑处理,比如重新渲染视图)。

开始编码之前,我们首先得回答以下几个问题:

1.如何侦测对象的变化?

  • 使用 Object.defineProperty()。读数据的时候会触发 getter,修改数据会触发 setter。
  • 只有能侦测对象的变化,才能在数据发生变化的时候发出通知

2.当数据发生变化的时候,我们通知谁?

  • 通知用到数据的地方。而数据可以用在模板中,也可以用在 vm.$watch() 中,地方不同,行为也不相同,比如这里要渲染模板,那里要进行其他逻辑。所以干脆抽象出一个类。当数据变化的时候通知它,再由它去通知其他地方。
  • 这个类起名叫 Watcher。就是一个中介。

3.依赖谁?

  • 通知谁,就依赖谁,依赖 Watcher。

4.何时通知?

  • 修改数据的时候。也就是 setter 中通知

5.何时收集依赖?

  • 因为要通知用数据的地方。用数据就得读数据,我们就可以在读数据的时候收集,也就是在 getter 中收集

6.收集到哪里?

  • 可以在每个属性里面定义一个数组,与该属性有关的依赖都放里面

编码如下(可直接运行):

  1. // 全局变量,用于存储依赖
  2. let globalData = undefined;
  3.  
  4. // 将数据转为响应式
  5. function defineReactive (obj,key,val) {
  6. // 依赖列表
  7. let dependList = []
  8. Object.defineProperty(obj, key, {
  9. enumerable: true,
  10. configurable: true,
  11. get: function () {
  12. // 收集依赖(Watcher)
  13. globalData && dependList.push(globalData)
  14. return val
  15. },
  16. set: function reactiveSetter (newVal) {
  17. if(val === newVal){
  18. return
  19. }
  20. // 通知依赖项(Watcher)
  21. dependList.forEach(w => {
  22. w.update(newVal, val)
  23. })
  24. val = newVal
  25. }
  26. });
  27. }
  28.  
  29. // 依赖
  30. class Watcher{
  31. constructor(data, key, callback){
  32. this.data = data;
  33. this.key = key;
  34. this.callback = callback;
  35. this.val = this.get();
  36. }
  37. // 这段代码可以将自己添加到依赖列表中
  38. get(){
  39. // 将依赖保存在 globalData
  40. globalData = this;
  41. // 读数据的时候收集依赖
  42. let value = this.data[this.key]
  43. globalData = undefined
  44. return value;
  45. }
  46. // 数据改变时收到通知,然后再通知到外界
  47. update(newVal, oldVal){
  48. this.callback(newVal, oldVal)
  49. }
  50. }
  51.  
  52. /* 以下是测试代码 */
  53. let data = {};
  54. // 将 name 属性转为响应式
  55. defineReactive(data, 'age', '88')
  56. // 当数据 age 改变时,会通知到 Watcher,再由 Watcher 通知到外界
  57. new Watcher(data, 'age', (newVal, oldVal) => {
  58. console.log(`外界:newVal = ${newVal} ; oldVal = ${oldVal}`)
  59. })
  60.  
  61. data.age -= 1 // 控制台输出: 外界:newVal = 87 ; oldVal = 88

在控制台下继续执行 data.age -= 1,则会输出 外界:newVal = 86 ; oldVal = 87

附上一张 Data、defineReactive、dependList、Watcher和外界的关系图。

首先通过 defineReactive() 方法将 data 转为响应式(defineReactive(data, 'age', '88'))。

外界通过 Watcher 读取数据(let value = this.data[this.key]),数据的 getter 则会被触发,于是通过 globalData 收集Watcher。

当数据被修改(data.age -= 1), 会触发 setter,会通知依赖(dependList),依赖则会通知 Watcher(w.update(newVal, val)),最后 Watcher 再通知给外界。

二、关于 Object 的问题

思考一下:上面的例子,继续执行 delete data.age 会通知到外界吗?

不会。因为不会触发 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. <title>Document</title>
  7. <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  8. </head>
  9. <body>
  10. <div id='app'>
  11. <section>
  12. {{ p1.name }}
  13. {{ p1.age }}
  14. </section>
  15. </div>
  16. <script>
  17. const app = new Vue({
  18. el: '#app',
  19. data: {
  20. p1: {
  21. name: 'ph',
  22. age: 18
  23. }
  24. }
  25. })
  26. </script>
  27. </body>
  28. </html>

运行后,页面会显示 ph 18。我们知道更改数据,视图会重新渲染,于是在控制台执行 delete app.p1.name,发现页面没有变化。这与上面示例中执行 delete data.age 一样,都不会触发setter,也就不会通知到外界。

为了解决这个问题,Vue提供了两个 API(稍后将介绍它们):vm.$set 和 vm.$delete。

如果你继续执行 app.$delete(app.p1, 'age'),你会发现页面没有任何信息了(name 属性已经用 delete 删除了,只是当时没有重新渲染而已)。

:如果这里执行 app.p1.sex = 'man',用到数据 p1 的地方也不会被通知到,这个问题可以通过 vm.$set 解决。

三、Array 的变化侦测

3.1、背景

假如数据是 let data = {a:1, b:[11, 22]},通过 Object.defineProperty 将其转为响应式之后,我们修改数据 data.a = 2,会通知到外界,这个好理解;同理 data.b = [11, 22, 33] 也会通知到外界,但如果换一种方式修改数据 b,就像这样 data.b.push(33),是不会通知到外界的,因为没走 setter。请看示例:

  1. function defineReactive(obj, key, val) {
  2. Object.defineProperty(obj, key, {
  3. enumerable: true,
  4. configurable: true,
  5. get: function () {
  6. console.log(`get val = ${val}`)
  7. return val
  8. },
  9. set: function reactiveSetter (newVal) {
  10. if(val === newVal){
  11. return
  12. }
  13. console.log(`set val = ${newVal}; oldVal = ${val}`)
  14. val = newVal
  15. }
  16. });
  17. }
  18.  
  19. // 以下是测试代码 {1}
  20. let data = {}
  21. defineReactive(data, 'a', [11,22])
  22. data.a.push(33) // get val = 11,22 (没有触发 setter) {2}
  23. data.a // get val = 11,22,33
  24. data.a = 1 // set val = 1; oldVal = 11,22,33(触发 setter)

通过 push() 方法改变数组的值,确实没有触发 setter(行{2}),也就不能通知外界。这里好像说明了一个问题:通过 Object.definePropery() 方法,只能将对象转为响应式,不能将数组转为响应式。

其实 Object.definePropery() 可以将数组转为响应式。请看示例:

  1. // 继续上面的例子,将测试代码(行{1})改为:
  2. let data = []
  3. defineReactive(data, '0', 11)
  4. data[0] = 22 // set val = 22; oldVal = 11
  5. data.push(33) // 不会触发 {10}

虽然 Object.definePropery() 可以将数组转为响应式,但通过 data.push(33)(行{10})这种方式修改数组,仍然不会通知到外界。

所以在 Vue 中,将数据转为响应式,用了两套方式:对象使用 Object.defineProperty();数组则使用另一套。

3.2、实现

es6 中可以用 Proxy 侦测数组的变化。请看示例:

  1. let data = [11,22]
  2. let p = new Proxy(data, {
  3. set: function(target, prop, value, receiver) {
  4. target[prop] = value;
  5. console.log('property set: ' + prop + ' = ' + value);
  6. return true;
  7. }
  8. })
  9. console.log(p)
  10. p.push(33)
  11. /*
  12. 输出:
  13. [ 11, 22 ]
  14. property set: 2 = 33
  15. property set: length = 3
  16. */

es6 以前就稍微麻烦点,可以使用拦截器。原理是:当我们执行 [].push() 时会调用数组原型(Array.prototype)中的方法。我们在 [].push()Array.prototype 之间增加一个拦截器,以后调用 [].push() 时先执行拦截器中的 push() 方法,拦截器中的 push() 在调用 Array.prototype 中的 push() 方法。请看示例:

  1. // 数组原型
  2. let arrayPrototype = Array.prototype
  3.  
  4. // 创建拦截器
  5. let interceptor = Object.create(arrayPrototype)
  6.  
  7. // 将拦截器与原始数组的方法关联起来
  8. ;('push,pop,unshift,shift,splice,sort,reverse').split(',')
  9. .forEach(method => {
  10. let origin = arrayPrototype[method];
  11. Object.defineProperty(interceptor, method, {
  12. value: function(...args){
  13. console.log(`拦截器: args = ${args}`)
  14. return origin.apply(this, args);
  15. },
  16. enumerable: false,
  17. writable: true,
  18. configurable: true
  19. })
  20. });
  21.  
  22. // 测试
  23. let arr1 = ['a']
  24. let arr2 = [10]
  25. arr1.push('b')
  26. // 侦测数组 arr2 的变化
  27. Object.setPrototypeOf(arr2, interceptor) // {20}
  28. arr2.push(11) // 拦截器: args = 11
  29. arr2.unshift(22) // 拦截器: args = 22

这个例子将能改变数组自身内容的 7 个方法都加入到了拦截器。如果需要侦测哪个数组的变化,就将该数组的原型指向拦截器(行{20})。当我们通过 push 等 7 个方法修改该数组时,则会在拦截器中触发,从而可以通知外界。

到这里,我们只完成了侦测数组变化的任务。

数据变化,通知到外界。上文编码的实现只是针对 Object 数据,而这里需要针对 Array 数据。

我们也来思考一下同样的问题:

1.如何侦测数组的变化?

  • 拦截器

2.当数据发生变化的时候,我们通知谁?

  • Watcher

3.依赖谁?

  • Watcher

4.何时通知?

  • 修改数据的时候。拦截器中通知。

5.何时收集依赖?

  • 因为要通知用数据的地方。用数据就得读数据。在读数据的时候收集。这和对象收集依赖是一样的。
  • {a: [11,22]} 比如我们要使用 a 数组,肯定得访问对象的属性 a。

6.收集到哪里?

  • 对象是在每个属性中收集依赖,但这里得考虑数组在拦截器中能触发依赖,位置可能得调整

就到这里,不在继续展开了。接下来的文章中,我会将 vue 中与数据侦测相关的源码摘出来,配合本文,简单分析一下。

四、关于 Array 的问题

  1. // 需要自己引入 vue.js。后续也尽可能只罗列核心代码
  2. <div id='app'>
  3. <section>
  4. {{ p1[0] }}
  5. {{ p1[1] }}
  6. </section>
  7. </div>
  8. <script>
  9. const app = new Vue({
  10. el: '#app',
  11. data: {
  12. p1: ['ph', '18']
  13. }
  14. })
  15. </script>

运行后在页面显示 ph 18,控制台执行 app.p1[0] = 'lj' 页面没反应,因为数组只有调用指定的 7 个方法才能通过拦截器通知外界。如果执行 app.$set(app.p1, 0, 'pm') 页面内容会变成 pm 18

以上就是浅析vue侦测数据的变化之基本实现的详细内容,更多关于vue侦测数据的变化的资料请关注w3xue其它相关文章!

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号