变化侦测 
什么是变化侦测?
 
Vue.js自动通过状态生成DOM,并将其输出到页面,此为渲染。Vue.js的渲染过程是声明式的,我们通过模版来描述状态与DOM之间的映射关系。而当应用内部状态变化时需要重新渲染,而如何确定状态中什么变化了就是变化侦测要解决的问题。
object的变化侦测 如何追踪变化? 两种方式可以追踪变化:Object.defineProperty和ES6的Proxy。
由于ES6在浏览器中的支持度不理想,所以Vue2采用了Object.defineProperty实现,而Vue3则采用了Proxy。
利用Object.defineProperty侦测对象变化可以写出这样的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function  defineReactiv  (data, key, val) {     Object .defineProperty (data, key, {         enumerable : true ,         configurable : true ,         get : function  ( ) {             return  val         },         set : function  (newVal ) {             if (val === newVal) {                 return              }             val = newVal         }     }) }
 
defineReactive是对Object.defineProperty的封装,其作用为定义一个响应式数据,在这个函数中进行变化追踪,封装后只需要传递data,key,value即可。封装好后,每当从data的key中读取数据,get函数被触发;往data的key中设置数据时,set函数被触发。
如何收集依赖?收集在哪里? 
在Vue2中,模板使用数据等同于组件使用数据,所以数据变化时,会将通知发送到组件,组件内部再通过虚拟DOM重新渲染。
 
我们观察依赖的目的是当数据发生变化的时候,通知那些使用了该数据的地方。所以我们需要收集依赖,把用到数据的地方收集起来,等属性发生变化时,将之前收集好的依赖循环触发一遍,也就是在getter中收集依赖,setter中触发依赖。
现在已经知道了要在哪里收集触发依赖,那么要把它收集到哪里?我们创建一个专门帮助管理依赖的类Dep,使用这个类,我们可以收集、删除依赖或向依赖发送通知。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 export  default  class  Dep  {     constructor ( ) {         this .subs  = []     }          addSub (sub ) {         this .subs .push (sub)     }          removeSub (sub ) {         remove (this .subs , sub)     }     depend ( ) {         if (window .target ) {             this .addSub (windwo.target )         }     }          notify ( ) {         const  subs = this .subs .slice ()         for (let  i = 0 , l = subs.length ; i < l; i++) {             subs[i].update ()         }     } }function  remove  (arr, item) {     if  (arr.length ) {         const  index = arr.indexOf (item)         if (index > -1 ) {             return  arr.splice (index, 1 )         }     } }
 
再改造一下defineReactive
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function  defineReactive  (data, key, val) {     let  dep = new  Dep ()     Object .defineProperty (data, key, {         enumerable : true ,         configurable : true ,         get : function  ( ) {             dep.depend ()             return  val         },         set : function  ( ) {             if (val === newVal) {                return               }             val = newVal             dep.notify ()         }     }) }
 
什么是Watcher 上边的代码中,我们收集的依赖是window.target,那我们究竟要收集谁呢。收集说的通俗易懂点就是当属性变化时,我们应该通知谁。
我们需要通知用到数据的地方,而用到它的地方可能会很多,有可能是模版,也有可能是watch等等,所以我们需要抽象出一个能集中处理这些情况的类。我们在收集依赖阶段只收集这个封装好的类实例,同样也只通知它自己,由它负责通知其他地方,这就是Watcher。
Watcher是一个中介的角色,数据变化时通知它,他负责通知其他地方。
首先看一下Watcher的经典使用方式:当a.b.c变化时会触发第二个参数中的函数
1 2 3 vm.$watch('a.b.c' , function  (newVal, oldVal ) {      })
 
