Skip to content
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

vue3 中的 arrayInstrumentations 数组查找方法解析 #56

Open
jiangjiu opened this issue Apr 24, 2020 · 4 comments
Open

vue3 中的 arrayInstrumentations 数组查找方法解析 #56

jiangjiu opened this issue Apr 24, 2020 · 4 comments

Comments

@jiangjiu
Copy link
Owner

问题

在阅读vue3的源码过程中,遇到了很多有意思的代码,比如以下这一段:

const arrayInstrumentations: Record<string, Function> = {}
;['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
  arrayInstrumentations[key] = function(...args: any[]): any {
    const arr = toRaw(this) as any
    for (let i = 0, l = (this as any).length; i < l; i++) {
      track(arr, TrackOpTypes.GET, i + '')
    }
    // we run the method using the original args first (which may be reactive)
    const res = arr[key](...args)
    if (res === -1 || res === false) {
      // if that didn't work, run it again using raw values.
      return arr[key](...args.map(toRaw))
    } else {
      return res
    }
  }
})

这是一组数组方法,当vue3遇到includes/indexOf/lastIndexOf这些数组标识方法时,要特殊处理。

看到这个方法的时候是有点懵的:

  1. 万恶的this是个什么鬼?
  2. includes/indexOf这些方法和Proxy又有什么关系?
  3. 遇到这些方法为何要自行遍历原始元素,再去track所有的元素呢?
  4. 最后要用两次元素查询是为了什么?

this

 if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

可以看到这里使用了Reflect.get执行操作,这个方法的签名为Reflect.get(target, propertyKey[, receiver])

第三个参数receiver: 如果target对象中指定了getter,receiver则为getter调用时的this值,所以上文出现的this其实是这里的target对象,也就是那个reactive的proxy对象。

const arr = toRaw(this) as any即获取了当前响应式对象指向的那个原始数组。

includes/indexOf这些方法和Proxy有什么关系

实际上,并不是arr[0]这样直接获取元素的时候才会触发proxy的getter,任何对数组元素、方法的访问都会。

let handler = {
  get(target, key, receiver) {
  	console.log('get操作', key);
    return Reflect.get(target, key);
  },
  set(target, key, value, receiver) {
  	console.log('set操作', key, value);
    return Reflect.set(target, key, value);
  }
};


let proxyArray = new Proxy([1, 2, 3], handler);
proxyArray.push(4);
// get操作 push
// get操作 length
// set操作 3 4
// set操作 length 4

proxyArr.includes(3)
// get操作 includes
// get操作 length
// get操作 0
// get操作 1
// get操作 2
// true

我们对proxy的数组做push includes操作,可以看到控制台输出了很多信息。

当你使用这三个标志方法去查找元素时,就遇到了一个问题:这个数组里是否存在reactive对象?他们是不是和raw值混在一起?

看这个例子:

const raw = {}
const arr = reactive([{}, {}])
arr.push(raw)

console.log(arr[2] === raw)
console.log(isReactive(arr[2]))

arr是一个reactive的数组,当把一个空对象push进去时,arr中保存的已经不是那个原始的raw对象了,这里面存入的是一个响应式的object。

arr.includes(raw)时,如果不做上述特殊处理,返回结果应该是false,这显然有点不符合使用者的思维逻辑。

所以,当遇到数组标志查询方法时,需要转换成原始值再进行查找。

遇到这些方法为何要自行遍历原始元素,再去track所有的元素呢?

百思不得其解,去翻了当时的commit和解决issue,大概弄懂了。

composition API设计之初,reactive的数组元素如果是一个ref类型,是会自动展开的,不需要arr[0].value去操作,这样带来了一系列的问题:

{
  1,
  2,
  { value: 3 }
}

array.reverse()

{
  3,
  2,
  { value: 1 }
}
  • 当reactive数组包含ref类型和raw类型时,数组的某些方法比如sort、reverse会出问题,ref类型并不会移位,但是值却变了;
  • 即使vue在内部hack这些数组的方法,当使用第三方库时比如lodash, 仍然会挂掉;
  • 某些集合类型如Map Set,本来就不会自动展开,那数组也不该去自动展开ref类型的元素
  • 一个混着raw和ref类型的数组在实际业务中应该很少见,用户如果真的需要,可以自行创建一个computed属性来做展开。

基于以上原因,vue从此移除掉了数组元素的ref类型展开。

当这里的数组元素使用raw值进行查找,不再操作proxy对象,就需要手动遍历进行track,实现依赖收集了。

这里确实要吐槽一句,hack的东西越多,心智负担就越重,不如保持simple clear,也许使用上会繁琐一些,但是权衡之下,应该尽可能的保持简洁。

-w649

vuejs/core#737 这里有关于这个问题的讨论,有兴趣的话值得一看。

最后要用两次元素查询是为了什么

直接看相关的测试用例:

  test('Array identity methods should work if raw value contains reactive objects', () => {
    const raw = []
    const obj = reactive({})
    raw.push(obj)
    const arr = reactive(raw)
    expect(arr.includes(obj)).toBe(true)
  })

当raw数组包含一个reactive对象obj时,再把raw数组转换成reactive数组,这时是否包含obj?

这里就不能直接比较toRaw后的原始对象了,这种情况就要查找toRaw之前的数组元素才能找到。

但是除此之外,大部分的查找还是需要使用raw对象的,所以这里要做两次查询满足所有场景需求,性能嘛,当然就差一些了。

总结

看源码多看commit信息+测试用例+issue讨论,事半功倍。

@gdh51
Copy link

gdh51 commented Mar 23, 2021

最后的最后要用两次元素查询是为了什么的解释不太对吧。实际上运行测试案例会发现不会走到if分支中,原因是转化为arr变量的原数组本来就带有一个reactive的元素obj。所以运行测试案例直接就会返回true。要想触发if分支需要这样模拟:

const raw = [],
 o = {},
 ro = reactive({})
 
// 此时原数组实际上就是个纯纯的数组 + 对象,没有响应式
raw.push(o)
 
const arr = reactive(raw)
 
console.log(arr.includes(obj)) // true
console.log(arr.includes(o)) // true

这种情况下会进入if分支, 这种情况比较特殊,相当于在一个纯包含对象的数组被reactive()后返回的A,单独将这其中的对象reactive()后返回的B,再在这个A中查找B。即我们查找一个响应化后的值和未响应前的值行为一样。

@caoyifeng007
Copy link

Reflect.get(arrayInstrumentations, key, receiver)
这里arrayInstrumentations中也没有getter呀,为什么会将receiver作为this传过去呢?

@loo41
Copy link

loo41 commented Nov 9, 2021

const arrayInstrumentations: Record<string, Function> = {}
;['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
  arrayInstrumentations[key] = function(...args: any[]): any {
    const arr = toRaw(this) as any
    for (let i = 0, l = (this as any).length; i < l; i++) {
      track(arr, TrackOpTypes.GET, i + '')
    }
    // we run the method using the original args first (which may be reactive)
    const res = arr[key](...args)
    if (res === -1 || res === false) {
      // if that didn't work, run it again using raw values.
      return arr[key](...args.map(toRaw))
    } else {
      return res
    }
  }
})

@caoyifeng007 实际上这里的this和Reflect.get(arrayInstrumentations, key, receiver)没有关系,Reflect.get只是取到这个方法的方法,this是调用的时候确定的,这个方法的调用者是代理的数组对象,所以this指向这个对象。

@Secret1007
Copy link

为什么只有这三个方法需要特殊处理,find这种的方法不需要吗

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants