Object.defineproperty和proxy在项目中的妙用

1,229 阅读4分钟

之前说过,现在在用vue替换原来的jq老项目,采用的是分批次的重构,现在重构告一段落,准备进行一个阶段性的总结,其中让自己比较满意的点就是对Object.defineproperty和proxy的使用,提升了团队的开发体验,以及观察者模式的深入理解。废话不多说,开始正题!

项目背景:

原来的老项目全部使用jq写的,不可能一次性全部用vue重构,其中必然会出现vue和jq一起操作一份数据的情况。
众所周知,vue是响应式框架,核心理念就是数据驱动视图,不需要操作DOM,只需要操作数据即可使视图更新, 而jq则是充斥着大量的DOM及数据操作

项目需求

将jq的一部分操作替换为vue,那就需要将数据改为响应式

Object.defineproperty和proxy

Object.defineproperty

使用场景:通过点击按钮来控制组件的显示,隐藏以及提交数据的逻辑,比如在一个页面中,有三个选项卡,用户点击不同的选项卡,会显示不同的表单组件及提交逻辑
解决方法:在window上挂载一个字段,在setter中增加一些判断,通过不同的newValue来控制以上的逻辑,调用loading动画等操作

Object.defineProperty(window,"loadingControl",{
    set(value){
        //根据value的值进行判断
    }
})

proxy

使用场景:在地图上增删坐标点的同时,要使vue写的列表组件一起联动,在列表增删数据的时候,地图也随之刷新并呈现给用户
解决方法:表面上是地图和列表的显示息息相关,实际上就是因为它俩操作的是同一份数据,与此同时,列表数据一般都是数组,综上所述,那就不得不提到proxy,proxy最大的优点就是可以监听整个对象(当然,它只能监听第一层,如果这个对象的值是一个对象,那么需要用proxy再监听一层,通常做法是递归遍历,如果值为对象,那么再次用proxy监听即可),那么我们在拿到数据的时候就对数据进行处理,并且将更新地图和列表的方法都放进去,这样的话,只要通过接口或是用户操作修改了这个数据,那么地图和列表就会被通知并作出相应的更新
流程图

伪代码实现

let timer=null
function deepProxy(obj, cb=()=>{console.log("操作视图")}) {
    if (typeof obj === 'object') {
        for (let key in obj) {
            if (typeof obj[key] === 'object') {
                obj[key] = deepProxy(obj[key], cb);
            }
        }
    }
    return new Proxy(obj, {
        set: function (target, key, value, receiver) {
            if(timer!==null){
                clearTimeout(timer)
            }
            timer=setTimeout(() => {
               cb()
            }, 0);
            
            console.log("key===="+key+"===value==="+value)
            return Reflect.set(target, key, value, receiver);
        },
    });
 
}

总结

  1. 关于Object.defineproperty和proxy
    从上面的描述中,我们可以明显的看出,Object.defineproperty在监听对象的某个属性的时候更方便,proxy可以直接监听对象而非属性,在多个组件共享一份数据的时候表现的更出色,尤其是在数据为数组的时候
  2. 关于设计模式
    通过上面的描述,我们很明显看出,这两个api都可以在项目中轻易的实现一个观察者模式(定义了对象间一种一对多的依赖关系,当目标对象 Subject 的状态发生改变时,所有依赖它的对象 Observer 都会得到通知),比如在proxy用例中,地图和列表所用的是同一份数据,当数据改变时,立即通知到地图和列表并作出相应的操作
  3. 关于项目
    我们通过Object.defineproperty和proxy把对数据修改和DOM操作部分解耦,让项目的结构看起来更加的合理,在后续维护的时候也可以更好的修改

对于vue源码的意外收获

在项目过程中,经常会遇到需要在vue外面调用vue里面的方法,当时想的比较简单,就是直接调用vm['内部的方法']

let vm=new Vue({
            el: "#root",
            data(){
                return {
                    name:"test"
                }
            },
            methods: {
                getName(){
                    console.log(this==vm)
                    console.log(this.name)
                }
            }
        });
        console.log(vm.getName())

后面我觉得这种写法实在是别扭,看看有没有别的方法,于是我就去查看在new vue之后,到底对里面的methods做了哪些操作 当我们new Vue之后
第一步 初始化
在new Vue之后,代码走到了初始化这里,这里有生命周期,事件注册等 第二步 找到初始化methods方法 第三步 进入initMethods 然后就可以发现,原来在初始化的时候,vue对每个methods方法都进行了bind,使其this指向vm 简化操作如下

var vm={a:2, option:{getName(){console.log(this.a)}}}
vm.getName=vm.option.getName.bind(vm)
var z=vm.getName
z() //2

第四步 查看bind,vue为了兼容还是考虑的很周全的

在第二步的时候,我们发现此操作是在created之前,那么我们只需要在created之后将其赋值给外界即可

	new Vue({
            el: "#root",
            data(){
                return {
                    name:"test"
                }
            },
            methods: {
                getName(){
                    console.log(this==vm)
                    console.log(this.name)
                }
            },
            created(){
                getName=this.getName
            }
        });
        getName()

待续...