Vue数据劫持

lidenghui

在修改数据或访问数据的时候,

拦截这一步骤,并做一些额外的事情

实现方式

 

  • Vue2的defineProperty
  • Vue3的Proxy

Vue2 defineProperty

```ts
interface Props {
  // true 当且仅当该属性描述符的类型可以被改变并且该属性可以从对应对象中删除。
  // 默认为 false
  configurable: boolean;
  // true 当且仅当在枚举相应对象上的属性时该属性显现。
  // 默认为 false
  enumerable: false;

  // true 当且仅当在枚举相应对象上的属性时该属性显现。
  // 默认为 false
  value: any;

  // true当且仅当与该属性相关联的值可以用assignment operator改变时。
  // 默认为 false
  writable: boolean;

  // 作为该属性的 getter 函数,如果没有 getter 则为undefined。函数返回值将被用作属性的值。
  // 默认为 undefined
  get?: () => any;

  // 作为属性的 setter 函数,如果没有 setter 则为undefined。函数将仅接受参数赋值给该属性的新值。
  // 默认为 undefined
  set?:(value)=>void
}
/**
 * @params obj: 修改对象
 * @params key: 要修改的值
 * @params Props:
 */
Object.defineProperties(obj,key, props: Props)
```
```js
var data = {
    name: '张三',
}

var p = defineReactive(data)

function defineReactive(data) {
    var result = data
    Object.keys(data).forEach((key) => {
        var value = data[key]
        Object.defineProperty(result, key, {
            enumerable: true,
            configurable: true,
            set(val) {
                console.log(`set`, key, val)
                value = val
            },
            get() {
                console.log(`get`, key, value)
                return value
            },
        })
    })

    return result
}

console.log(p.name)
p.name = '李四'
console.log(p.name)
```

输出结果

遗留问题

`Object.defineProperty`只能根据已存在的 key 来修改`get``set`方法,所以当`key`不存在的时候没有办法触发`get``set`所以需要强制刷新,使用`$set`

数组不能监听变化?

数组可以看成`key`为 0,1...的对象,也可以使用`Object.defineProperty`改写`get``set`,也就是说`vue2`可以监控数组的数据变化,

当新增值的时候,数据的新的 key 没有被`Object.defineProperty`改写,也就没有办法触发页面刷新,因此需要用`$set`

```js
var data = ['张三', '李四', '王五']

var p = defineReactive(data)

function defineReactive(data) {
    var result = data
    Object.keys(data).forEach((key) => {
        var value = data[key]
        Object.defineProperty(result, key, {
            enumerable: true,
            configurable: true,
            set(val) {
                console.log(`set`, key, val)
                value = val
            },
            get() {
                console.log(`get`, key, value)
                return value
            },
        })
    })

    return result
}

p.unshift('赵六')
```

原因

  • 当数组`unshift`插入值的时候
  • 先在数组结尾插入一个值
  • 再取读最后一位放入,新插入的值的位置,
  • 再读取最后第二位放入,最后第一位的位置
  • 直到将第0号位置的数放入,1号位置
  • 最后将插入的值放入第0号位置

vue2如何处理的

node_modules/vue/src/core/observer/array.js

```ts
const methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse',
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
    // cache original method
    const original = arrayProto[method]
    def(arrayMethods, method, function mutator(...args) {
        const result = original.apply(this, args) //执行方法
        const ob = this.__ob__
        let inserted
        switch (method) {
            case 'push':
            case 'unshift':
                inserted = args
                break
            case 'splice':
                inserted = args.slice(2)
                break
        }
        if (inserted) ob.observeArray(inserted) // 改写新插入的值
        // notify change
        ob.dep.notify()
        return result // 返回结果
    })
})
```

检测数组中的方法有没有被添加数据,如果添加了数据,就将添加的数据用`Object.defineProperty`改写。因此也就解决了,push 后的值没有被监听的问题

遗留问题

因为要对对象的所有属性改写`set``get`方法,所以需要`递归遍历`数据的元素,所以性能比较差,所以不要在`vue``data`上挂不需要的值。如`window``jquery`

Vue3 Proxy

```ts

interface Handler {
  get: Function
  set: Function
}

/**
 * @params target 代理对象
 * @params handler 处理对象
 */
const p = new Proxy(target:object, handler:Handler)
```
```js
var data = {
    name: '张三',
}

var p = defineReactive(data)

function defineReactive(data) {
    var result = data

    p = new Proxy(result, {
        set(obj, prop, value) {
            console.log(`set`, prop, value)
            obj[prop] = value
        },
        get(obj, prop) {
            console.log(`get`, prop, obj[prop])

            return obj[prop]
        },
    })

    return p
}

p.name = '李四'
p.name
```

vue3 解决了 vue2 什么问题

Proxy 不需要遍历 data 的所有 key 修改 key 的 get 和 set,只需要在最上使用 Proxy,就能监听到当前对象 get 和 set。

 

那么当我们`p.name.first="李"`还能触发 set 吗

```js
var data = {
    name: {
        first: '张',
        content: '三',
    },
}

var p = defineReactive(data)

function defineReactive(data) {
    var result = data

    p = new Proxy(result, {
        set(obj, prop, value) {
            console.log(`set`, prop, value)
            obj[prop] = value
        },
        get(obj, prop) {
            console.log(`get`, prop, obj[prop])

            return obj[prop]
        },
    })

    return p
}

p.name.first = '李'
```

从 console 可以看出,只触发 data 的 name 的 get 方法,没有触发 set,也就是说 Proxy 只能监听代理数据的子项,不能监听代理数据的孙子项

怎么解决

```js
var data = {
    name: {
        first: '张',
        content: '三',
    },
}

var p = defineReactive(data)

function defineReactive(data) {
    var result = data

    p = new Proxy(result, {
        set(obj, prop, value) {
            console.log(`set`, prop, value)
            obj[prop] = value
        },
        get(obj, prop) {
            console.log(`get`, prop, obj[prop])

            return defineReactive(obj[prop])
        },
    })

    return p
}

p.name.first = '李'
```

监听到了 p.name.first 的 set 事件,这样就解决了数据递归遍历的噩运,提高了速度。

是不是可以在 get 被触发的时候,将子项也代理,从而监听孙子项

再考虑一下数组

```js
var data = ['张三', '李四', '王五']

var p = defineReactive(data)

function defineReactive(data) {
    var result = data

    p = new Proxy(result, {
        set(obj, prop, value) {
            console.log(`set`, prop, value)
            obj[prop] = value
            return true
        },
        get(obj, prop) {
            var val = obj[prop]

            if (typeof obj[prop] === 'object') {
                val = defineReactive(obj[prop])
            }

            return val
        },
    })

    return p
}

p.unshift('赵六')
```

在 vue3 中使用了 Proxy 也没有解决触发多次 set 事件的问题

因此这就要求我们在平时开发的过程中尽量少在中间插值

推荐的做法是,算好数组后,重新赋值

thanks

Vue数据劫持

By Denghui Li