根据使用方法我们可以首先实现Watcher:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 export  default  class  Watcher  {     constructor (vm, expOrFn, cb ) {         this .vm  = vm         this .getter  = parsePath (expOrFn)         this .cb  = cb         this .value  = this .get ()     }          get ( ) {         window .target  = this          let  value = this .getter .call (this .vm , this .vm )         windwo.target  = undefined          return  value     }          update ( ) {         const  oldValue = this .value          this .value  = this .get ()         this .cb .call (this .vm , this .value , oldValue)     } }
 
这段胆码可以把自己主动添加到data.a.b.c的Dep中。在get方法中先把window.target设置为this即当前的watcher实例,然后读data.a.b.c的值,触发getter。
触发了getter,就会触发收集依赖的逻辑,从window.target中读取一个依赖添加到Dep中。这样只要在window.target赋一个this,再读一下值,触发getter,就可以把this主动添加到keypath的Dep中。
依赖注入到Dep中后,每次值变化就会让依赖列表中所有依赖循环触发update方法,执行参数中的回调函数,将value和oldValue传到参数中。
递归侦测所有key 现在已经可以实现变化侦测的功能,但是我们希望把数据中所有属性(含子属性)都侦测到,需要一个Observer类。它的作用是将一个数据内所有属性都转换成getter/setter形式,再去追踪他们的变化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 export  class  Observer  {     constructor (value ) {         this .value  = value                  if (!Array .isArray (value)) {             this .walk (value)         }     }          walk ( ) {         const  keys = Object .keys (obj)         for (let  i = 0 ; i < keys.length ; i++) {             defineReactive (obj, keys[i], obj[keys[i]])         }     } }function  defineReactive (data, key, val ) {     if (typeof  val === 'object' ) {         new  Observer (val)     }          let  dep = new  Dep ()     Object .defineProperty (data, key, {     enumerable : true ,     configurable : true ,     get : function  ( ) {         dep.depend ()         return  val     },     set : function  ( ) {         if (val === newVal) {            return           }         val = newVal         dep.notify ()         }     }) }
 
我们定义Observer类,将正常对象转换成被侦测的对象。然后判断数据类型,只有Object才会调用walk进行转换,最后在defineReactive中新增new Observer(val)来递归子属性。
关于Object的问题 上边我们介绍了Object类型的变化侦测原理,正是因为数据变化是通过getter/setter进行追踪,所以有些语法中即使数据变化,Vue也追踪不到。
比如,向object添加或删除属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var  vm = new  Vue ({     el :'#app' ,     template :'#demo-template' ,     methods :{         action ( ) {             this .obj .age  = '23'          },         dAction ( ) {             delete  this .obj .name          }     },     data : {         obj :{             name :'abc'          }     } })
 
Object.defineProperty将对象的key转化为getter/setter来追踪变化,但它只能追踪一个数据是否被修改,无法侦测新增和删除。
所以Vue提供两个API:vm.$set和vm.$delete
总结 Object可以通过Object.defineProperty将属性转换成getter/setter形式来追踪变化,读取触发getter,修改触发setter。
在getter中对使用了数据的依赖进行收集,当setter触发时通知getter中收集的依赖数据发生变化。
收集依赖需要为依赖找一个储存的地方,所以有了Dep,它用来收集、删除依赖并向依赖发送消息。
依赖就是Watcher,只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter就把它收集到Dep中,数据发生变化时会循环依赖列表通知所有Watcher。
Watcher的原理是先把自己设置到全局唯一的位置,然后读取数据触发getter,接着getter中就会从唯一的位置读取当前正在读取数据的Watcher,并把它收集到Dep中,这样Watcher可以主动去订阅任一数据的变化。
此外,还创建了Observer类,它是用来将一个object中的所有数据(包括子数据)都转换成响应式的。
Array的变化侦测 Array的侦测为什么与Object不同,因为Object使用getter/setter侦测变化,而数组通常使用push等方法来改变,不会触发getter/setter。
如何追踪变化 这里不讨论ES6的情况,在ES6以前,我们没有能够拦截原型方法(push等)的能力,但我们可以用自定义方法覆盖原生的原型方法。
我们可以用一个拦截器覆盖Array.prototype。之后每当使用原型上的方法操作数组的时候,执行的其实是拦截器中的方法,之后在拦截器内使用原生的原型方法去操作数组。
拦截器 数组原型上的方法有七个,分别是push,pop,shift,unshift,splice,sort和reverse。我们可以实现一个和原型样的Object,里面的属性一模一样,只不过其中改变数组自身内容的方法是重写的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const  arrayProto = Array .prototype export  const  arrayMethods = Object .create (arrayProto)const  methods = ['push' ,'pop' ,'shift' ,'unshift' ,'splice' ,'sort' ,'reverse' ] methods.forEach ((method ) =>  {     const  original = arrayProto[method]          Object .defineProperty (arrayMethods , method , {         value : function  mutator (...args ) {             return  original.apply (this , args)         },         enumerable :false ,         writable : true ,         configurable :true      }) })
 
