前言
相信大家都知道,在vue2.0x中,使用数组下标改变值时,是不会触发响应式的
以下来自:Vue官方文档
Vue 不能检测以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:vm.items.length = newLength
但是其实还是有特殊情况的,让我们来分析分析
正常情况
让我们看看,使用数组下标直接改变数组元素的值,是否会响应式变化(按照官方文档是不可行的)
<div id="app">
<ul>
<li v-for="item in list">
{
{item}}
</li>
</ul>
</div>
const app = new Vue({
el: '#app',
data: {
list: ['4', '5', '6']
},
mounted () {
this.list[1] = '6'
console.log(this.list)
}
})
这里通过this.list[1],也就是数组下标改变数组值时,让我们看看是否会响应式变化
下面为页面中的打印结果
通过打印结果,可以看出,this.list[1]是成功改变了data中list数组下标为1的值的,因为打印出的list的索引为1的值为"6"了
但是在页面中,并没有将这个6渲染出来,所以可以得出这一个赋值不是响应式的,并不会让界面跟着渲染
所以可以得出结论
通过数组下标改变元素值时,是不会响应式变化的
误区
而这个时候,会有些人走入一个误区
这个误区,可以看看代码
<div id="app">
<ul>
<li v-for="item in objectList">
{
{item}}
</li>
</ul>
</div>
const app = new Vue({
el: '#app',
data: {
objectList: [
{ value: 1, id: 1 },
{ value: 2, id: 2 },
{ value: 3, id: 3 },
]
},
mounted () {
this.objectList[1].value = 3
}
})
打印结果如下
这里就是大多数人的误区:你看!我用的也是数组下标,为什么这样改变的就会是响应式的呢?
其实这就是被官方文档绕进去了,看到数组下标就以为不是响应式,其实这里的数组下标只是获取到那个对象而已,而这个对象却是响应式的,所以你改变这个对象的值,当然也就响应式的变化了
那么进入误区的人,现在又进入了一个难以理解的点,为什么数组的对象元素就是响应式的呢?
这时候,可以从源码中来看
数组中对象元素的响应式
说到响应式,之前也分析过,从initData中执行observe函数,那么从observe开始看
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 判断是否为对象 判断是否为VNode
if (!isObject(value) || value instanceof VNode) {
// 如果不是对象 或者 是实例化的Vnode 也就是vdom
return
}
// 观察者 创建一个ob
let ob: Observer | void
// 检测是否有缓存ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
// 直接将缓存的ob拿到
ob = value.__ob__
} else if (
// 如果没有缓存的ob
shouldObserve && // 当前状态是否能添加观察者
!isServerRendering() && // 不是ssr
(Array.isArray(value) || isPlainObject(value)) && // 是对象或数组
Object.isExtensible(value) && // 是否可以在它上面添加新的属性
!value._isVue // 是否是Vue实例
) {
// new 一个Observer实例 复制给ob
// 也是把value进行响应化,并返回一个ob实例,还添加了__ob__属性
ob = new Observer(value)
}
// 如果作为根data 并且当前ob已有值
if (asRootData && ob) {
// ++
ob.vmCount++
}
// 最后返回ob,也就是一个Obesrver实例 有这个实例就有__ob__,然后其对象和数组都进行了响应化
return ob
}
刚开始初始化时,内部肯定是没有ob这个属性的,所以会执行new Observer
因此继续看class Observer的构造函数
constructor (value: any) {
this.value = value
// 这里会new一个Dep实例
this.dep = new Dep()
this.vmCount = 0
// def添加__ob__属性,value必须是对象
def(value, '__ob__', this)
// 判断当前value是不是数组
if (Array.isArray(value)) {
// 如果是数组
// 检测当前浏览器中有没有Array.prototype
// 当能使用__proto__时
// 这里完成了数组的响应式,不使用这7个方法都不会触发响应式
if (hasProto) {
// 有原型时 将arrayMethods覆盖value.__proto__,也就是把增加了副作用的7个数组方法放了进来
protoAugment(value, arrayMethods)
} else {
// 复制增加了副作用的7个数组方法
copyAugment(value, arrayMethods, arrayKeys)
}
// 遍历将数组所有元素进行observe
this.observeArray(value)
} else {
// 不是数组是对象,执行这里
// walk就是给对象的所有key进行响应化
this.walk(value)
}
}
分析如图
可以看到,如果是数组,会首先对7个数组方法进行添加副作用,然后执行observeArray函数
所以继续看observeArray这个函数
// 遍历将数组所有元素进行observe
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
这段代码大家应该都能一下就知道在干啥,遍历数组,对所有数组元素执行observe函数
那么继续看observe函数
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 判断是否为对象 判断是否为VNode
if (!isObject(value) || value instanceof VNode) {
// 如果不是对象 或者 是实例化的Vnode 也就是vdom
return
}
// 观察者 创建一个ob
let ob: Observer | void
// 检测是否有缓存ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
// 直接将缓存的ob拿到
ob = value.__ob__
} else if (
// 如果没有缓存的ob
shouldObserve && // 当前状态是否能添加观察者
!isServerRendering() && // 不是ssr
(Array.isArray(value) || isPlainObject(value)) && // 是对象或数组
Object.isExtensible(value) && // 是否可以在它上面添加新的属性
!value._isVue // 是否是Vue实例
) {
// new 一个Observer实例 复制给ob
// 也是把value进行响应化,并返回一个ob实例,还添加了__ob__属性
ob = new Observer(value)
}
// 如果作为根data 并且当前ob已有值
if (asRootData && ob) {
// ++
ob.vmCount++
}
// 最后返回ob,也就是一个Obesrver实例 有这个实例就有__ob__,然后其对象和数组都进行了响应化
return ob
}
分析如图
可以看到,observe进行了一次处理,不是对象的他就不会执行observe去添加ob属性
如果是对象的话,就会执行new Observer,这时候又回到之前的那个构造函数,只不过这次执行的下面的else分支,这次不是数组了,所以会执行this.walk
如图
其实walk就是给对象所有的key进行defineReactive添加数据劫持(这里不细说,反正这样已经完成响应式了,不懂得可以看这篇)
所以数组中对象元素都是响应式的,因此下次再碰到这种情况,就不要再进入误区了,这里并没有通过数组下标去改变值,而是获取相应的对象,而这个对象是响应式的!
又一个误区
还有一个误区,也让很多人认为通过数组下标改变值会是响应式,直接看代码
<div id="app">
<ul>
<li v-for="item in objectList">
{
{item.value}}
</li>
</ul>
<ul>
<li v-for="item in list">
{
{item}}
</li>
</ul>
</div>
const app = new Vue({
el: '#app',
data: {
objectList: [
{ value: 1, id: 1 },
{ value: 2, id: 2 },
{ value: 3, id: 3 },
],
list: ['4', '5', '6']
},
mounted () {
this.list[1] = '6'
this.objectList[1].value = 3
}
})
这里一个通过数组下标改变了一个值,又通过数组下标获取一个对象并改变对象的值
由之前得出的结论,第一个值的改变是不会响应式变化的,第二个值的改变会响应式变化
所以页面上应该显示1,3,3, 4, 5,6
但是打印结果如下
阿这,跟我们预想的不一样啊,为什么通过数组下标的this.list[1] = '6’响应式变化了呢?
这就是又一个误区
其实这里我们可以在this.list[1]='6’后,打印一下this.list
mounted () {
this.list[1] = '6'
console.log(this.list)
this.objectList[1].value = 3
}
可以看到,这时的list值是成功改变的了,只是没有响应式的渲染在页面上
所以继续执行后面的this.objectList[1].value = 3时,这是一个响应化的操作,因此当值改变时,会触发setter中的dep.notify,去通知视图更新,经过一系列vdom,patch后,vue会发现data中有个list数组中一个元素值也改变了,因此也会将当前改变的值和list数组中改变的那个值都给重新渲染了
因此这里的数组下标改变值的响应化其实是后一句执行 this.objectList[1].value = 3,这一句通知视图更新时,会检测到前一个list数组中值有变化,但是视图中没更新,因此才会一起渲染
实现数组下标改变值的响应式
其实,vue2.0x是可以实现数组下标改变值的响应式的,且非常简单
前面我们也知道,通过数组下标改变值,能成功改变data中的值,但是因为没有监听,因此不会触发响应式更新
那么我们可以通过添加属性监听完成这一操作,众所周知,数组索引也是数组的一个属性,因此让我们重写一下对数组的操作
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// this.observeArray(value)
this.walkArray(value)
} else {
this.walk(value)
}
}
walkArray (obj: Object) {
obj.forEach((item, index) => {
defineReactive(obj, index)
})
}
我把原本的observeArray注释了,换成了自己写的walkArray,而walkArray就是实现了对索引的监听。
这时候就已经实现成功了,看看效果
<div id="app">
<ul>
<li v-for="item in list">
{ { item}}
</li>
</ul>
</div>
const app = new Vue({
el: "#app",
data: {
list: ['1', '2', '3']
},
mounted (){
this.list[1] = 4
}
})
来看看这次,通过数组下标改变值会不会响应式变化
打印如下图
好的,成功实现了!
疑问
这时肯定都会有个疑问,如果这么简单就能实现这个数组下标的响应式的话,为什么尤大不写进去呢?
这里尤大有回答过:为什么vue没有提供对数组属性的监听?
既然尤大都说了性能问题,那不写入数组下标响应式肯定是他们经过考量之后的决定了
所以现在只要知道怎么实现以及避免这两个误区即可
总结
这也是在群里有人问了之后,我也踩进了误区,然后经过看源码后,算是对其有了清晰的认识了
所以作为一篇随笔写在这分享给大家,也算是避雷吧哈哈哈