转载本文请联系三分钟学习前端公众号。监听一个变量的变化,并在变量变化时进行一定的操作。这类似于流行的前端框架(如React、Vue等)中的数据绑定功能,在数据更新时自动更新DOM渲染,那么如何实现数据绑定呢?定?本文给出了两个思路:ES5的Object.definePropertyES6的ProxyES5的Object.definePropertyObject.defineProperty()方法会直接在一个对象上定义一个新的属性,或者修改一个对象已有的属性,并返回这个Object——MDNObject.defineProperty(obj,prop,descriptor)其中:obj:定义属性的对象prop:要定义或修改的属性的名称或符号描述符:要定义或修改的属性描述符varuser={name:'sisterAn'}Object.defineProperty(用户,'name',{enumerable:true,configurable:true,set:function(newVal){this._name=newValconsole.log('set:'+this._name)},get:function(){console。log('get:'+this._name)returnthis._name}})user.name='an'//set:anconsole.log(user.name)//get:an如果完成监控每个子属性variable://监听对象functionobserve(obj){//遍历对象,使用get/set重新定义对象的各个属性值Object.keys(obj).map(key=>{defineReactive(obj,key,obj[键])})}函数defineReactive(obj,k,v){//递归子属性if(typeof(v)==='object')observe(v)//重定义get/setObject.defineProperty(obj,k,{enumerable:true,可配置:真,获取:函数反应eGetter(){console.log('get:'+v)returnv},//重置值时,触发收集器set的通知机制:functionreactiveSetter(newV){console.log('set:'+newV)v=newV},})}letdata={a:1}//监控对象observe(data)data.a//get:1data.a=2//set:2遍历map,监控子子属性通过深度递归注意Object.defineProperty有如下缺陷:不支持IE8及以下版本。IE无法检测到对象属性的添加或删除。如果修改了数组的长度(Object.defineProperty无法监听数组的长度),数组的push等,mutation方法无法触发setter。我们来看看vue2.x是如何解决这个问题的。vue2.x中如何监听数组变化,采用函数劫持的方式,重写数组的方法,Vue将数据中的数据转换数组的原型链重写,指向自己定义的数组原型方法so调用arrayapi时,可以通知依赖更新。如果数组中包含引用类型,则会再次监控数组中引用类型的递归遍历。这样就实现了监听数组变化。对于数组,Vue内部重写了以下函数来实现dispatchupdates//获取数组原型constarrayProto=Array.prototypeexportconstarrayMethods=Object.create(arrayProto)//重写了以下函数constmethodsToPatch=['push','pop','shift','unshift','splice','sort','reverse']methodsToPatch.forEach(function(method){//缓存原生函数constoriginal=arrayProto[method]//重写函数def(arrayMethods,method,functionmutator(...args){//先调用原函数得到结果constresult=original.apply(this,args)constob=this.__ob__letinserted//调用后面的函数时,监听新的数据switch(method){case'push':case'unshift':inserted=argsbreakcase'splice':inserted=args.slice(2)break}if(inserted)ob.observeArray(inserted)//手动分发更新ob.dep.notify()returnresult})})vue2.x如何解决由于现代JavaSc的限制给对象添加属性不会触发组件重新渲染的问题ript(Object.observe已被弃用),Vue无法检测到对象属性的添加或删除。由于Vue在初始化实例时会对属性进行getter/setter转换,所以属性必须存在于数据对象上,Vue才能将其转换为响应式。对于已经创建的实例,Vue不允许动态添加根级响应式属性。但是,可以使用Vue.set(object,propertyName,value)方法向嵌套对象添加响应式属性。vm.$set()实现原理exportfunctionset(target:Array|Object,key:any,val:any):any{//target是一个数组if(Array.isArray(target)&&isValidArrayIndex(key)){//修改数组长度,避免索引>数组长度导致splice()执行错误,val);returnval;}//target是一个对象,key在target或者target.prototype上,不能是在Object.prototype上直接赋值if(keyintarget&&!(keyinObject.prototype)){target[key]=val;returnval;}//以上都不成立,即开始为target创建一个全新的属性//获取Observer实例constob=(target:any).__ob__;//target本身不是响应式数据,直接赋值if(!ob){target[key]=val;returnval;}//响应式处理defineReactive(ob.value,key,val);ob.dep.notify();returnval;}如果target是数组,使用vuesplice实现的mutation方法实现了响应式。如果目标是对象,判断属性是否存在,是响应式的。如果目标本身没有响应,直接赋值。如果属性不是响应式的,调用defineReactive方法进行响应式处理ES6的Proxy是众所周知的,尤其是bigvue3.0版本使用Proxy代替defineProperty来实现数据绑定,因为Proxy可以直接监听对象和数组的变化,并且拦截方式多达13种。并且作为新标准,浏览器制造商将专注于持续的性能优化。ProxyProxy对象用于创建一个对象的代理,从而实现对基本操作(如属性查找、赋值、枚举、函数调用等)的拦截和定制——MDNconstp=newProxy(target,handler)其中:target:使用Proxy包装target对象(可以是任何类型的对象,包括原生数组、函数,甚至是另一个代理)handler:通常有一个函数作为属性的对象,每个属性中的函数定义执行各种操作时代理pBehaviorvarhandler={get:function(target,name){returnnameintarget?target[name]:'noprop!'},set:function(target,prop,value,receiver){target[prop]=value;console.log('propertyset:'+prop+'='+value);returntrue;}};varuser=newProxy({},handler)user.name='an'//propertyset:name=anconsole.log(user.name)//anconsole.log(user.age)//noprop!上面说到Proxy一共提供了13种拦截行为,分别是:getPrototypeOf/setPrototypeOfisExtensible/preventExtensionsownKeys/getOwnPropertyDescriptordefineProperty/deletePropertyget/set/hasapply/construct有兴趣的可以查看MDN,一一尝试。这里就不细说了,考虑另外两个问题:Proxy只会代理第一层对象,那么这个问题怎么处理呢?监听数组的时候,可能会触发多次get/set,那么如何防止多次触发(因为获取push修改长度的时候也会触发)Vue3Proxy对于第一个问题,我们可以判断当前是否有Reflect.get返回值为Object,如果是,则通过reactive方法进行生成这样,深度观察对于第二个问题,我们可以判断是否是hasOwProperty。我们自己写一个案例,使用代理自定义获取、增删改查等行为。//存储代理对象functionreactive(target){//创建一个响应对象.hasOwnProperty(key);}functioncreateReactiveObject(target){if(!isObject(target)){returntarget;}letobserved=toProxy.get(target);if(observed){//判断是否被代理returnobserved;}if(toRaw.has(target)){//判断是否重复代理returntarget;}consthandlers={get(target,key,receiver){letres=Reflect.get(target,key,receiver);track(target,'get',key);//依赖集合==returnisObject(res)?reactive(res):res;},set(target,key,value,receiver){letoldValue=target[key];lethadKey=hasOwn(target,key);letresult=Reflect.set(target,key,value,receiver);if(!hadKey){trigger(target,'add',key);//触发器添加}elseif(oldValue!==value){trigger(target,'set',key);//触发修改}returnresult;},deleteProperty(target,key){console.log("delete");constresult=Reflect.deleteProperty(target,key);returnresult;}};//启动代理observed=newProxy(target,handlers);toProxy.set(target,observed);toRaw.set(observed,target);//做一个映射表returnobserved;}Proxy相比defineProperty的优势总结:基于Proxy和Reflect,原生可以监听数组,可以监听addition和删除对象属性。深度遍历监控:判断当前Reflect.get返回值是否为Object,如果是则使用reactive方法作为代理,这样深度观察只在对象为getter时才劫持下一层对象(性能优化)因此推荐使用Proxy来监控变量的变化。参考MDN带你了解vue-next(Vue3.0)的熟练程度