创建了arrayMethods继承自Array.prototype,具备所有功能,接下来使用Object.defineProperty对改变数组的方法进行封装。所以当我们调用push的时候,实际调用的是arrayMethods.push,而它实际上是mutator函数,在mutator函数中执行original(原生方法)来做应该做的事,我们可以在mutator中做其他的事比如发送变化通知。
使用拦截器覆盖Array原型 我们需要用它覆盖Array.prototype,但又不能直接覆盖,因为这样会污染全局的Array。我们希望拦截只针对那些被侦测了变化的数据生效,也就是希望拦截器只覆盖那些响应式数组的原型。
而数据转成响应式的,需要通过Observer,所以我们只需要在Observer中用拦截器覆盖那些Array类型数据的原型。
1 2 3 4 5 6 7 8 9 10 11 export  class  Observer  {     constructor (value ) {         this .value  = value                  if (Array .isArray (value)) {             value._proto_  = arrayMethods         } else  {             this .walk (value)         }     } }
 
它的作用是将拦截器赋给value._proto_,覆盖value原型的功能。
拦截器挂载到数组属性上 部分浏览器可能不支持_proto_,而Vue的做法是如果不能使用_proto_,则直接将arrayMethods身上的方法设置到被侦测的数组上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 const  hasProto = '_proto_'  in  {}const  arrayKeys = Object .getOwnPropertyNames (arrayMethods)export  class  Observer  {     constructor (value ) {         this .value  = value                  if (Array .isArray (value)) {             const  augment = hasProto ? protoAugment : copyAugment                          augment (value, arrayMethods, arrayKeys)         } else  {             ...         }     }          ... }function  protoAugment (target, src, keys ) {     target._proto_  = src }function  copyAugment (target, src, keys ) {     for (let  i = 0 ,l = keys.length ;i < l; i++) {         const  key = keys[i]         def (target, key, src[key])     } }
 
如何收集依赖 我们已完成了拦截器,本质上是为了当数组内容变化时能够得到通知Dep中的依赖的能力。而数组也是在getter中收集依赖的。想要读取数组,首先肯定会触发这个数组的名对应属性的getter。而Array的依赖和Object一样,也会在defineReactive中收集:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function  defineReactive (data, key, val ) {     if (typeof  val === 'object' ) new  Observer (val)          let  dep = new  Dep ()          Object .deintProperty (data,key, {         enumerable :true ,         configurable :true ,         get :function ( ) {             dep.depend ()                                       return  val         },         set :function (newVal ) {             if (val === newVal) return                           dep.notify ()             val = newVal         }     }) }
 
Array在getter中收集依赖,在拦截器中触发依赖。
依赖列表存在哪 Array的依赖被存放在Observer中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 export  class  Observer  {     constructor (value ) {         this .value  = value         this .dep  = new  Dep ()                   if (Array .isArray (value)) {             const  augment = hasProto ? protoAugment : copyAugment                          augment (value, arrayMethods, arrayKeys)         } else  {             ...         }     }          ... }
 
