接上一篇文章:用proxy模拟数据监听

938 阅读1分钟

刚才回头看了看,发现昨天说今天要看的是vm.$nextTick & Vue.nextTick,我记错了,emm....

这篇文章主要想模拟proxy实现Vue的监控数据改变,所以我先来复习一下proxy。 想模拟proxy实现Vue的监控数据改变 先来复习一下proxy

reflect

不是说复习proxy吗!干嘛给我看reflect?当然是因为他们有关联啦。

Reflect是什么?

Reflect是一个内置的js对象(会有一些兼容性问题)它提供了一系列方法,可以让开发者通过调用这些方法来访问一些JS底层功能。由于它类似于其他语言的"反射",因此取名为Reflect。

他可以做什么?

使用Reflect可以实现属性的赋值与取值、调用普通函数、调用构造函数、判断属性是否在对象中等等功能。当然我们依然可以使用符号去编程,也可以使用这些api去编程。 他只是给我们提供了一些操作底层的方法。

这些功能不是已经存在了吗 为什么还需要用Reflect实现一次?

有一个重要的理念,就是在ES5被提出减少魔法(也就是暴露底层方法)让代码更加纯粹,这种理念很大程度上是受到函数式编程的影响。

ES6进一步贯彻了这种理念,他认为对属性内存的控制、原型链的修改、函数的调用等等这些都属于底层实现,属于一种魔法。因此要把他们提取出来,形成一个正常的API。

它里面到底提供了哪些API呢

  • Reflect.set(target, propertyKey, value): 设置对象target的属性propertyKey的值为value 等同于给对象的属性赋值

    const obj = {
        a: 1,
        b: 2
    }
    Reflect.set(obj, "a", 10)
    //相当于obj.a = 10
    
  • Reflect.get(target, propertyKey):读取对象target的属性propertyKey 等同于读取对象的属性值

    Reflect.get(obj, "a")
    //相当于obj.a
    
  • Reflect.apply(target, thisArgument, argumentsList):调用一个指定的函数 并绑定this和参数列表 等同于函数调用

    function method(a, b){
        console.log("method", a, b)
    }
    
    //相当于method(3,4)
    Reflect.apply(method, null, [3, 4])
    
  • Reflect.deleteProperty(target, propertyKey):删除一个对象的属性

    const obj = {
        a: 1,
        b: 2
    }
    //相当于 delete obj.a = 10
    Reflect.deleteProperty(obj, "a")
    console.log(obj)
    
  • Reflect.defineProperty(target,propertyKey,attributes): 类似于Object.defineProperty,不同的是如果配置出现问题,返回false而不会报错。

  • Reflect.consruct(target, argumentsList):用构造函数的方式创建一个对象。

    function Test(a, b){
        this.a = a
        this.b = b
    }
    //相当于const t = new Test(1, 3)
    const t = Reflect.construct(Test, [1, 3])
    console.log(t)
    
  • Reflect.has(target, propertyKey):判断一个对象是否拥有一个属性。

    const obj = {
        a: 1,
        b: 2
    }
    //相当于console.log("a" in obj)
    console.log(Reflect.has(obj, "a"))
    
  • 其他API:developer.mozilla.org/zh-CN/docs/…

proxy代理

代理是啥?

把我们的目标对象当成霸道总裁,代理就是霸道总裁的小助理。

我们不能直接接触到霸道总裁(目标对象),我们要是想和霸道总裁(目标对象)打交道 那就必须要通过小助理(代理)。我们交互的时候是和小助理进行交互,有什么事情要让小助理去找霸道总裁,霸道总裁处理事件后就把结果交给小助理,小助理再交给我们(卑微)。

那如果让代理去做我们平常就可以实现的事情是没有意义的:如果要设置属性值就给属性值,要读取属性值就读取属性值,有啥意义,我们也会啊!那我们既然使用了代理,代理一定有一些权力可以修改我们做不到的事(也就是底层实现)。比如说在修改一个属性的时候,代理要有方法,有能力去修改底层的实现。

