Vue如何实现数据劫持?

·  阅读 541

我们平时在面试的时候,用过vue的同学,有一个问题肯定被问过,你知道双向绑定吗?知道什么是MVVM吗?

所谓的双向绑定其实就是,ui或者数据有一方做了修改,那么另外一个也会随着改变。

视图驱动数据,这个很简单,我们只要绑定事件就可以,但是数据要怎么驱动视图呢?这里就需要去对数据做监听,我们通常称之为”数据劫持“在每一次数据改变的时候,去执行更新视图的操作。

<template>
  <div>
    <input v-model="message">
    <p>{{ message }}</p>
  </div>
</template>
<script>
export default {
    data() {
        return {
            message: 'Hello World'
        };
    }
};
</script>复制代码

Object.defineProperty

在vue2.x版本中,数据劫持是用过Object.defineProperty这个API来实现

var message = 'hello world';
const data = {};
Object.defineProperty(data, 'message', {
    get() {
        return message;
    },
    set(newVal) {
        message = newVal;
    }
});
data.message // 'hello world'
data.message = 'test' // 'test'复制代码

第一个参数是要定义属性的对象,第二个参数是要定义属性的名称,第三个参数是被定义的属性描述符

当我们读取或者设置被定义的属性时,就会执行get或者set方法,也就是在这两个方法中,我们实现数据劫持,可以在这里添加一些我们附加的操作,而不只是简单的读写。

那么在vue中,是怎么实现对data中所有的属性做到数据劫持的呢?其实原理都一样,我们只需遍历所有data对象中的所有属性,并对每一个属性使用Object.defineProperty劫持即可,当属性的值发生变化的时候,我们执行一系列的渲染视图的操作。下面我们看一个简单的vue数据劫持的实现。

const data = {
    name: '你不知道滴前端',
    age: 25,
    info: {
        address: '北京'
    },
    numbers: [1, 2, 3, 4]
};
function observerObject(target, name, value) {
    if (typeof value === 'object' || Array.isArray(target)) {
        observer(value);
    }
    Object.defineProperty(target, name, {
        get() {
            return value;
        },
        set(newVal) {
            if (newVal !== value) {
                if (typeof value === 'object' || Array.isArray(value)) {
                    observer(value);
                }
                value = newVal;
            }
            renderView();
        }
    });
}
function observer(target) {
    if (typeof target !== 'object' || !target) {
        return target;
    }
    for (const key in target) {
        if (target.hasOwnProperty(key)) {
            const value = target[key];
            observerObject(target, key, value);
        }
    }
}
observer(data);复制代码

遍历这个data对象,对每一个属性都使用observerObject方法进行数据劫持。

observerObject主要做的就是使用Object.defineProperty去监听传入的属性,如果target是一个对象的话,就递归执行observer,确保data中所有的对象中的所以属性都能够被监听到。当我们set的时候,去执行renderView(执行视图渲染相关逻辑)。

Object.defineProperty的问题在于,只能够作用在对象上,那么vue中,对数组是怎么实现数据劫持的呢? 只需要修改数组的原型方法,往这些方法里添加一些视图渲染的操作。

const oldArrayProperty = Array.prototype;
const newArrayProperty = Object.create(oldArrayProperty);
['pop', 'push', 'shift', 'unshift', 'splice'].forEach((method) => {
    newArrayProperty[method] = function() {
        renderView();
        oldArrayProperty[method].call(this, ...arguments);
    };
});
 // 在observer函数中加入数组的判断,如果传入的是数组,则改变数组的原型对象为我们修改过后的原型。
    if (Array.isArray(target)) {
        target.__proto__ = newArrayProperty;
    }复制代码

上面代码就是vue2x版本数据劫持的原理实现(不是源代码)。

现在我们可以看出Object.defineProperty的一些问题

  1. 递归遍历所有的对象的属性,这样如果我们数据层级比较深的话,是一件很耗费性能的事情
  2. 只能应用在对象上,不能用于数组
  3. 只能够监听定义时的属性,不能监听新加的属性,这也就是为什么在vue中要使用Vue.set的原因,删除也是同理

Proxy

在vue3版本中,使用了proxy去实现对象的监听,避免了以上问题的出现,下面我们用proxy实现一个简易版本的数据劫持。在这之前,我们先来看一些基本的概念

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

Proxy对拦截操作多达13种,所以在这13中操作中,我们可以自定义很多逻辑。Proxy的出现,其实就是为了扩展对象的能力。

实现方式: new Proxy(target, handler)

target: 拦截的目标对象

handler:定义拦截的方法

下面是Proxy所拥有的拦截方法

  • handler.apply
  • handler.construct 拦截构造函数调用 new Example()
  • handler.difineProperty
  • handler.get 拦截对象属性的读取 obj.name
  • handler.set 拦截对象属性的设置 obj.name = '前端的自我修养'
  • handler.getOwnPropertyDwscriptor
  • handler.has
  • handler.ownKeys 拦截Object.keys for in 等
  • handler.setPrototypeOf
  • handler.isExtensible
  • handler.getPropertyOf
  • handler.preventExtensiions
  • handler.enumerate

大家可以再MDN上查看方法具体的作用,这里我们主要讲数据劫持,所以着重使用handle.get、handle.set和handle.deleteProperty的使用

既然有了拦截,自然还有有使用,这里不得不提到Reflect,它也是JS的一个内置的对象。

Refect设计的目的主要有一下几点:

  1. 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。
  2. 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
  3. 让Object操作都变成函数行为。比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。
  4. Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。

我们用get来举例

var p = new Proxy(target, {
  get: function(target, property, receiver) {
  }
});
Reflect.get(target, propertyKey, receiver)复制代码

Proxy数据劫持实现

function observe(target) {
    if (typeof target !== 'object' || target == null) {
        return target;
    }
    const obseved = new Proxy(target, {
        get(target, key, receiver) {
           return observe(Reflect.get(target, key, receiver);)
     
        },
        set(target, key, value, receiver) {
            if (value === target[key]) {
                return true;
            }
            const ownKeys = Reflect.ownKeys(target);
              if (ownKeys.includes(key)) {
                console.log('旧属性');
            } else {
                console.log('新添加的属性');
           
           return Reflect.set(target, key, value, receiver);


        },
        deleteProperty(target, key) {
           return result  Reflect.deleteProperty(target, key);
            
        }
    });
    return obseved;
}




const data = {
    name: '你不知道的前端',
    age: 25,
    info: {
        city:'beijing'
    },
    numbers: [1, 2, 3, 4]
};
const proxyData = observe(data复制代码

从上面的代码中可以看出,proxy在实现数据劫持时,具有以下优点:

  • proxy可以直接监听数组的修改
  • proxy可以直接监听属性的新增和删除
  • 在实现深度监听的时候,只有在data对象的属性被访问的时候,才去对这个属性做监听处理,而不是一次性递归所有的。

以上就是一个vue实现数据劫持的简单实例,只是在原理层面给大家展示出来,具体的源码实现上,还是有很多细节的处理,大家看完本文有了一个大概的思路后,可以去阅读下源码,对我们的理解框架的能力会有很高的提升,这些提升也会体现到日常的开发中去。


分类:
阅读
标签:
分类:
阅读
标签:
收藏成功!
已添加到「」, 点击更改