1概述Vue的响应式原理主要基于:数据劫持,依赖收集和异步更新,通过对象劫持进行依赖收集和数据变化检测,通过维护队列视图进行异步更新。1.2什么是对象劫持?在JavaScript中,对象以key/value键值对的形式存在,存在对对象的增删改查等基本操作:constdog={name:'dog',single:false,girlFriend:'charm',house:'villa'}deletedog.housedog.single=truedog.character='easy'deletedog.girlFriendconsole.log(dog.name)而我们所说的对象劫持就是劫持这对对象的操作方法,那我们来看看1.3中如何劫持?主要是通过Object.defineProperty方法来实现的。举个小例子:“如果一个穷小子有房子,他就会有女朋友。如果房子没有了,那么女朋友也就没有了。”constdog={name:'dog',single:true,girlFriend:null}_house=nullObject.defineProperty(dog,'house',{configurable:true,get:()=>_house,set:(house)=>{if(house){_house=housedog.girlFriend='charm'dog.single=false}else{_house=''dog.girlFriend=nulldog.single=true}}})dog.house='villa'//{//name:'dog',//single:false,//girlFriend:'charm'//}dog.house=null//{//name:'dog',//single:true,//girlFriend:null//}1.4为什么劫持?从上面数据劫持的例子可以看出,通过数据劫持,可以拦截某个属性的修改操作,进而处理应该由这个变化触发的其他值和状态的更新。这个非常适合处理:数据驱动的视图更新。2深入2.1Object.defineProperty的局限性2.1.1无法检测新增和删除的属性操作Object.defineProperty无法拦截新增的属性:constdog={}dog.name='dog'vue官网也有提到:Vue无法检测属性的添加或删除。由于Vue在初始化实例时会对属性进行getter/setter转换,因此该属性必须存在于数据对象上,Vue才能将其转换为响应式。例如:varvm=newVue({data:()=>({a:1})})//`vm.a`是响应式的vm.b=2//`vm.b`是非响应式的因为无法劫持b的新属性,即使视图中已经引用了b,也不会响应式修改视图。Vue组件实例中提供了Vue.set方法来解决给对象添加新属性的问题。Object.defineProperty无法检测现有属性的删除:constdog={}Object.defineProperty(dog,'name',{configurable:true,get(){return'dog'},set(value){console.log(value)}})console.log(dog.name)//'dog'deletedog.nameconsole.log(dog.name)//undefineddefineProperty的设置描述符无法劫持删除操作。所以在vue中专门提供了一个Vue.delete方法来删除一个属性。2.1.2无法检测数组constdogs=[]Object.defineProperty(dogs,0,{configurable:true,get:()=>'easy',set:console.log})Object.defineProperty(dogs,1,{configurable:true,get:()=>'poor',set:console.log})dogs.length//2dogs[0]//'easy'dogs[1]//'poor'looksviaObject.defineProperty配置的数组元素表现正常,所以尝试操作数组的方法:dogs.push('newdog')//['easy','poor','newdogs']dogs.unshift('newdog2')//easy//newdog2//4//['easy','poor','poor','newdogs']从这个打印输出来看,push方法没有问题,但是unshift方法不符合预期;unshift方法里面应该是先把第一个元素赋给第二个,第二个赋给第三个,以此类推,然后把newdog2复制给第一个元素。不过这在原理上是有道理的,毕竟我们只截取了index为0和1的属性。2.1.3.注意使用Object.assignMDN官网提到,Object.assign方法在执行时,只会调用属性的getter和setter方法,所以在执行过程中会丢失属性描述符:constdog={}目的。defineProperty(dog,'name',{configurable:true,enumerable:true,get(){return'dog'},set(value){console.log(value)}})constdogBackup=Object.assign({},dog)//{name:'dog'}dogBackup.name='dogBackup'//{name:'dogBackup'}2.2升级方案ProxyProxy用于定义基本操作的自定义行为(例如属性查找、赋值、枚举、函数调用等);Object.defineProperty的上述限制可以通过Proxy来解决:2.2.1拦截对象的基本操作constdog={name:'dog',single:true,girlFriend:null,house:null}constproxyDog=newProxy(dog,{get(target,prop,receiver){//拦截查找操作returnReflect.get(target,prop,receiver)},set(target,prop,value,receiver){//拦截新添加的属性if(!Reflect.has(target,prop)){throwTypeError('Unknowntype'+prop)}//拦截赋值操作if(prop==='house'){if(value){Reflect.set(target,'girlFriend','魅力',接收者)Reflect.set(target,'single',false,receiver)}else{Reflect.set(target,'girlFriend',null,receiver)Reflect.set(target,'single',true,receiver)}}返回反射。set(target,prop,value,receiver)},deleteProperty(target,prop){//拦截删除操作if(prop==='house'){Reflect.set(target,'girlFriend',null)Reflect.set(target,'single',true)}returnReflect.deleteProperty(target,prop)}})2.2.2数组原型上的拦截方法constdogs=[]varproxyDog=newProxy(dogs,{apply(targetFun,ctx,args){//拦截方法调用returnReflect.apply(targetFun,ctx,args)}})proxyDog.push('easy')2.3响应式原理-Vue2中的Object.defineProperty使用了解Vue2的响应式原理,即可从变更检测机制、集合依赖、异步更新三个角度探讨:2.3.1变更检测机制-数据劫持在实例化一个vue组件时,会对组件的props和data进行defineReactive。方法是对Object.defineProperty的封装。它主要做了几个核心的事情:实例化一个依赖管理类Dep。这是非常重要的一点。对象的每个属性都会实例化一个Dep类,通过这个类收集依赖,通知更新通过Object.defineProperty劫持getters,收集Object.define的依赖Property(obj,key,{enumerable:true,configurable:true,get:functionreactiveGetter(){constvalue=getter?getter.call(obj):valif(Dep.target){dep.depend()if(childOb){childOb.dep.depend()if(Array.isArray(value)){dependArray(value)}}}returnvalue}})问题来了,我们一般取值data.name或者this.name,它会触发reactiveGetter,但是此时Dep.target一定不存在,只有Dep.target存在才会进行依赖收集:dep.depend()那么Dep.target什么时候存在呢?dep.depend()方法有什么作用?通过Object.defineProperty劫持setter并通知依赖更新Object.defineProperty({set:functionreactiveSetter(newVal){constvalue=getter?getter.call(obj):val//判断是否更新if(newVal===value||(newVal!==newVal&&value!==value)){return}/*eslint-enableno-self-compare*/if(process.env.NODE_ENV!=='production'&&customSetter){customSetter()}//#7981:对于没有setter的访问器属性if(getter&&!setter)returnif(setter){setter.call(obj,newVal)}else{val=newVal}childOb=!shallow&&observe(newVal)//Notificationupdatedep.notify()}})reactiveSetter做的事情比较简单,主要做两件事:1.判断值是否发生变化;2.通知依赖更新2.3.2收集依赖在收集依赖的过程中,会引发两个问题:问题1:Dep.target什么时候存在?首先,target的类型是一个Watcher实例。当一个vue组件被实例化时,会创建一个渲染观察器。渲染观察者是一个非惰性观察者。Dep.target在实例化时会立即设置为自身;而在模板编译的时候,也就是执行了vm._render函数,通过with定义了当前组件的作用域:with(this){return${code}}with语法在模板引擎中通常使用,所以模板编译时访问变量时的作用域就是with指定的作用域,这样getter可以触发对象属性的方法Dep.target存在的条件可以认为是:需要数据来驱动更新;这在Vue中体现:viewrenderingcomputedproperty$watchmethod问题2:dep.depend方法是做什么的?当对象属性的getter触发时,调用dep.depend()方法:classDep{depend(){if(Dep.target){Dep.target.addDep(this)}}}2.3.3异步更新记住我刚开始接触Vue的时候,有人问我:vue数据驱动视图更新是同步的还是异步的?如果是异步的,怎么实现呢?显然,它应该是异步的。数据更新操作在事件循环中执行多次。最后,dom应该只需要渲染一次。