我们有个疑问,为什么数组的dep要保存在Observer实例上呢。因为数组在getter中收集依赖,在拦截器中触发依赖,所以依赖保存的位置很关键,需要在getter和拦截器中都能访问到。将Dep实例保存到Observer的属性上以后,我们能够在getter中访问并收集。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function  defineReactive (data, key, val ) {     let  childOb = observer (val)     let  dep = new  Dep ()     Object .defineProerty (data, key, {         enumerable :true ,         configurable :true ,         get :function ( ) {             dep.depend ()             if (childOb) {                 childOb.dep .depend ()             }             return  val         },         set :function (newVal ) {             if (val === newVal) {                 return              }                          dep.notify ()             val = newVal         }     }) }
 
在defineReactive中调用observe,将val当做参数传进去拿到返回值,就是observer实例。
在拦截器中获取Observer实例 因为Array拦截器是对原型的一种封装,所以在拦截器中可以访问到this。而dep保存在Observer中,所以需要在this上读到Observer实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function  def  (obj, key, val, enumerable) {     Object .defineProperty (obj, key,  {         value :val,         enumerable :!!enumerable,         writable :true ,         configruable :true      }) }export  class  Observer  {     constructor (value ) {         this .value  = value         this .dep  = new  Dep ()                  def (value , '__ob__' ,this )                  if (Array .isArray (value)) {             ...         } else  {             ...         }     }     ... }
 
上述代码在Observer中新增了一段代码,它在value上新增了一个不可枚举的__ob__属性即当前Observer实例。它还可以用来标记当前value是否被Observer转换成响应式数据。
也就是说所有被侦测了的数据身上都会有一个__ob__来表示它们是响应式的,可以通过value.__ob__来访问Observer实例。如果是Array拦截器,因为它是原型方法,所以可以直接通过this.__ob__来访问Observer实例。
1 2 3 4 5 6 7 8 9 10 11 12  ['push' ,'pop' ,……].forEach (function (method ) {     const  original = arrayProto[method]     Object .defineProperty (arrayMethods,method, {         value :function  mutator (...args ) {             const  ob = this .__ob__              return  original.apply (this ,args)         },         ...     }) })
 
向数组依赖发送通知 我们只需要在拦截器中访问Observer实例,拿到dep属性,直接发送通知即可。
1 2 3 4 5 6 7 8 9 10 11 12 13  ['push' ,'pop' ,……].forEach (function (method ) {     const  original = arrayProto[method]     Object .defineProperty (arrayMethods,method, {         value :function  mutator (...args ) {             const  ob = this .__ob__              ob.dep .notify ()             return  original.apply (this ,args)         },         ...     }) })
 
侦测数组元素变化及新增变化 元素变化 介绍Observer时说过,作用是将object的属性变为getter/setter形式,现在Observer类不仅处理Object类型,还要处理Array类型。所以我们要在Observer中新增一些处理,让它能把Array也变成响应式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 export  class  Observer  {     constructor (value ) {         this .value  = value         def (value,'__ob__' ,this )         if (Array .isArray (value)) {             this .observeArray (value)         } else  {             this .walk (value)         }     }          ...          observeArray (items ) {         for (let  i = 0 ,l = items.length ;i < l; i++) {             observe (items[i])         }     } }
 
新增元素的变化 
获取新增元素 
 
我们需要在拦截器中队数组方法的类型进行判断,如果是push、unshift和splice(添加元素方法),需要把参数中新增的元素拿过来,用Observer侦测。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ['push' ,'pop' ,……].forEach (function (method ) {     const  original = arrayProto[method]     def (arrayMethods, method, function  mutator (...args ) {         const  result = original.apply (this , args)         const  ob = this .__ob__          let  inserted         switch (method) {             case  'push' :             case  'unshift' :                 inserted = args                 break              case  'splice' :                 inserted = args.slice (2 )                 break          }         ob.dep .notify ()         return  result     }) })
 
