阅读 101

vue3响应原理之proxy

仓库地址:vue-next

Vue2.x 中,实现数据的可响应,需要对 ObjectArray 两种类型采用不同的处理方式。 Object 类型通过 Object.defineProperty 将属性转换成 getter/setter ,这个过程需要递归侦测所有的对象 key,来实现深度的侦测。

为了感知 Array 的变化,对 Array 原型上几个改变数组自身的内容的方法做了拦截,虽然实现了对数组的可响应,但同样存在一些问题,或者说不够方便的情况。 同时,defineProperty 通过递归实现 getter/setter 也存在一定的性能问题。

vue3.x实现响应式通过 ES6 提供的 Proxy API

proxy

Proxy API 具有更加强大的功能, 相比旧的 defineProperty API ,Proxy 可以代理数组,并且 API 提供了多方法 ,可以实现诸多功能。

这里主要说两属性: getset , 以及其中的一些比较容易被忽略的细节。

1.get,set
var data = {
    name: '就是玩儿',
    age: 666
}

var proxyData = new Proxy(data, {
    get(target, key, receiver) {
        // receiver --> proxyData
        const result = Reflect.get(target, key, receiver)
        console.log('get', key)
        return result  // 返回结果
    },
    set(target, key, val, receiver) {
        const result = Reflect.set(target, key, val, receiver)
        console.log('set', key, val)
        return result  // 是否设置成功
    },
    deleteProperty(target, key){
        const result = Reflect.deleteProperty(target, key)
        console.log('delete property', key)
        return result // 是否删除成功
    }
})

// vue2.x
/* 响应式监听属性的函数,一旦有赋新值就发生变化 , 监听每个数据的改变*/
    function defineReactive(obj, key, val){
        var dep = new Dep();
        Object.defineProperty(obj, key, {
            get: function(){
                if(Dep.target){
                    console.log('获取')
                    dep.addSub(Dep.target);
                }
                return val;
            },
            set: function(newVal){
                if(newVal === val){
                    return;
                }
                val = newVal;
                // console.log(`${val}改变了`)
                // 一旦更新立马通知
                dep.notify();
            }
        })
    }

// 相比于vue2.x设置属性值简洁不少
// 输出
proxyData.name = '不靠谱'
set name 不靠谱
"不靠谱"

// 也可以直接直接监测数组的操作
var data = ['a', 'b', 'c']
// 输出
proxyData.push('d')
get push
get length
set 3 d
set length 4
复制代码

通过输出结果可以看到,进行push操作,除了操作当前数据,还触发了数组本身length属性的更改。push 操作除了给数组的第 3 位下标设置值 d ,还给数组的 length 值更改为 4。 同时这个操作还触发了 get 去获取 push 和 length 两个属性。

2.多次触发
var proxyData = new Proxy(data, {
    get(target, key, receiver) {
        // receiver --> proxyData
        const result = Reflect.get(target, key, receiver)
        console.log('get', key)
        return result  // 返回结果
    },
    set(target, key, val, receiver) {
        // 重复数据不做处理, 上面push d 之后,length已经为4,无需再次设置
        if(val === target[key]){
            return true
        }
        const result = Reflect.set(target, key, val, receiver)
        console.log('set', key, val)
        return result  // 是否设置成功
    },
    deleteProperty(target, key){
        const result = Reflect.deleteProperty(target, key)
        console.log('delete property', key)
        return result // 是否删除成功
    }
})

// 输出
proxyData.push('d')
get push
get length
set 3 d
复制代码
3.深度监听
const data = {
    name: '就是玩儿',
    age: 666,
    info: {
        title: '不靠谱'
    },
    a: {
        b: {
            c: {
                d: 1
            }
        }
    }
}
function reactive(target = {}) {
    if(typeof target !== 'object' || target == null){
        return target
    }

    // 代理配置
    const proxyConf = {
        get(target, key, receiver) {
            // receiver --> proxyData
            const result = Reflect.get(target, key, receiver)
            console.log('get', key)
            return result

            // 深度监听
            // return reactive(result)
        },

        set(target, key, val, receiver) {
            // 重复数据不做处理, 上面push d 之后,length已经为4,无需再次设置
            // if(val === target[key]){
            //     return true
            // }

            // const ownKeys = Reflect.ownKeys(target)
            // if(ownKeys.includes(key)) {
            //     // 已存在,修改
            // }else {
            //     // 新增
            // }

            const result = Reflect.set(target, key, val, receiver)
            console.log('set', key, val)
            return result  // 是否设置成功
        },

        deleteProperty(target, key){
            const result = Reflect.deleteProperty(target, key)
            console.log('delete property', key)
            return result // 是否删除成功
        }

    }

    // 生成代理对象
    const observed = new Proxy(target, proxyConf)
    return observed
}

const proxyData = reactive(data)
复制代码

image.png 从输出结可以看到,proxy代理了最外层data,当获取proxyData.info时,info依然是一个普通的对象,并没有代理属性。那如何实现内部对象的代理呢,可以看下vue3.x的做法,vue3将内部对象代理放在了get中

function createGetter(isReadonly = false, shallow = false) {
    return function get(target, key, receiver) {
        if (isObject(res)) {
            return isReadonly ? readonly(res) : reactive(res)
        }
        return res
    }
}
复制代码

下面是加入深度监听后获取到的数据,可以看到info已经被监听了 image.png

优缺点

vue2用Object.defineProperty来作为响应式原理的实现,会有局限性,比如:

  • 深度监听需要一次性递归
  • 无法监听data内新增属性,需要用Vue.set
  • 无法监听删除属性,要用Vue.delete才能解决删除但视图不更新的限制
  • 无法监听原生数组

vue3.x:

  • 对数组不用单独做拦截处理
  • 惰性递归,什么时候用到什么时候递归

Reflect

  • Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。
  • 修改某些Object方法的返回结果,让其变得更合理
  • Object操作都变成函数行为。
// 老写法
'assign' in Object // true

// 新写法
Reflect.has(Object, 'assign') // true
复制代码
文章分类
前端
文章标签