前言最近在项目中使用vue制作抽奖页面时,在向数组添加对象数据时,由于对象是引用类型,没有进行深拷贝,导致响应不正确。虽然不是什么大问题,但毕竟也是遇到的小坑,记录一下吧。彩票逻辑彩票的逻辑非常简单。它类似于老虎机模式中的彩票。我使用滚动监视器记录滚动条的滚动距离来计算获胜者。因为初始数据可能不够,当滚动到底部,抽奖还没有结束的时候,就用初始数据concat进去继续抽奖,如此反复,直到中奖。以下是伪代码。

前言最近在项目中使用vue制作抽奖页面时,在向数组添加对象数据时,由于对象是引用类型,没有进行深拷贝,导致响应不正确。虽然不是什么大问题,但毕竟也是遇到的小坑,记录一下吧。彩票逻辑彩票的逻辑非常简单。它类似于老虎机模式中的彩票。我使用滚动监视器记录滚动条的滚动距离来计算获胜者。因为初始数据可能不够,当滚动到底部,抽奖还没有结束的时候,就用初始数据concat进去继续抽奖,如此反复,直到中奖。以下是伪代码。看到这里,你或许一眼就能看出哪里不对。是的,在这种情况下,索引为2的lucky类也会被添加到数据中,但是当时比较迷惑,可能是对引用类型理解的不透彻,以为这样就可以了在创建阶段深度克隆后。当然,这种情况必须打印出users数据,看看$set之后的users数据。乍一看,索引2和4数据的__ob__属性中dep的id是一样的。我们知道,数据劫持时,会为每条数据创建一个observe观察者对象,并为其绑定一个唯一id的dep来收集watchers,然后使用__ob__来标记该对象是否被观察到。下面是vue的源码,观察一个值函数observe(value,asRootData){if(!isObject(value)||valueinstanceofVNode){return}varob;//当有__ob__属性时,说明对象已经被Observed,直接赋值给obif(hasOwn(value,'__ob__')&&value.__ob__instanceofObserver){ob=value.__ob__;}//如果不是,ob=newObserve,实例化Observeelseif(shouldObserve&&!isServerRendering()&&(Array.isArray(value)||isPlainObject(value))&&Object.isExtensible(value)&&!value._isVue){ob=newObserver(值);}if(asRootData&&ob){ob.vmCount++;}returnob}可以看到vue在观察一个对象的时候,如果__ob__属性已经存在,说明已经被监听了,会直接赋值给ob,不会创建Observe对象(这也是做的内部通过vue的一种性能优化方法),所以当你看到__ob__属性有相同id的dep值时,你基本上可以确认它是引用类型的幽灵。我们concat进入的clone数组是一个引用类型,所以当concat进入的时候,vue为里面的每一个数据对象实例化一个observe对象,用__ob__标记,因为clone是引用类型的关系,所以clone数组也是一样变了。所以我们第二次concat的时候,只是引用了最后一个clone数组,clone数组其实已经被观察过了,所以在重新遍历users数据的观察时,新添加的clone数组直接返回了__ob__属性。那么为什么$set之后,在索引2和索引4上都添加了lucky属性呢,我们来看看$setVue.prototype.$set=set;functionset(target,key,val){的源码。........(省略)varob=(target).__ob__;//获取__ob__属性,它是Observe对象if(!ob){target[key]=val;returnval}defineReactive$$1(ob.value,key,val);//手动绑定响应对象ob.dep.notify();//手动触发notify通知view更新returnval}//这个函数是Vue实现相应样式的核心函数,即使用Object.defineProperty劫持对象的属性(具体可以查看去vue源码看具体例子)functiondefineReactive$$1(obj,key,val,customSetter,shallow){vardep=newDep();varproperty=Object.getOwnPropertyDescriptor(obj,key);如果(属性&&property.configurable===false){return}vargetter=property&&property.get;varsetter=property&&property.set;如果((!getter||setter)&&arguments.length===2){val=obj[key];}varchildOb=!shallow&&observe(val);Object.defineProperty(obj,key,{enumerable:true,configurable:true,get:函数reactiveGetter(){varvalue=getter?getter.call(obj):val;如果(Dep.target){dep.depend();如果(childOb){childOb.dep.depend();如果(数组.isArray(值)){dependArray(值);}}}返回值},set:functionreactiveSetter(newVal){varvalue=getter?getter.call(obj):val;if(newVal===value||(newVal!==newVal&&value!==value)){return}if(process.env.NODE_ENV!=='production'&&customSetter){customSetter();}if(getter&&!setter){return}if(setter){setter.call(obj,newVal);}else{val=newVal;}childOb=!shallow&&observe(newVal);部门通知();}});}我们看到$set的原理是使用传入的target值,取target的__ob__属性,然后为新的key值重新绑定responsive对象,然后手动触发notify函数ob.dep的,通知watcher更新,并触发view重新渲染因为用户插入的数据是同一个引用,__ob__也是同一个,导致这样的渲染结果。所以每次concat之前都要深拷贝一个新数组,执行concatconstclone=JSON.parse(JSON.stringify(this.clone))this.users=this.users.concat(clone)SummaryHereis也是引用类型的一个小坑,所以当我们要复制一个对象(引用类型),或者在其他对象中操作这个对象(引用类型)时,为了不影响原对象(引用类型)的值,我们应该deepCopy的一个副本维护一个新的内存副本。最后,如果文章有什么问题,欢迎指出。