Object.defineProperty

提到数据劫持,首先想到的就是Object.definePropertyObject.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。先来看一下Object.defineProperty(obj, prop, descriptor)的参数。

obj:要定义属性的对象;

prop:要定义和修改的属性的名称或Symbol;

descriptor:要定义或修改的属性描述符(数据描述符和存取描述符)。

详情可看:Object.defineProperty() - JavaScript | MDN

通过Object.defineProperty绑定的对象,会变成响应式化。利用的是Object.defineProperty第三个参数的存取描述符;也就是在改变对象时触发get和set方法来触发一系列视图更新。看例子:

function defineReactive (obj, key, val) {
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: () => {
            console.log('我被读了,我要不要做点什么好?');
            return val;
        },
        set: newVal => {
            if (val === newVal) {
                return;
            }
            val = newVal;
            console.log("数据被改变了,我要把新的值渲染到页面上去!");
        }
    })
}

let data = {
    text: 'hello world',
};

// 对data上的text属性进行绑定
defineReactive(data, 'text', data.text);

console.log(data.text); // 控制台输出 <我被读了,我要不要做点什么好?> 和 hello world
data.text = 'hello Vue'; // 控制台输出 <数据被改变了,我要把新的值渲染到页面上去!> 和 hello Vue

 从例子中可以看到当data.text的值发生变化的时候就会触发set这个事件打印<数据被改变了,我要把新的值渲染到页面上去!>,如果他的值没有发生变化机会触发get这个事件打印<我被读了,我要不要做点什么好?>。在vue中则是将这个响应式的Object.defineProperty放到了Observer类中进行管理。

//源码 
class Observer {
  constructor (value) {
    this.value = value   // 传入值
    this.dep = new Dep()
    if (Array.isArray(value)) { // 判断是否是数组
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  walk (obj) { // 非数组
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i]) // 调用响应式事件
    }
  }

  observeArray (items) { // 数组的方法
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i]) // 递归调用Observer
    }
  }
}

从源码中可以清晰地看出,通过Observer这个类将vue中this.date里的属性进行响应式的绑定。

接下来就是依赖收集:

我们通过defineReactive方法将data中的数据进行响应式后,虽然可以监听到数据的变化了,那我们怎么处理通知视图就更新呢?

在Vue中有一个Dep,它就是帮助我们进行依赖的收集。比如

<div>
    <p> {{message}} </p>
</div>

data: {
    name: '舒楠',
    message: '依赖被收集'
}

在这例子中,虽然data中有两个属性,但是只有message属性被渲染到页面上,因此我们只用通过dep收集message属性既可,可以避免无用的消耗。

响应式代码进行如下修改:

function defineReactive (obj, key, val) {
    let Dep; // 依赖

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: () => {
            console.log('我被读了,我要不要做点什么好?');
            // 被读取了,将这个依赖收集起来
            Dep.depend(); // 本次新增,进行依赖的收集哪些属性需要用到
            return val;
        },
        set: newVal => {
            if (val === newVal) {
                return;
            }
            val = newVal;
            // 被改变了,通知依赖去更新
            Dep.notify(); // 本次新增,进行依赖的更新
            console.log("数据被改变了,我要把新的值渲染到页面上去!");
        }
    })
}

dep实现的是收集依赖和通知更新,具体dep的执行过程不做详细解释(后期会做)。

最后就是Watcher

它是一个观察者的实例;创建实例的时候会把实例添加到dep中,简单的来说就是Watcher能够控制自己属于哪个,是data中的属性的还是watch,或者是computedWatcher自己有统一的更新入口,只要你通知它,就会执行对应的更新方法。(具体后期会做)。

总结

核心说的数据劫持,其实就是通过Object.defineProperty中的descriptor的存取描述符进行数据的劫持。

get事件在属性没有变化时触发并且还会触发dep收集依赖,set事件在属性发生变化触发并且触发dep收集依赖再触发Watch执行更新,这个两个事件在一起就实现了vue中Observer的数据劫持。

Logo

前往低代码交流专区

更多推荐