经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » Vue.js » 查看文章
手写Vue源码之数据劫持示例详解
来源:jb51  时间:2021/1/4 9:51:34  对本文有异议

源代码: 传送门

Vue会对我们在data中传入的数据进行拦截:

  • 对象:递归的为对象的每个属性都设置get/set方法
  • 数组:修改数组的原型方法,对于会修改原数组的方法进行了重写

在用户为data中的对象设置值、修改值以及调用修改原数组的方法时,都可以添加一些逻辑来进行处理,实现数据更新页面也同时更新。

Vue中的响应式(reactive): 对对象属性或数组方法进行了拦截,在属性或数组更新时可以同时自动地更新视图。在代码中被观测过的数据具有响应性

创建Vue实例

我们先让代码实现下面的功能:

  1. <body>
  2. <script>
  3. const vm = new Vue({
  4. el: '#app',
  5. data () {
  6. return {
  7. age: 18
  8. };
  9. }
  10. });
  11. // 会触发age属性对应的set方法
  12. vm.age = 20;
  13. // 会触发age属性对应的get方法
  14. console.log(vm.age);
  15. </script>
  16. </body>

在src/index.js中,定义Vue的构造函数。用户用到的Vue就是在这里导出的Vue:

  1. import initMixin from './init';
  2.  
  3. function Vue (options) {
  4. this._init(options);
  5. }
  6.  
  7. // 进行原型方法扩展
  8. initMixin(Vue);
  9. export default Vue;
  10.  

在init中,会定义原型上的_init方法,并进行状态的初始化:

  1. import initState from './state';
  2.  
  3. function initMixin (Vue) {
  4. Vue.prototype._init = function (options) {
  5. const vm = this;
  6. // 将用户传入的选项放到vm.$options上,之后可以很方便的通过实例vm来访问所有实例化时传入的选项
  7. vm.$options = options;
  8. initState(vm);
  9. };
  10. }
  11.  
  12. export default initMixin;
  13.  

在_init方法中,所有的options被放到了vm.$options中,这不仅让之后代码中可以更方便的来获取用户传入的配置项,也可以让用户通过这个api来获取实例化时传入的一些自定义选选项。比如在Vuex 和Vue-Router中,实例化时传入的router和store属性便可以通过$options获取到。

除了设置vm.$options,_init中还执行了initState方法。该方法中会判断选项中传入的属性,来分别进行props、methods、data、watch、computed 等配置项的初始化操作,这里我们主要处理data选项:

  1. import { observe } from './observer';
  2. import { proxy } from './shared/utils';
  3.  
  4. function initState (vm) {
  5. const options = vm.$options;
  6. if (options.props) {
  7. initProps(vm);
  8. }
  9. if (options.methods) {
  10. initMethods(vm);
  11. }
  12. if (options.data) {
  13. initData(vm);
  14. }
  15. if (options.computed) {
  16. initComputed(vm)
  17. }
  18. if (options.watch) {
  19. initWatch(vm)
  20. }
  21. }
  22.  
  23. function initData (vm) {
  24. let data = vm.$options.data;
  25. vm._data = data = typeof data === 'function' ? data.call(vm) : data;
  26. // 对data中的数据进行拦截
  27. observe(data);
  28. // 将data中的属性代理到vm上
  29. for (const key in data) {
  30. if (data.hasOwnProperty(key)) {
  31. // 为vm代理所有data中的属性,可以直接通过vm.xxx来进行获取
  32. proxy(vm, key, data);
  33. }
  34. }
  35. }
  36.  
  37. export default initState;
  38.  

在initData中进行了如下操作:

  1. data可能是对象或函数,这里将data统一处理为对象
  2. 观测data中的数据,为所有对象属性添加set/get方法,重写数组的原型链方法
  3. 将data中的属性代理到vm上,方便用户直接通过实例vm来访问对应的值,而不是通过vm._data来访问

新建src/observer/index.js,在这里书写observe函数的逻辑:

  1. function observe (data) {
  2. // 如果是对象,会遍历对象中的每一个元素
  3. if (typeof data === 'object' && data !== null) {
  4. // 已经观测过的值不再处理
  5. if (data.__ob__) {
  6. return;
  7. }
  8. new Observer(data);
  9. }
  10. }
  11.  
  12. export { observe };
  13.  

observe函数中会过滤data中的数据,只对对象和数组进行处理,真正的处理逻辑在Observer中:

  1. /**
  2. * 为data中的所有对象设置`set/get`方法
  3. */
  4. class Observer {
  5. constructor (value) {
  6. this.value = value;
  7. // 为data中的每一个对象和数组都添加__ob__属性,方便直接可以通过data中的属性来直接调用Observer实例上的属性和方法
  8. defineProperty(this.value, '__ob__', this);
  9. // 这里会对数组和对象进行单独处理,因为为数组中的每一个索引都设置get/set方法性能消耗比较大
  10. if (Array.isArray(value)) {
  11. Object.setPrototypeOf(value, arrayProtoCopy);
  12. this.observeArray(value);
  13. } else {
  14. this.walk();
  15. }
  16. }
  17.  
  18. walk () {
  19. for (const key in this.value) {
  20. if (this.value.hasOwnProperty(key)) {
  21. defineReactive(this.value, key);
  22. }
  23. }
  24. }
  25.  
  26. observeArray (value) {
  27. for (let i = 0; i < value.length; i++) {
  28. observe(value[i]);
  29. }
  30. }
  31. }
  32.  

需要注意的是,__ob__属性要设置为不可枚举,否则之后在对象遍历时可能会引发死循环

Observer类中会为对象和数组都添加__ob__属性,之后便可以直接通过data中的对象和数组vm.value.__ob__来获取到Observer实例。

当传入的value为数组时,由于观测数组的每一个索引会耗费比较大的性能,并且在实际使用中,我们可能只会操作数组的第一项和最后一项,即arr[0],arr[arr.length-1],很少会写出arr[23] = xxx的代码。

所以我们选择对数组的方法进行重写,将数组的原型指向继承Array.prototype新创建的对象arrayProtoCopy,对数组中的每一项继续进行观测。

创建data中数组原型的逻辑在src/observer/array.js中:

  1. // if (Array.isArray(value)) {
  2. // Object.setPrototypeOf(value, arrayProtoCopy);
  3. // this.observeArray();
  4. // }
  5. const arrayProto = Array.prototype;
  6. export const arrayProtoCopy = Object.create(arrayProto);
  7.  
  8. const methods = ['push', 'pop', 'unshift', 'shift', 'splice', 'reverse', 'sort'];
  9.  
  10. methods.forEach(method => {
  11. arrayProtoCopy[method] = function (...args) {
  12. const result = arrayProto[method].apply(this, args);
  13. console.log('change array value');
  14. // data中的数组会调用这里定义的方法,this指向该数组
  15. const ob = this.__ob__;
  16. let inserted;
  17. switch (method) {
  18. case 'push':
  19. case 'unshift':
  20. inserted = args;
  21. break;
  22. case 'splice': // splice(index,deleteCount,item1,item2)
  23. inserted = args.slice(2);
  24. break;
  25. }
  26. if (inserted) {ob.observeArray(inserted);}
  27. return result;
  28. };
  29. });
  30.  

通过Object.create方法,可以创建一个原型为Array.prototype的新对象arrayProtoCopy。修改原数组的7个方法会设置为新对象的私有属性,并且在执行时会调用arrayProto 上对应的方法。

在这样处理之后,便可以在arrayProto中的方法执行前后添加自己的逻辑,而除了这7个方法外的其它方法,会根据原型链,使用arrayProto上的对应方法,并不会有任何额外的处理。

在修改原数组的方法中,添加了如下的额外逻辑:

  1. const ob = this.__ob__;
  2. let inserted;
  3. switch (method) {
  4. case 'push':
  5. case 'unshift':
  6. inserted = args;
  7. break;
  8. case 'splice': // splice(index,deleteCount,item1,item2)
  9. inserted = args.slice(2);
  10. break;
  11. }
  12. if (inserted) {ob.observeArray(inserted);}

push、unshift、splice会为数组新增元素,对于新增的元素,也要对其进行观测。这里利用到了Observer中为数组添加的__ob__属性,来直接调用ob.observeArray ,对数组中新增的元素继续进行观测。

对于对象,要遍历对象的每一个属性,来为其添加set/get方法。如果对象的属性依旧是对象,会对其进行递归处理

  1. function defineReactive (target, key) {
  2. let value = target[key];
  3. // 继续对value进行监听,如果value还是对象的话,会继续new Observer,执行defineProperty来为其设置get/set方法
  4. // 否则会在observe方法中什么都不做
  5. observe(value);
  6. Object.defineProperty(target, key, {
  7. get () {
  8. console.log('get value');
  9. return value;
  10. },
  11. set (newValue) {
  12. if (newValue !== value) {
  13. // 新加的元素也可能是对象,继续为新加对象的属性设置get/set方法
  14. observe(newValue);
  15. // 这样写会新将value指向一个新的值,而不会影响target[key]
  16. console.log('set value');
  17. value = newValue;
  18. }
  19. }
  20. });
  21. }
  22.  
  23. class Observer {
  24. constructor (value) {
  25. // some code ...
  26. if (Array.isArray(value)) {
  27. // some code ...
  28. } else {
  29. this.walk();
  30. }
  31. }
  32.  
  33. walk () {
  34. for (const key in this.value) {
  35. if (this.value.hasOwnProperty(key)) {
  36. defineReactive(this.value, key);
  37. }
  38. }
  39. }
  40.  
  41. // some code ...
  42. }
  43.  

数据观测存在的问题

检测变化的注意事项

我们先创建一个简单的例子:

  1. const mv = new Vue({
  2. data () {
  3. return {
  4. arr: [1, 2, 3],
  5. person: {
  6. name: 'zs',
  7. age: 20
  8. }
  9. }
  10. }
  11. })

对于对象,我们只是拦截了它的取值和赋值操作,添加值和删除值并不会进行拦截:

  1. vm.person.school = '北大'
  2. delete vm.person.age

而对于数组,用索引修改值以及修改数组长度不会被观测到:

  1. vm.arr[0] = 0
  2. vm.arr.length--

为了能处理上述的情况,Vue为用户提供了$set和$delete方法:

  • $set: 为响应式对象添加一个属性,确保新属性也是响应式的,因此会触发视图更新
  • $delete: 删除对象上的一个属性。如果对象是响应式的,确保删除触发视图更新。

结语

通过实现Vue的数据劫持,将会对Vue的数据初始化和响应式有更深的认识。

在工作中,我们可能总是会疑惑,为什么我更新了值,但是页面没有发生变化?现在我们可以从源码的角度进行理解,从而更清楚的知道代码中存在的问题以及如何解决和避免这些问题。

代码的目录结构是参考了源码的,所以看完文章的小伙伴,也可以从源码中找出对应的代码进行阅读,相信你会有不一样的理解!

到此这篇关于手写Vue源码之数据劫持的文章就介绍到这了,更多相关Vue源码之数据劫持内容请搜索w3xue以前的文章或继续浏览下面的相关文章希望大家以后多多支持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号