vue源码(十三) 数组下标改变值的响应式误区以及实现

前言

相信大家都知道,在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],也就是数组下标改变数组值时,让我们看看是否会响应式变化

下面为页面中的打印结果

vue源码(十三) 数组下标改变值的响应式误区以及实现_第1张图片
vue源码(十三) 数组下标改变值的响应式误区以及实现_第2张图片
通过打印结果,可以看出,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)
  }
}

分析如图

vue源码(十三) 数组下标改变值的响应式误区以及实现_第3张图片
可以看到,如果是数组,会首先对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
}

分析如图

vue源码(十三) 数组下标改变值的响应式误区以及实现_第4张图片
可以看到,observe进行了一次处理,不是对象的他就不会执行observe去添加ob属性

如果是对象的话,就会执行new Observer,这时候又回到之前的那个构造函数,只不过这次执行的下面的else分支,这次不是数组了,所以会执行this.walk

如图

vue源码(十三) 数组下标改变值的响应式误区以及实现_第5张图片

其实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

但是打印结果如下

vue源码(十三) 数组下标改变值的响应式误区以及实现_第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没有提供对数组属性的监听?

vue源码(十三) 数组下标改变值的响应式误区以及实现_第7张图片
在这里插入图片描述
既然尤大都说了性能问题,那不写入数组下标响应式肯定是他们经过考量之后的决定了

所以现在只要知道怎么实现以及避免这两个误区即可

总结

这也是在群里有人问了之后,我也踩进了误区,然后经过看源码后,算是对其有了清晰的认识了

所以作为一篇随笔写在这分享给大家,也算是避雷吧哈哈哈

你可能感兴趣的:(vue,javascript,源码,vue,javascript,源码,数组,响应式)