《源码系列》助你理解vue响应式源码——实现Observer数据劫持

前言

在上一节我们实现了Compile类来进行视图的初始化 时光机 这一节来实现一下对所有数据的劫持监听Observer类,相对来说较为简单。

Observer语法

实现数据劫持必不可少的就是使用Object.defineProperty方法(V2),所以在实现Observer之前先来了解一下它。

定义

Object.defineProperty()  静态方法会直接在一个对象上定义一个新属性,或修改其现有属性,并返回此对象。

参数

该方法有三个参数分别为对象本身,要监听的属性,属性描述符,其中属性描述符又分为数据描述符和存取描述符,存取描述符顾名思义就是get set。数据描述符中有value(默认值)enumberable(是否可枚举)writable(是否可以被修改)configurable(是否可删除)需要注意的是描述符对象中不能同时存在valueget或者writableset,原因也很简单,如果设置了valueget将会一直调用get方法。

形式

const obj = {};
Object.defineProperty(obj, "name", {
  enumerable: false,
  configurable: false,
  writable: false,
  value: "skyler",
});

缺点

该方法也存在一些弊端,也是vue中存在的并且改用proxy来进行响应式的原因之一。

1、不能监听添加的额外属性或者删除属性;

const obj = { foo: 'hello' };

Object.defineProperty(obj, 'foo', {
  get() {
    console.log('get foo');
    return this._foo;
  },
  set(value) {
    console.log('set foo');
    this._foo = value;
  }
});

obj.bar = 'world'; // 无法被监听到
delete obj.foo; // 无法被监听到

2、不能监听数组下标的变化

const arr = [1, 2, 3];
Object.defineProperty(arr, '0', {
  get() {
    console.log('get 0');
    return this._0;
  },
  set(value) {
    console.log('set 0');
    this._0 = value;
  }
});

arr[0] = 0; // 可以被监听到
arr.push(4); // 无法被监听到

3、性能问题

使用Object.defineProperty监听数据变化,需要为每个属性都创建一个 settergetter,这会带来一定的性能问题,特别是当数据对象较大时。

实现Observer数据劫持

作用

  1. 劫持监听所有属性

实现

新建Observer.js文件并在index.html文件中进行引入

<script src="./Observer.js"></script>
<script src="./Mvue.js"></script>

然后在Mvue.js文件中给Observer类传递要监听的数据源

class MVue {
    constructor(options) {
        this.$el = options.el;
        this.$data = options.data;
        this.$options = options;
        if (this.$el) {
            // 1、实现一个数据观察者
            new Observer(this.$data);
            // 2、实现一个指令解析器
            new Compile(this.$el, this)
        }
    }
}

创建Observer类,判断传递过来的数据是否为对象,这里只考虑data为对象的情况,如果是对象就获取到对象中的每一个属性并传入defineReactive中进行监听,该方法就是用来定义响应式数据的。

class Observer {
    constructor(data) {
        this.observer(data)
    }
    observer(data) {
        if (data && typeof data == 'object') {
            Object.keys(data).forEach(key => {
                this.defineReactive(data, key, data[key])
            })
        }
    }
 }

考虑到person:{name:'Joker'}的这种情况也就是对象中还有对象的情况时,需要对其进行递归遍历,目的就是要为每个属性添加上响应式。

   defineReactive(obj, key, value) {
        // 为每个属性添加响应式
        this.observer(value)
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                return value
            },
            set:(newVal)=> {
                if (newVal != value) {
                    value = newVal
                }
            }
        })
   }

这时候我们在控制台中测试一下可以发现已经为每个属性添加上了getset,但是如果为其重新赋值一个对象的话并没有被监听到,解决方法也很简单,就是在做set操作的时候重新调用一下observer方法并传入新的值即可。

《源码系列》助你理解vue响应式源码——实现Observer数据劫持

     set: (newVal) => {
       this.observer(newVal)
          if (newVal != value) {
              value = newVal
          }
      }

《源码系列》助你理解vue响应式源码——实现Observer数据劫持

到此为止数据劫持这部分就做完了,下一步就是要为每个属性创建观察者,当数据发生变化时要去通知观察者更新视图,观察者会有很多所以我们会使用订阅器Dep来对它进行收集并且还要与Observer进行关联,当前的首要问题就是在哪里以及如何创建观察者呢?这部分我们放到下一章讲解。

原文链接:https://juejin.cn/post/7341282461202530338 作者:孤独的根号_

(0)
上一篇 2024年3月2日 下午4:52
下一篇 2024年3月2日 下午5:03

相关推荐

发表回复

登录后才能评论