这就要求底层实现必须做成api的形式:因为修改赋值符号或是修改new是很难实现的,但是修改一个函数就比较简单。我们只要定义一个函数再覆盖原来的函数就可以了,这也是出现reflect的原因。

使用代理

我们先来创建一个代理:

//代理一个目标对象
//target: 目标对象
//handler: 是一个普通对象 
//         其中可以重写代理的底层实现 参数和reflect是一样的
//         也就是说里面放了我们之前学习的reflect的api 
//最后会返回一个被代理的对象
new Proxy(target, handler)

简单的使用一下试试

const obj = {
    a: 1,
    b: 2
}
const proxy = new Proxy(obj, {
    //代理偷懒 什么活也没干
})
proxy.a = 1
console.log(proxy.a)//1

让代理干点活!不能偷懒

const obj = {
    a: 1,
    b: 2
}
const proxy = new Proxy(obj, {
    //赋值的时候输出一下
    set(target, propertyKey, value){
        console.log(target, propertyKey, value)
    }
})
proxy.a = 10 //Object "a" 10
console.log(proxy.a) //输出1 因为赋值函数里面什么都没做 根本就没有赋值嘛!

干点有意义的活!

const obj = {
    a: 1,
    b: 2
}
const proxy = new Proxy(obj, {
    set(target, propertyKey, value){
        //我虽然重写了方法 但是还是想先调用底层的方法 再进行重写
        Reflect.set(target, propertyKey, value)
        //当然也可以写成target[propertyKey] = value
        //但是这样更有点内种味道 懂吧 就是这里是底层的操作 你整一个target[propertyKey] = value看起来就不是很像在操作底层了
        ...//当然还可以写一些其他操作 自由发挥吧 这里就不操作了
    }
})
proxy.a = 10
console.log(proxy.a)//10

不知道有没有人好奇proxy里面是什么东西,反正我挺好奇的,于是在上面的代码中输出了一下,但是也没看出个啥.. 博客JavaScript 深入理解proxy指出,new Proxy返回的实例对象proxy的原型就是目标对象obj的原型。

console.log(proxy)
/*稍微看看proxy到底是个啥东西
Proxy {a: 10, b: 2}
    //我们重写的方法
    [[Handler]]: Object
        set: ƒ set(target, propertyKey, value)
        __proto__: Object
    //目标对象和它里面的属性
    [[Target]]: Object
        a: 10
        b: 2
        __proto__: Object
    //未撤销
    [[IsRevoked]]: false
*/

有两点需要注意:

  1. 创建了代理之后,后续使用这个对象要记得通过代理来使用 虽然不通过代理也不会报错。
  2. 其实set有第四个参数,是代理对象本身。