关于数组的问题 对Array的变化侦测是通过拦截原型的方法实现,所以有些操作Vue无法拦截,比如:
1 2 this .list [0 ] = 2 this .list .length  = 0 
 
无法侦测数组变化,不会触发re-render或者watch。
总结 Array的追踪方式和Object不同,因为它是通过方法来改变内容,所以通过创建拦截器去覆盖数组的原型方法来追踪变化。
而为了不污染全局的原型方法,我们在Observer中只针对需要侦测变化的数组用__proto__来覆盖原型方法,而ES6之前并不是所有浏览器都支持,所以针对不支持的浏览器,我们循环拦截器,将方法设置到数组身上来拦截Array.prototype上的原型方法。
Array同Object收集依赖的方式相同,均在getter中,但是使用依赖的位置不同,数组要在拦截器中向依赖发送消息,所以依赖不能像Object一样保存在defineReactive中,而是保存在Observer实例上。
在Observer中队每个侦测了变化的数据都标记__ob__,并把this保存在__ob__上。主要有两个作用,一方面为了标记数据是否被侦测变化(保证同一数据只侦测一次),另一方面可以很方便的通过数据取到__ob__,从而拿到Observer上保存的依赖,发送通知。
除了侦测数组自身变化,数组中元素的变化也要侦测,在Observer中判断如果当前被侦测数据是数组,则调用observerArray将数组中每个元素转换成响应式。
除了已有数据的侦测,当使用push等方法新增数据时,新增的数据也要侦测,我们使用当前操作数组的方法判断,如果是push、unshift和splice,则将参数中的新增数据提取出来,对其转换。
变化侦测相关的api实现原理 vm.$watch 用法 vm.$watch(expOfFn, callback, [options])
用于观察一个表达式或computed函数在vue实例上的变化。回调函数调用时会从参数得到新数据和旧数据,表达式只接受以点分隔的路径,例如a.b.c。
options包括deep和immediate,deep为了发现对象内部值的变化,immediate将立即以表达式的当前值触发回调。
1 2 3 4 vm.$watch('someObject' ,callback, {     deep :true ,      immediate :true   })
 
内部原理 vm.$watch是对Watcher的一种封装,通过Watcher完全可以实现vm.$watch的功能,但它的deep和immediate是Watcher没有的。
1 2 3 4 5 6 7 8 9 10 11 Vue .property .$watch  = function (expOrFn, cb, options ) {     const  vm = this      options = options || {}     const  watcher = new  Watcher (vm, expOfFn, cb, options)     if (options.immediate ) {         cb.call (vm, watcher)     }     return  function  unwatchFn ( ) {         watcher.teardown ()     } }
 
expOrFn是支持函数的,而之前介绍Watcher时没有添加这部分,需要对Watcher简单修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export  default  class  Watcher  {     constructor (vm, expOrFn, cb ) {         this .vm  = vm                           if (typeof  expOrFn === 'function' ) {             this .getter  = expOrFn         } else  {             this .getter  = parsePath (expOrFn)         }                  this .cb  = cb         this .value  = this .get ()     } }
 
