当前位置: 首页 > Web前端 > vue.js

Vue2源码解读之三:数据变化检测-数据响应原理

时间:2023-04-01 01:29:08 vue.js

Vue2.X的官方文档已经对深度响应原理进行了讲解。简单的说就是数据被修改后,在es5中被Object.defineProperty和setter拦截。通知watcher,watcher渲染函数,这个过程需要创建一个新的虚拟dom节点,对比旧的虚拟dom节点,比对之后打补丁,把补丁放到真实的dom结构中,然后更新真实的dom,视图发生了变化。Object.defineProperty()数据劫持/数据代理使用javascript引擎给出的函数来检测对象属性的变化。Object.defineProperty()方法会直接在一个对象上定义一个新的属性,或者修改一个对象已有的属性,并返回这个对象。varobj={};Object.defineProperty(obj,'a',{value:3})Object.defineProperty(obj,'b',{value:5})console.log(obj)//{a:3,b:5}console.log(obj.a,obj.b)//35Object.defineProperty()可以设置额外的隐藏属性Object.defineProperty(obj,'a',{//value:3,get(){},//是否可写:true})Object.defineProperty()实际上是通过自己的getter函数(读取)和setter函数(设置)对数据进行操作的:Object.defineProperty(obj,'a',{//getter函数get(){console.log(ole.log('访问一个属性');return7;},//setterfunctionset(nVal){console.log('修改一个属性为'+nVal)}})console.log(obj.a);//7obj.a=10;console.log(obj.a);//7从上面的例子来看,访问obj的a属性时,值为7,修改后a属性的值,从打印结果可以看出确实执行了setter函数,但是新的值并没有赋值给getter的返回值,此时getter和setter缺少一个连接桥:变量,所以上面的代码稍微改变一下:vartemp='';Object.defineProperty(obj,'a',{//getterfunctionget(){console.log(ole.log('访问属性');返回温度;},//setter函数set(nVal){console.log('修改属性为'+nVal);温度=nVal;}})C控制台日志(obj.a);//7obj.a=10;console.log(obj.a);//从10到这里,Object.defineProperty()的用法已经很清楚了,接下来要做的就是如何让它更漂亮优雅~defineReactivefunctionvarobj={};functiondefineReactive(data,key,val){//val为defineReactive函数中的getter和setter函数创建一个闭包环境,这样就不需要再声明一个TemporaryvariableObject.defineProperty(data,key,{//enumerableenumerable:true,//可以配置,比如可以删除configurable:true,//getterfunctionget(){console.log(ole.log('Accessaproperty');returnval;},//setterfunctionset(nVal){console.log('修改属性为'+nVal);if(nVal===val)return;val=nVal;}})}defineReactive(obj,'a',10);conso.log(obj.a);//10obj.a++;//赋值时调用setter函数console.log(obj.a);//11这段代码解决了变量的问题,但是这个例子只满足单层对象,所以像obj:{a:{m:{n:5}}}这样复杂结构的数据需要逐层遍历,递归调用Object.defineProperty()来处理~varobj={a:{m:{n:5}}}访问obj.a.m.n属性,你可以't每次都设置一个属性,所以当有结构复杂的对象时,defineReactive函数需要做参数判断:functiondefineReactive(data,key,val){if(arguments.length==2){val=data[key]}...}defineReactive(obj,'a')console.log(obj.a.m.n);//当要访问obj.a.m.n属性时,只能访问a级别的递归检测对象的所有属性。对于obj的a.m.n属性,需要递归实现Object.defineProperty()。这时候创建一个类Observer,主要是将普通对象转换成任意属性都可以检测的工具类:stateDiagram-v2Observer(observer)-->将普通对象转换成对象Observer,其每一层的属性都是responsive(可检测).jsexportdefaultclassObserver{constructor(value){//每个Observer实例都有一个depthis.dep=newDep()//对于实例(this,构造函数中的this不代表class本身,而是代表一个实例)添加了__ob__属性,值为value(这次是new的一个实例)def(value,'__ob__',this,false)//console.log('IamanObserverconstructor',value)//Observer类的作用:将一个普通对象转换为一个对象,其每一层的属性都是响应式的(可以检测到)//判断是数组还是对象if(Array.isArray(value)){//如果是数组,肯定很强,把这个数组的原型指向arrayMethods//setPrototypeOf强制定义值的原型Object.setPrototypeOf(value,arrayMethods)//使这个数组可观察this.observeArray(value)}else{this.walk(value)}}//遍历walk(value){for(letkinvalue){defineReactive(value,k)}}//特殊数组遍历observeArray(arr){for(leti=0,l=arr.length;i{//console.log('methodName',methodName)//备份原来的方法,因为push,pop等7个函数不能剥夺constoriginal=arrayPrototype[methodName];//把这个数组上的__ob__取出来,增加了__ob__,因为数组肯定不是最高级的,比如obj.g属性是数组,obj不能是数组。第一次遍历对象obj的第一层,g属性中已经添加了__ob__属性(这是数组)//定义一个新的方法def(arrayMethods,methodName,function(){//恢复原来的functionconstresult=original.apply(this,arguments);//将类数组对象转换为数组constargs=[...arguments];//console.log(arguments);constob=this.__ob__;//有push/unshift/splice这三个方法可以插入新item,现在要插入的新item也要改成observe的letins已注册=[];switch(methodName){case'push':case'unshift':inserted=args;休息;case'splice'://拼接格式为splice(下标,数量,插入新项)inserted=args.slice(2)break;}//判断是否有新item要插入,并让新item响应if(inserted){ob.observeArray(inserted);}console.log('lalala')ob.dep。通知()返回结果;},false)})依赖收集在getters中收集依赖,在setter中触发依赖将依赖收集的代码封装成一个Dep类,专门用来管理依赖,每个Observer实例,member中都有一个Dep实例他们;Watcher是一个中介,当数据发生变化时,会通过Watcher中转通知组件;dependency是指只有Watcher触发的getter才会收集依赖,哪个Watcher触发getter会收集哪个Watcher到Dep中;Dep使用发布-订阅模型。当数据发生变化时,它会循环依赖列表并通知所有Watcher一次。Dep类是用来收集依赖的,Watcher就是依赖。Dep.jsvaruid=0;exportdefaultclassDep{constructor(){//console.log('我是classdep的构造函数')this.id=uid++;//将您自己的订阅者存储在一个数组中。subs是英文subscribes订阅者的意思。//这个数组包含一个Watcher的实例this.subs=[];}//添加订阅addSub(sub){this.subs.push(sub)}//添加依赖depend(){//Dep.target是我们自己指定的全局位置。你可以使用window.target,只要它是全局唯一的并且没有歧义。if(Dep.target){//getter函数会从这个全局唯一的地方Watcher读取读取数据,并将这个Watcher收集到Depthis.addSub(Dep.target)}}//Notifyupdatenotify(){console.log('Iamnotify')//浅克隆一个constsubs=this.subs.slice();//遍历for(leti=0,l=subs.length;i{for(leti=0;i