//重写get has属性
const obj = {
    a: 1,
    b: 2
}
const proxy = new Proxy(obj, {
    get(target, propertyKey){
        //这里是直接操作目标对象的 不是通过代理 所以不会被下面改写过的has影响
        if(Reflect.has(target, propertyKey)){
            return Reflect.get(target, propertyKey)
        } else {
            return -1
        }
    }
    has(target, propertyKey){
        return false
    }
}
//相当于Reflect.get(obj, "d")
console.log(proxy.d)//-1
//相当于Reflect.has(obj, "a")
console.log("a" in proxy)//false

用proxy实现数据监听

复习完之后,我们就直接上手吧!

const data = {
    name: "小饼",
    blog: {
        name: "快点吃饼"
    }
}
//设置重写方法的对象
let handlerObj = {
    set(target, propertyKey, value) {
        Reflect.set(target, propertyKey, value)
        render()
    }
}
const proxy = new Proxy(data, handlerObj)
proxy.name = "仙女" //数据改变 输出"页面渲染了"
proxy.blog.name = "仙女" //数据改变 但是没有输出"页面渲染了"

所以说,该来的递归还是跑不掉的..

//递归 只要是对象就创建proxy
function observer(data) {
    //如果当前数据是一个数组
    if (Array.isArray(data)) {
        //直接返回代理
        return new Proxy(data, handlerObj)
    }
    //如果当前数据是一个对象
    if (typeof data === "object") {
        //遍历里面的所有属性
        for (let key in data) {
            //递归 如果有哪个属性的是对象就要让proxy代理之后再返回
            data[key] = observer(data[key]) || data
        }
        //返回proxy代理之后的数据
        return new Proxy(data, handlerObj)
    }
}
// handlerObj没有改动 放在这里只是为了方便看
let handlerObj = {
    set(target, propertyKey, value) {
        Reflect.set(target, propertyKey, value)
        render()
    }
}
let proxy = observer(data)
proxy.name = "仙女"
proxy.blog.name = "仙女"

啊,原来proxy也要递归啊,好像也不是很厉害嘛...

然后我又测试了一下那几个defineProperty的硬伤 测试结果如下:

//所有测试数据
const data = {
    name: "小饼",
    blog: {
        name: "快点吃饼"
    },
    like: ["蜡笔小新", "小葵", "野原美伢", "野原广志"]
}

//能否监听到新增对象
proxy.sex = "仙女" //输出"页面渲染了" 说明能监听到
//能否监听到数组不存在的索引的改变
proxy.like[4] = "小白" //输出"页面渲染了" 说明能监听到
//能否监听到数组长度的改变
proxy.like.length = 0 //输出"页面渲染了" 说明能监听到
//能否监听到删除对象
delete proxy.like //啥都没输出 但是确实被删除了

proxy就是牛!

为什么defineProxy为什么会有那些缺陷?

defineProperty只能监听对象中的某一个属性。

从前,有一个对象,他辛辛苦苦遍历完所有的属性,每一个属性都上一个defineProperty监听器,搞了大半天总算能管好自己的每个属性了,这时候你还要给他插进来一个属性,那他真的管不了啦。

他控诉道:"嘤嘤嘤,我好不容易搞完了,你又给我来一个,难道我还要重新遍历一次所有的属性,就为了给你这一个突然插进来的属性加一个监听器吗?呜呜呜呜,我不干了。就算你插进来我也不会给你加监听器的。"

由于新插进来的属性上没有侦听器,这个属性的变化当然就没办法被监听到了,也就不能重新渲染页面了。

而proxy之所以能监听到这些变化,都是因为proxy监听的是一整个对象而不是对象中的某一个属性。proxy是这样说的:我监听的是一整个对象!只要对象里的属性改变了就是对象被改变了!所以统统给老子刷新!

总结一下,proxy一些优点:

  1. 官方一点的说法就是:

    Object.defineProperty只能劫持对象的属性,而Proxy是直接代理对象。

    接地气一点的说法就是:

    我们之前使用Object.defineProperty的时候,只能监听属性。所以我们需要对对象中的每一个属性进行遍历,然后加上监听器。

    但是我们使用proxy之后,proxy管的就是整个对象,只要监听对象就可以监听到他的所有儿子属性的改动。

    (但是孙子属性是监听不到滴,所以还是要递归。)

    由于这个特性,我们得到了proxy的两个优点:

    1. 不需要遍历添加监听器

      (这是我有点纠结的地方,因为到处都说是不需要遍历。但是不遍历怎么去监听对象里面的对象呢?所以我也不敢乱说话,但是有篇文章的观点是和我一样的,我贴在文章后面了。)

    2. 不需要使用$set方法

  2. Proxy支持13种拦截操作,这是defineProperty所不具有的

    就是reflect支持的方法他都支持了.

  3. 新标准性能红利

    Proxy作为新标准,长远来看,JS引擎会继续优化Proxy,但getter和setter基本不会再有针对性优化。(这句话是引用的,出处放在文章最后了)


为什么vue2.x不使用Proxy呢?兼容性不好

参考文章

  1. 为什么Vue3.0使用Proxy实现数据监听(defineProperty表示不背这个锅)
  2. ES6之Proxy与数据劫持
  3. vue3.0尝鲜 -- 摒弃Object.defineProperty,基于 Proxy 的观察者机制探索