我似乎发现了vue的一个bug

字节逆旅

共 9676字,需浏览 20分钟

 · 2021-08-03


公元2021年7月23日,我以为我发现了vue的一个bug,此时此刻,我离给vue提issue只有1个字节的距离。

用过vue的同学应该都知道Vue.set这个api的用法吧,来,今天教你一个"新玩法"。。

事件还原

事情得从同事的一行代码说起,看这里:

if (data.result.length > 0) {
        data.result.forEach(item1 => {
          this.assoStatList.forEach((item2, index2) => {
            if (item1.LGTD == item2.LGTD && item1.LTTD == item2.LTTD) {
              // this.$set(item2, 'RAIN_FORECAST_24H', item1.RAIN_FORECAST_24H)
              // item2.RAIN_FORECAST_24H = item1.RAIN_FORECAST_24H
              // this.$set(this.assoStatList, index2, item2)
              this.$set(this.assoStatList, item2, (item2.RAIN_FORECAST_24H = item1.RAIN_FORECAST_24H))
              console.log(this.assoStatList)
              console.log(item2)
            }
          })
        })
      }

划重点:

this.$set(this.assoStatList, item2, (item2.RAIN_FORECAST_24H = item1.RAIN_FORECAST_24H))

就是这行代码,我觉得写错了,因为参数传得不对了。

话不多说,来看官方文档:

显然,同事这个写法明显是有问题的,按文档意思,第一个参数如果是数组,那第二个应该给索引值,他这里居然给了个对象!我难以忍受,甚至手把手地教他怎么按文档来,然后用两种正确方式都写出来了。

vue出bug了?

但是同事坚持说他没有错,然后执行给我看,结果确实没有报错,而且正常执行了!?后面在用到this.assoStatList的代码可以正常执行。这让我相当的尴尬!

我又仔细看了文档,上面写得很清楚,第二个参数要么是字符串键值,要么是索引,这取决于你要操作的对象即第一个参数是对象还是数组;第三个参数没做限制,写个函数也没问题。

这矛盾的情况让我百思不得其解,直觉告诉我,这里面有问题:

这张图是我打印的被赋值过的数组this.assoStatList,结果发现这个数组除了正常的索引元素,还多了一个属性[object Object],属性值为0,而这个0就是item1.RAIN_FORECAST_24H带过来的值。

image.png

从这张图可以看出来,第三个参数的赋值操作成功了,但是同时也给数组this.assoStatList加了一个[object Object]属性,这个属性其实是多余的,并不是我要的。

image.png

所以如果严格按照文档来,这种写法肯定是错误的,只是钻了空子,没报错而已。

至于这种写法为什么会不报错,我本着认真的钻研精神开始了下面的一系列分析,先从vue源码来看。

源码分析

源码位置:src/core/observer/index.js

我从vue2中找到set定义的完整代码:

/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */

export function set (target: Array<any> | Object, key: any, val: any): any {
  // target如果是`undefined`、`null`或是原始类型,则报错
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // 如果target是数组且key是数组索引,则修改数组对应键的值
  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: any).__ob__
  // 如果是Vue对象或者是Vue实例的根数据对象,则报错
  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方法赋值
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

看源码,我加上了备注,还是很容易看明白的。在我们这种条件下,target是数组但key不是索引值,代码最后其实会走到defineReactive,那就顺藤摸瓜继续找。。

源码位置:src/core/observer/index.js

class Observer {
    constructor (data) {
        this.walk(data)
    }
    walk (data) {
        // 遍历 data 对象属性,调用 defineReactive 方法
        let keys = Object.keys(data)
        for(let i = 0; i < keys.length; i++){
            defineReactive(data, keys[i], data[keys[i]])
        }
    }
}

// defineReactive方法仅仅将data的属性转换为访问器属性即响应式
function defineReactive (data, key, val{
    // 递归观测子属性
    observer(val)

    Object.defineProperty(data, key, {
        enumerabletrue,
        configurabletrue,
        getfunction ({
            return val
        },
        setfunction (newVal{
            if(val === newVal){
                return
            }
            // 对新值进行观测
            observer(newVal)
        }
    })
}

// observer 方法首先判断data是不是纯JavaScript对象,如果是,调用 Observer 类进行观测
function observer (data{
    if(Object.prototype.toString.call(data) !== '[object Object]') {
        return
    }
    new Observer(data)
}

这段代码的意思就是给响应式属性设置值,我加上了注释,应该容易看明白,不过重点在这一步:Object.defineProperty!请接着往下看:

终极奥义?

上面的代码在调用Object.defineProperty方法时,就会把对象类型的键值转换为'[object Object]'类型的字符串,最后给数组加了一个'[Object object]'属性。不明白的可以看下mdn上的defineProperty定义[2],然后走下这段代码:

let Person = [1,2]
Object.defineProperty(Person, {sL:1}, {
   value'jack'// 属性值
   writabletrue // 是否可以改变
});
Person.s = 2;
console.log(Person) // logs  [1, 2, s: 2, [object Object]: "jack"]

输出的结果非常奇葩!直观看起来,[object Object]: "jack"似乎也是一个值,但是仔细一想又不是,因为它不是一个对象,你把[1, 2, s: 2, [object Object]: "jack"]输入控制台是会报错的,但是Person['[object Object]']又可以取到值jack,然后你再看一下Person的length,你会发现,长度却是2!我把Person展开来看,发现是这样的:

image.png

这样看应该比较明白了,你可以把Person理解为特殊的对象,数组本身的索引值也是这个“对象”的键值,"s"也是键值,[object Object]也是键值。为了谨慎起见,我又打印了它的键值:

Object.keys(Person)  // ["0", "1", "s"]

看到这结果,是不是又被惊讶到了?刚刚明明似乎看到了[object Object]这个键值,结果在这里却没有被打印出来!?好吧,神奇的js。。。我猜也许是因为[object Object]这个键值的特殊性吧,而且你用for in 去遍历Person的属性,也是遍历不到[object Object]的,也就是说这个属性不是可枚举的。

[object Object]

最后的最后还是要说一下[object Object],它是怎么来的呢?其实它是通过toString[5]方法得到的,mdn上对它是有说明的,就是在默认情况下任何对象调用toString()都会返回返回 "[object type]"。可以看下面的代码:

var s = new Object();
s.toString() // [object Object]

但是我们知道数组也有toString方法,它覆盖了默认的toString方法,所以并不会输出"[object Array]",如果要输出这个结果,可以这样写:

var a = new Array();
toString.call(a) 

回到上一步,也就是说Object.defineProperty实际上是把它的第二个参数强制toSring了,所以在文章最开始的地方,我们执行这句this.$set(array, object, ())会得到一个包含[object Object]属性的数组。

要不要提issue?

好了,本文有点冷门,本来只是怀疑找到了vue的一个bug,结果搞出了这么多瓜来,把我吃撑了都!但是话说回来,如果api规定了参数及类型,入参传错了,即使执行不报错,从严格上来讲也要警告才对,所以,要不要提issue呢?

参考资料:

    1. https://blog.csdn.net/leelxp/article/details/107212555

    2. https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

    3. https://www.jianshu.com/p/8fe1382ba135

    4. http://hcysun.me/2017/03/03/Vue%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0/

    1. https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/toString

    2. https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/in


扫码关注 字节逆旅 公众号,为您奉献更多技术干货!


浏览 76
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报