vuejs / core

🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
https://vuejs.org/
MIT License
47.75k stars 8.34k forks source link

fix(reactivity): toRef edge cases for ref unwrapping #12420

Open skirtles-code opened 4 days ago

skirtles-code commented 4 days ago

This PR aims to address several related edge cases.

Case 1

The original motivation came from https://github.com/vuejs/pinia/issues/2812.

Long story short, consider the following code:

const c = computed(() => console.log('here'))
const r = reactive({ c })
toRef(r, 'c')

The call to toRef currently triggers the logging, even if the returned ref is never used:

While attempting to fix this, I kept running into other edge cases, mostly involving toRef and shallowReactive/shallowReadonly. I've tried to fix all of those edge cases too.

Case 2

Consider the following:

const s = shallowReactive({ num: ref(0) })
const t = toRef(s, 'num')
s.num = ref(1)

This should result in t.value being 1, but currently it ends up as 0.

Case 3

Consider this example:

const s = shallowReactive({ num: ref() })
const t = toRef(s, 'num', 7)

t.value should be 7, respecting the default value. But currently it is undefined.

Case 4

This is somewhat separate, but it impacted the tests I wrote for array handling with toRef.

Consider the following:

const arr = reactive([])
const r = ref(0)
arr.foo = r
arr.foo = 2

Arrays don't unwrap refs used as elements, but they do unwrap arrays added using custom properties such as foo. Essentially, custom properties on reactive arrays behave just like properties of normal reactive objects. Apart from in the case outlined above, where the ref is replaced rather than updated by arr.foo = 2. I've changed this in baseHandlers.ts, so that updating a custom property on an array behaves the same as for an object:

Notes on the changes

The key change is to move the logic for handling nested refs from propertyToRef to inside ObjectRefImpl.

Detecting whether a source will automatically unwrap refs is a bit tricky. It isn't sufficient just to check for isShallow, as you could have shallowReadonly wrapped around reactive. Instead I've recursively checked each wrapper proxy to check whether any level would unwrap.

As noted earlier, arrays also pose a specific challenge, as they don't unwrap elements even inside reactive or readonly.

There is one existing test case that no longer passes. Specifically, this one:

const r = { x: ref(1) }
expect(toRef(r, 'x')).toBe(r.x)

This is expecting the exact same ref to be returned by toRef. But returning the same ref is the underlying cause of most of the edge cases outlined above. From what I can tell, the original motivation for that test case was just to ensure that nested refs are unwrapped, not that they need to be ===. Returning an equivalent ref should be sufficient.

I do wonder whether there's an easier way to implement all of this, but currently I'm not seeing it.

github-actions[bot] commented 4 days ago

Size Report

Bundles

File Size Gzip Brotli
runtime-dom.global.prod.js 100 kB (+189 B) 38 kB (+64 B) 34.3 kB (+55 B)
vue.global.prod.js 158 kB (+189 B) 57.9 kB (+79 B) 51.5 kB (+80 B)

Usages

Name Size Gzip Brotli
createApp (CAPI only) 47.2 kB (+1 B) 18.4 kB (+6 B) 16.8 kB (-1 B)
createApp 55.2 kB (+1 B) 21.4 kB (+4 B) 19.5 kB (+44 B)
createSSRApp 59.3 kB (+1 B) 23.1 kB (+4 B) 21 kB (+2 B)
defineCustomElement 60.1 kB (+1 B) 22.9 kB (+7 B) 20.9 kB (+2 B)
overall 69.1 kB (+1 B) 26.5 kB (+4 B) 24.1 kB (-12 B)
pkg-pr-new[bot] commented 4 days ago

Open in Stackblitz

@vue/compiler-core

``` pnpm add https://pkg.pr.new/@vue/compiler-core@12420 ```

@vue/compiler-dom

``` pnpm add https://pkg.pr.new/@vue/compiler-dom@12420 ```

@vue/compiler-ssr

``` pnpm add https://pkg.pr.new/@vue/compiler-ssr@12420 ```

@vue/compiler-sfc

``` pnpm add https://pkg.pr.new/@vue/compiler-sfc@12420 ```

@vue/reactivity

``` pnpm add https://pkg.pr.new/@vue/reactivity@12420 ```

@vue/runtime-core

``` pnpm add https://pkg.pr.new/@vue/runtime-core@12420 ```

@vue/runtime-dom

``` pnpm add https://pkg.pr.new/@vue/runtime-dom@12420 ```

@vue/server-renderer

``` pnpm add https://pkg.pr.new/@vue/server-renderer@12420 ```

@vue/shared

``` pnpm add https://pkg.pr.new/@vue/shared@12420 ```

vue

``` pnpm add https://pkg.pr.new/vue@12420 ```

@vue/compat

``` pnpm add https://pkg.pr.new/@vue/compat@12420 ```

commit: 9ceb497

edison1105 commented 4 days ago

/ecosystem-ci run

vue-bot commented 4 days ago

📝 Ran ecosystem CI: Open

