变化侦测
什么是变化侦测?
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 () }