如果expOrFn是函数,将它直接赋给getter,如果不是,则用parsePath读取keypath中的数据,keypath是属性路径,例如a.b.c.d就代表从vm.a.b.c.d中读取数据。
而expOrFn是函数时,不止可以动态返回数据,其中读取的数据也都会被Watcher观察。当expOrFn是字符串类型的keypath时,Watcher会读取它指向的数据并观察数据的变化。而expOrFn是函数时,Watcher会同时观察expOrFn函数中读取的所有Vue实例上的响应式数据。
执行new Watcher后,代码会判断用户是否用了immediate参数,使用了则立即执行一次cb。
最后返回一个函数unwatcheFn,它的作用是取消观察数据。用户执行它时,实际上是执行了watcher.teardown()来取消观察数据,其本质是把watcher实例从当前正在观察的状态的依赖列表中移除。
现在需要实现watcher中的teardown方法,来实现unwatch功能。
首先需要在Watcher中记录自己都订阅了谁,当Watcher不想继续订阅时,循环自己记录的列表来通知他们将自己从他们的依赖列表中移除。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 export  default  class  Watcher  {     constructor (vm, expOrFn, cb ) {         this .vm   = vm         this .deps  = []         this .depsIds = new  Set ()         ……     }          ……          addDep (dep ) {         const  id = dep.id          if (!this .depIds .has (id)) {             this .depIds .add (id)             this .deps .push (dep)             dep.addSub (this )         }     } }
 
上述代码中,用depIds判断当前Watcher是否订阅了Dep,不会发生重复订阅。
接着执行this.depIds.add来记录当前Watcher已经订阅了这个Dep。然后执行this.deps.push(dep)记录自己订阅了那些Dep。最后触发dep.addSub(this)将自己订阅到Dep中。
Watcher中新增了addDep方法后,Dep中收集依赖的逻辑也需要改变:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 let  uid = 0 export  default  class  Dep  {     constructor ( ) {         this .id  = uid++         this .subs  = []     }          ……          depend ( ) {         if (window .target ) {             window .target .addDep (this )         }     } }
 
Dep会记录数据发生变化的时候,需要通知哪些Watcher,而Watcher中也记录了自己会被哪些Dep通知,是多对多的关系。
在Watcher中记录了自己都订阅了那些Dep后,可以在Watcher中增加teardown方法来通知订阅的Dep,让他们把自己从依赖中移除:
1 2 3 4 5 6 7 teardown ( ) {     let  i = this .deps .length      while (i--) {         this .deps [i].removeSub (this )     } }
 
1 2 3 4 5 6 7 8 9 10 export  default  class  Dep  {     ……          removeSub (sub ) {         const  index = this .subs .indexOf (sub)         if (index > -1 ) {             return  this .subs .splice (index, 1 )         }     } }
 
deep参数实现原理 deep的 作用是监听它及其子对象的数据,其实就是除了要触发当前被监听数据的收集依赖逻辑外,还要把当前监听值在内的所有子值都触发一遍收集依赖逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 export  default  class  Watcher  {     constructor (vm, expOrFn, cb, options ) {         this .vm  = vm                  if (options) {             this .deep  = !!options.deep          } else  {             this .deep  = false          }                  ……     }          get ( ) {         window .target  = this          let  value = this .getter .call (vm, vm)                           if (this .deep ) {             traverse (value)         }         windwo.target  = undefined          return  value     } }
 
如果使用了deep参数,则在target = undefined前调用traverse来处理deep的逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 const  seenObjects = new  Set ()export  function  traverse (value ) {     _traverse (val, seenObjects)     seenObjects.clear () }function  _traverse (val, seen ) {     let  i , keys     const  isA = Array .isArray (val)     if ( (!isA && !isObject (val)) || (Object .isFrozen (val)) ) {         return      }          if (val.__ob__ ) {         const  depId = val.__ob__ .dep .id          if (seen.has (depId)) {             return          }         seen.add (depId)     }     if (isA) {         i = val.length          while (i--) _traverse (val[i], seen)     } else  {         keys =Object .keys (val)         i = keys.length          while (i--) _traverse (val[keys[i]], seen)     } }
 
利用递归来判断子值的类型,数组则直接循环递归调用_traverse,而对象则利用key读取并递归子值,而val[keys[i]]会触发getter,所以要在target = undefined之前触发收集依赖的原因。
vm.$set 用法 vm.$set(target, key, value)
在object上设置一个属性,如果object是响应式的,Vue保证属性在创建后也是响应式的,并能够触发视图更新。此方法为了避开Vue不能侦测属性被添加的限制。
target 不能是Vue实例或Vue实例的根数据对象。
 