suite result latest scheduled
language-tools :white_check_mark: success :white_check_mark: success
nuxt :white_check_mark: success :white_check_mark: success
pinia :white_check_mark: success :white_check_mark: success
primevue :white_check_mark: success :white_check_mark: success
quasar :white_check_mark: success :white_check_mark: success
radix-vue :white_check_mark: success :white_check_mark: success
router :white_check_mark: success :white_check_mark: success
test-utils :white_check_mark: success :white_check_mark: success
vant :white_check_mark: success :white_check_mark: success
vite-plugin-vue :white_check_mark: success :white_check_mark: success
vitepress :white_check_mark: success :white_check_mark: success
vue-i18n :white_check_mark: success :white_check_mark: success
vue-macros :white_check_mark: success :white_check_mark: success
vuetify :white_check_mark: success :white_check_mark: success
vueuse :white_check_mark: success :white_check_mark: success
vue-simple-compiler :white_check_mark: success :white_check_mark: success
edison1105 commented 4 days ago

This PR has some performance impact on ref. The baseline is the main branch.

 ✓ packages/reactivity/__benchmarks__/ref.bench.ts (4) 11856ms
   ✓ ref (4) 11854ms
     name                       hz     min     max    mean     p75     p99    p995    p999     rme   samples
   · create ref      11,642,281.81  0.0000  0.2558  0.0001  0.0001  0.0001  0.0001  0.0002  ±0.21%   5821141  [0.55x] ⇓
     create ref      21,231,067.58  0.0000  0.1998  0.0000  0.0000  0.0001  0.0001  0.0001  ±0.21%  10615534  (baseline)
   · write ref       10,862,161.63  0.0000  2.2381  0.0001  0.0001  0.0001  0.0001  0.0002  ±0.95%   5431081  [0.53x] ⇓
     write ref       20,480,924.69  0.0000  0.5687  0.0000  0.0000  0.0001  0.0001  0.0002  ±0.55%  10240463  (baseline)
   · read ref        15,235,715.02  0.0000  0.7686  0.0001  0.0001  0.0001  0.0002  0.0002  ±0.59%   7617858  [0.61x] ⇓
     read ref        24,909,611.96  0.0000  0.7011  0.0000  0.0000  0.0000  0.0001  0.0001  ±0.76%  12454807  (baseline)
   · write/read ref  10,840,440.94  0.0000  0.3820  0.0001  0.0001  0.0001  0.0001  0.0002  ±0.39%   5420221  [0.53x] ⇓
     write/read ref  20,325,089.63  0.0000  0.6985  0.0000  0.0000  0.0001  0.0001  0.0002  ±0.71%  10162545  (baseline)

with the following steps:

1. switch to main branch
2. nr prebench-compare
3. nr bench ref
4. switch to this PR
5. nr prebench-compare
6. nr bench-compare ref

envinfo:

  System:
    OS: macOS 14.5
    CPU: (8) arm64 Apple M1
    Memory: 127.44 MB / 16.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 22.7.0 - /usr/local/bin/node

skirtles-code commented 3 days ago

@edison1105 Would you be able to try that performance benchmark again?

I tried running it locally, but it seems to get stuck running bench-compare. It starts off OK, then it just gets stuck at 100% CPU and never completes.

I don't think my changes should impact those benchmarks. The benchmark you mentioned seems to be testing ref(), which should be unchanged by this PR.

I would expect some performance impact on toRef() when passing toRef(obj, 'property'), but that doesn't seem to be what's tested in the benchmarks.

edison1105 commented 2 days ago

@skirtles-code

I tested it again. This time, I repeated step 6 above three times, and the results showed that performance was not affected. Below are the results from the third execution.

The above test results were obtained from the first time run nr bench-compare ref. It is strange that there is a significant performance difference.

✓ packages/reactivity/__benchmarks__/ref.bench.ts (4) 16730ms
   ✓ ref (4) 16728ms
     name                       hz     min     max    mean     p75     p99    p995    p999     rme   samples
   · create ref      21,076,099.66  0.0000  0.2064  0.0000  0.0000  0.0001  0.0001  0.0002  ±0.23%  10538050  [1.00x] ⇓
     create ref      21,152,011.37  0.0000  0.5231  0.0000  0.0000  0.0001  0.0001  0.0002  ±0.38%  10576006  (baseline)
   · write ref       20,557,804.48  0.0000  0.4285  0.0000  0.0000  0.0001  0.0001  0.0001  ±0.51%  10278903  [1.02x] ⇑
     write ref       20,076,703.63  0.0000  0.5026  0.0000  0.0000  0.0001  0.0001  0.0002  ±0.52%  10038353  (baseline)
   · read ref        24,080,925.98  0.0000  0.7341  0.0000  0.0000  0.0000  0.0001  0.0002  ±1.10%  12040464  [0.99x] ⇓
     read ref        24,253,846.74  0.0000  1.1237  0.0000  0.0000  0.0000  0.0001  0.0001  ±1.02%  12126924  (baseline)
   · write/read ref  19,876,380.25  0.0000  0.5281  0.0001  0.0000  0.0001  0.0001  0.0002  ±0.64%   9938191  [1.03x] ⇑
     write/read ref  19,240,027.69  0.0000  2.3183  0.0001  0.0000  0.0001  0.0001  0.0002  ±1.21%   9620095  (baseline)