在阅读Vue3
的触发更新trigger
函数中对于数组新增key
索引中有这样一段hack代码。
switch (type) {
case "add" /* ADD */:
if (!isArray(target)) {
add(depsMap.get(ITERATE_KEY));
if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY));
}
}
else if (isIntegerKey(key)) {
// new index added to array -> length changes
add(depsMap.get('length'));
}
break;
case "delete" /* DELETE */:
if (!isArray(target)) {
add(depsMap.get(ITERATE_KEY));
if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY));
}
}
break;
case "set" /* SET */:
if (isMap(target)) {
add(depsMap.get(ITERATE_KEY));
}
break;
}
复制代码
简单来聊聊v3
中的这段代码,实质上是在做触发更新的一些hack
处理。来看看这里:
case "add" /* ADD */:
if (!isArray(target)) {
add(depsMap.get(ITERATE_KEY));
if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY));
}
}
// 主要留意这段代码
else if (isIntegerKey(key)) {
// new index added to array -> length changes
add(depsMap.get('length'));
}
break;
复制代码
你可以这样理解这段代码,当我们在页面中定义了一个响应式的数组时
import { reactive } from 'vue'
const a = reactive({
arr:[1,2,3]
})
// 假使模版中已经使用了a.arr 进行过来依赖收集
// 当我改变它的值,为她新增一个索引
a.arr[5] = 'wang.haoyu'
复制代码
**我们知道在V3中Vue已经支持对于修改数组下标的响应式支持了。**此时就会进入上边的逻辑中。
当满足类型是add
时,并且新增的是数组的一个索引。我们明白为数组新增一个索引一定是会该改变length
属性,所以这里调用了add(depsMap.get('length'));
进行添加更新effect
函数。这没有任何问题。
但是你有没有想过,当我们在模板中这样使用呢?
<template>
<div>
{{ obj.arr }}
</div>
</template>
<script>
import { reactive } from 'vue'
export default {
setup() {
const obj = reactive({
arr:[1,2,3]
})
setTimeout(() => {
obj.arr[10] = 'wang.haoyu'
}, 2000);
return {
obj
}
}
}
</script>
复制代码
我们在模板中直接调用了obj.arr
,它是一个数组。当我们新增数组arr
的索引的时候,首先按照vue3
中的依赖收集。他会对与整个数组进行依赖收集。
当我们为arr
新增一个索引的时候,按照上边的逻辑会触发到这里
else if (isIntegerKey(key)) {
// new index added to array -> length changes
add(depsMap.get('length'));
}
复制代码
他会去depsMap
中去找(depsMap
就是关于数组所有依赖收集对象的一个map
对象)length
属性,从而添加关于length
属性收集的effect
去触发更新。
可是我明明是为数组新增了一个索引,而且我在模板中使用的是obj.arr
整个数组对象。为什么它的length
属性就会被依赖收集了呢?
这个其实就源自于Symbol.toPrimitive
在javascript
引擎中,当执行特定操作时,经常会尝试对对象转化到相应的原始值,例如,比较一个字符串和一个对象,如果使用双等号==
运算符,对象会在比较操作执行前被转换为一个原始值。到底使用哪一个原始值以前是由内部操作决定的。在ES6中,通过Symbol.toPrimitive
方法可以更改那个暴露触发的值。
Symbol.toPrimitive
方法呗定义在每一个标准类型的原型上,并且规定了当对象被转化为原始值时当执行的操作。每当执行原始值转换时,总会调用Symbol.toPromitive
方法传入一个值作为参数,这个值在规范中被称为类型提示hint
,类型提示参数的值有三种选择:number
,string
或者default
,传递这些参数的时,Symbol.toPrimitve
返回的分别是:数字,字符串或无类型偏好的值。
对与绝大多数标准对象,数字模式有以下特性,根据优先级顺序排序如下:
valueOf
方法,如果结果为原始值则返回。toString()
防范,如果为原始值,则返回。同样,对与大多数标准对象,字符串模式具有以下优先级:
toString()
方法,如果结果为原始值则返回。valueOf
方法,如果结果为原始值,则返回。如果自定义
Symbol.toPrimitive
方法则可以覆盖这些默认的强制转化特性。
这个时候大家应该大概已经明白了,当我们在模板中调用obj.arr
访问整个数组的时候,vue中
首先会调用这个数组的Symbol.toPrimitive
方法将它转化为字符串,也就是调用数组的toString()
方法,再将结果进行依赖收集。
当调用数组的toString()
方法的时候,究竟发生了什么。我们在看看
const arr = [1, 2];
const proxy = new Proxy(arr, {
get: (target, key, receiver) => {
console.log(key);
return Reflect.get(target, key, receiver);
},
});
proxy.toString()
// 打印结果
toString
join
length
0
1
复制代码
可以看到数组的toString
方法内部其实首先调用join
方法,然后访问length
以及数组中每一个元素,最终转为为1,2
。
这个时候其实就很清晰了。
obj.arr
访问数组obj.arr.prototype[Symbol.toPrimitive]
尝试将obj.arr
转为字符串toString
方法arr.toString()
方法会访问数组中的每一个元素和它的length进行join
。这个时候我们可以看到,当在模板中访问整个数组进行依赖收集的时候,实质上vue3
中将整个数组的转化成为了字符串类型调用了内部Symbol.toPrimitive
方法。 从而依赖手机中对与这个数组的每一项以及对应length
进行了依赖收集,此时当数组新增一个索引。v3
中手动调用了数组中的length
去触发对应更新。
新增索引一定会修改数组长度,当模版中访问整个数组将数组转为String时候,对与长度进行了依赖收集。所以触发更新时,新增索引就会触发数组的更新。
留下一个问题之后去解决,在vue
中如果在模板中使用一个对象比如{{ obj }}
,(const obj = { name:wang,haoyu }
)。
平常如果我们直接app.innerHTML = obj
,页面中div
中显示的是[Object object]
,而在vue
模板中显示的是name:wang.haoyu
。不知道大家有没有注意过这个小细节。
那么内部是不是和Vue
内部重写了Symbol.toPrimitive
有关呢?大家可以先猜一猜