举个例子来看看
1 2 3 4 5 6 7 8 9 10 11 12 13 var  vm = new  Vue ({     el :'#el' ,     template :'#demo-template' ,     data :{         obj     },     methods : {         action ( ) {             this .obj .name  = 'abc'          }     } })
 
调用action方法时,会为obj新增一个name属性,但Vue不会得到任何通知,新增的属性也不是响应式的,Vue不知道这个obj新增了属性等同于不知道我们使用了array.length = 0来清空数组一样。而vm.$set就用来解决这类问题。
Array的处理 首先创建set方法,并规定接收与$set规定的参数一致的三个参数,并对数组进行处理。
1 2 3 4 5 6 7 export  function  set (target, key, val ) {     if (Array .isArray (target) && isValidArrayIndex (key ) {         target.length  = Math .max (target.length , key)         target.splice (key, 1 , val)         return  val     } }
 
上述代码中,如果target是数组并且key是有效的索引值,就先设置length属性,这样如果我们传的索引值大于length,就需要让target的lenth等于索引。接下来通过splice方法把val设置到target的指定位置,当我们使用splice方法时,数组拦截器会侦测到target变化,自动把新增的val转换成响应式的,最后返回val。
key已经存在target中 因为key已存在target中,所以它已经被侦测了变化,此时修改数据直接用key和val就好,修改的动作会被侦测到。
1 2 3 4 5 6 7 8 9 10 11 12 export  function  set (target, key, val ) {     if (Array .isArray (target) && isValidArrayIndex (key ) {         target.length  = Math .max (target.length , key)         target.splice (key, 1 , val)         return  val     }          if (key in  target && !(key in  Object .prototype  )) {         target[key] = val         return  val     } }
 
处理新增的属性 现在处理在target上新增的key:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 export  function  set (target, key, val ) {     if (Array .isArray (target) && isValidArrayIndex (key ) {         target.length  = Math .max (target.length , key)         target.splice (key, 1 , val)         return  val     }          if (key in  target && !(key in  Object .prototype  )) {         target[key] = val         return  val     }                    const  ob = target.__ob__      if (target._isVue  || (ob && ob.vmCount )) {         process.env .NODE_ENV  !== 'production'  && warn (             'Avoid adding reactive properties to a Vue instance or its root $data'  +              'at runtime - declare it upfront in the data option'          )         return  val     }          if (!ob) {         target[key] = val         return  val     }          defineReactive (ob, value, key, val)     ob.dep .notify ()     return  val }
 
上述代码,首先获取了target的__ob__属性,然后处理边界条件即“target不能是Vue实例或Vue实例的跟数据对象”和target不是响应式的情况。若果它身上没有__ob__,则不是响应式的,直接用key和val设置即可。如果前边所有条件都不满足那么说明这是新增的属性,使用defineReactive将新增的属性转成getter/setter即可。
vm.$delete vm.$delete的作用是删除数据中的某个属性,因为Vue2采用Object.defineProperty实现监听,delete关键字删除无法侦测到。
用法 vm.$delete(target , key)
删除对象的属性,如果对象是响应式的,需要确保删除能更新视图。同样的,目标不能是Vue实例或Vue实例的跟数据对象。
 
实现原理 vm.$delete的实现原理和上述代码类似,删除属性后向依赖发消息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 export  function  del (target, key ) {     if (Array .isArray (target) && isValidArrayIndex (key)) {         target.splice (key , l)         return      }          const  ob = (target).__ob__      if (target._isVue  || (ob && ob.vmCount )) {         process.env .NODE_ENV  !== 'production'  && warn (             'Avoid deleting properties on a Vue instance or its root $data'  +              '- just set it to null.'          )         return      }          if (!hasOwn (target, key) ) {         return      }     delete  target[key]     if (!ob) {         return      }     ob.dep .notify () }