vue中的数据劫持

在浏览一篇博文的时候,看到里面提到了vue中数据劫持的概念,之前只是知道有这个东西,知道这个东西是vue的核心之一,是实现数据双向绑定的重要原理,但并未深入研究,那么今天就借这篇文章学习整理一下vue中的数据劫持到底是什么。

在面经中最常见的问题之一就是,你知道双向绑定吗,知道什么是MVVM吗?

学术性的回答模板有很多,其实简单来说就是数据和视图其中一方做出修改,另一方也随之变动。视图能够驱动数据,数据也能驱动视图。

视图驱动数据可以通过事件绑定来实现,那么数据驱动视图呢?

方法就是,给数据添加监听,一旦数据发生变化,就执行视图的修改操作,这个过程就是数据劫持

一段简单的vue代码

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

那么vue中的数据劫持究竟是怎么实现的呢?

其实就是通过 Object.defineProperty

Object.defineProperty

先来了解一下Object.defineProperty

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

语法:

Object.defineProperty(obj, prop, descriptor)

参数:

obj: 要定义属性的对象。

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

descriptor:要定义或修改的属性描述符。

对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由 getter 函数和 setter函数所描述的属性。一个描述符只能是这两者其中之一;不能同时是两者。

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'

那么在vue2.x中,要想实现data中所有属性都实现数据劫持,就要先遍历data中的所有属性,对每一个属性都使用Object.defineProperty,当属性的值发生变化时,就执行视图渲染操作。

参考一个vue数据劫持的简单实现(原理示例,非源码)

const data = {
    name: '数据劫持',
    info: {
        address: '北京'
    },
    numbers: [1, 2, 3, 4]
}

observerObject中对传入的每一个属性使用 Object.defineProperty 进行监听,如果传入的是一个对象则递归调用 observe 遍历对象的每一个属性,确保 data 中的所有属性都加入监听。

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() //模拟视图渲染操作
        }
    })
}

observe中遍历对象中的所有属性。

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)

可以看到,由于 Object.defineProperty 每次只能设置一个具体的属性,因此需要进行递归遍历操作,如果数据层级很深,就会造成性能隐患。

另外,Object.defineProperty 只能作用在对象上,那么对于数组数据应该如何处理呢?

数组其实也可以看作一宗特殊的对象,其下标就是对应的属性,理论上也可以使用Object.defineProperty进行数据劫持,但在vue中并没有选择这么做。而是选择劫持一些数组的常用操作方法,通过修改数组的原型方法,达到监听数组数据变化的目的。

//不能直接篡改Array.prototype对象,这样会对所有的数组实例都产生影响,需要通过原型继承得到一个新的原型对象
const oldArrayProperty = Array.prototype
const newArrayProperty = Object.create(oldArrayProperty)
const methods = ['pop', 'push', 'shift', 'unshift', 'splice', 'sort', 'reverse']

methods.forEach((method) => {
    newArrayProperty[method] = function() {
        renderView()
        oldArrayProperty[method].call(this, ...arguments)
    }
})
// 在observer函数中加入数组的判断,如果传入的是数组,则改变数组的原型对象为我们修改过后的原型。
if (Array.isArray(target)) {
    target.__proto__ = newArrayProperty
}

在vue3版本中,选择使用了proxy去实现对象的监听,避免了一些Object.defineProperty方法本身带来的问题。

参考链接:https://zhuanlan.zhihu.com/p/111591503

Logo

前往低代码交流专区

更多推荐