-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Vue 2中array observer和$set如何实现 #7
Comments
总结的很好哇! 父级进行依赖收集以后,如果子集是对象或数组,那么子集也进行一次收集,这样Vue.set等方法就可以使用dep.notify方法来更新watcher。 |
我重新组织思路: 数组比较特殊, 这些问题其实也是由于js的数据类型特性(引用数组类型)带来的,所以就强调单向数据流、prop 原子化,如果很难原子化,不管什么情况都不建议直接修改prop,这样也就能尽量避免这种无法预知的update。
|
对的,第一句话没有问题,第二句话其实不算是一个问题,因为只要保证data选项返回的是一个纯对象,那么用户真正定义的属性和值肯定是有父级的,源码中在data初始化的时候也进行了判断,data选项必须返回一个纯对象{},这是一个强制约定: // 这种情况肯定有父级
export default {
data() {
return {
arr: [1,2,3],
obj: {}
}
}
}
// 这种情况是不被允许的
// 从使用的角度上将,vue把data的数据都挂载到实例上,比如this.arr,那么下面这种写法也读取不到,难道用`this.$data`吗,哈哈,就算这样,没有收集到依赖,也没法触发更新。
export default {
data() {
return [1,2,3,4]
}
} |
最近发现了一个比较大的认知错误,一直以为vue官方说不支持数组的索引进行get和set就是因为
Object.defineProperty
不支持数组索引的拦截,这个认知真是打错特错了,自动动手测试了一下,数组索引是可以拦截的,静下心来思考一下,本来在js中数组就是一种特殊的对象,数组的索引其实就是对象的属性,理应和对象的行为是一样的,通过这个问题我们得到一个教训:一定要自己实践去验证,要不然误人子弟!!!既然如此,那么vue 2为什么没有对数组的索引定义响应式属性呢?这个问题在这个文章中有一定结论,总的来说:性能代价和实际用户体验的权衡,未来我也会专门写一篇文章来探讨一下。所以下面探讨的内容的动机也就变成了:vue 2中对于数组索引没有使用get/setter,那么如何实现自动响应呢?
可能大家都比较了解vue 2基于Object.defineProperty
实现的响应式原理,以及通过getter/setter
实现自动依赖收集的过程,不过Object.defineProperty
这个api有一些缺陷:只能对对象的属性进行拦截不支持数组索引的访问器定义属性必须先初始化才能定义拦截虽然Object.defineProperty
有上述的缺陷,但是vue 2还是实现了几乎全场景的自动响应和依赖收集(只限对象和数组,数组不能索引操作)$set
$set
可以实现对于某个对象新增响应式属性,并触发一次notify
,而且对于数组也是有效的,那么它到底是如何确保watcher
能够收集数组的依赖而进行更新呢?让我们在回顾一下响应式对象的定义和依赖收集过程
1. 响应式对象(数组)定义
响应式对象定义的实现是通过上面的
Observer
类属性:
方法:
实例化过程中,判断
value
类型,如果是对象,则对对象的的属性定义访问器属性,如果是数组,则进行其他特殊处理。了解vue 2响应式的同学可能都知道,在定义对象的访问器属性时,对于每个属性都会定义一个
Dep
实例,并存在闭包当中,用于后续的属性触发依赖收集和通知更新(notify
),但是定义响应式对象的构造器为什么也会存在一个Dep
实例呢?这个是我一直没有搞明白的地方。同时疑问也来源于
getter
里的这段代码:正如上面这段代码中注释的位置,我一直搞不明白这里是为了做什么。
疑问:watcher对于该属性的依赖已经收集了,为什么还要对属性值为对象的进行依赖收集呢?
这个疑问存在了好久,当某次再次重温这段代码的时候,突然想到,既然这里可以收集依赖,那么将来必定会有通知(
notify
)依赖更新的逻辑,所以全局搜索的了一下notify
方法名,出现的位置(排除setter
里的):$set
原始实现$del
原始实现我们先来研究一下
$set
方法这个方法的主要逻辑是对一个对象的属性进行扩展,如果对象中已经存在该属性,则直接设置新值后返回,如果没有这个这个属性,首先判断这个对象是不是一个响应式对象(
__ob__
),如果不是,直接设置属性和值后返回,如果是响应式对象,则对这个新属性设置访问器属性。然后通知这个依赖于该对象的watcher
进行更新(ob.dep.notify
)在
$set
方法里我们发现了ob.dep.notify
的调用,这个dep
就是在Observer
实例中针对响应式对象的Dep
实例,这既然有notify
,那就意味着dep
会在某个时机触发依赖收集,那么Observer
实例中的dep
在哪个环节进行的依赖收集呢?还记得刚才提出的疑问代码吗,不错,就是上面getter
里的逻辑,让我们再来看一下:正是这段逻辑触发了依赖收集,具体分析下:
childOb
是对value
进行响应式处理(构造Observer
),如果childOb
存在,说明value
是一个对象或数组,当属性读操作正好读取到这个value
的时候,watcher
除了收集属性本身的依赖,顺便也针对这个value
对象进行收集,只有这样,在运行时针对这个value
进行$set
操作时,才能正确的通知watcher
更新。本身对于
data
选项还有一个局限性:data
本身必须返回的是纯对象([Object object]
),不能是一个数组,因为数组无法对下标设置访问器属性,这个在data
初始化时已经进行了处理:同理,
$del
与$set
原理类似:Array Observer
开篇我们提到了,那么vue2是如何实现数组修改的自动响应呢?Object.defineProperty
不支持数组vue 2通过对数组的一些原始方法的代理,实现了数组的响应,但是对于数组的索引操作还是无能为力,所以vue2建议操作数组使用数组的原生方法进行操作。
下面我们来了解一下vue 2如何对数组的方法进行代理,并如何进行依赖收集的
回到
Observer
类(只保留了数组的处理):实例化
Observer
类时,会判断value
是否为数组,当为数组时对数组进行了如下处理:不管是
protoAugment
还是copyAugment
都是为了改写需要代理的数组方法:我们看一下如何进行方法代理:
这里逻辑比较简单,就是常规对于方法的重写代理,关键点在于对于进行依赖通知以及新添加元素的响应式处理。
observeArray
方法,对数组的每一项进行响应式处理,这样做也是为了手动跳过数组索引不能拦截的问题,直接访问数组每一项并尝试进行响应式注册:notify
逻辑比较容易理解,那么数组时如何进行依赖收集的呢?其实在探究$set
时已经出现答案了,当属性访问器触发getter
时,会进行该属性的依赖收集,同时如果属性值为对象或数组,会同时触发属性值(Observer.dep
)本身的依赖收集,数组更为特殊,不仅触发自己的依赖收集,还会对数组的每一项触发收集。为什么需要对数组的每一项执行收集呢?因为数组不像对象那样属性被代理后同时收集
value
对象的代理,因为数组每一项的访问拦截是断层的,arr[i]不会触发get,如果此项是一个对象,此对象不会触发依赖收集,由于数组本身依赖收集就需要对象属性访问的支持,所以在数组进行收集时,必须也对数组的每一项(对象)进行依赖收集,这样做就绕过数组每一项的访问无法被拦截的问题而直接访问每一项并收集当前依赖,如果不这样做,对于数组项中的对象进行$set
操作就会失效
,因为该对象没有被依赖,就无通知可发,但是$set新增的属性有可能真的在这个组件中使用,这就会导致组件没有更新,这样的错误是不应该出现的!。总结
为了实现数组的响应式处理,在
Observer
类中也存在一个Dep
实例进行依赖收集,这样可以通过一定约束(根data
必须为纯对象),在对属性进行依赖收集时同时对属性值为对象或数组类型的进行Observer
构造,使value
也能收集依赖,根本上来说这是为了解决数组响应式才采取的方案,通过这个方案,同时可以实现对对象和数组的运行时进行 $set 和 $del 的能力,不得不惊叹尤大的巧妙设计!The text was updated successfully, but these errors were encountered: