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

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

skirtles-code
Copy link
Contributor

@skirtles-code skirtles-code commented Nov 17, 2024

This PR aims to address several related edge cases.

Case 1

The original motivation came from vuejs/pinia#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.

Copy link

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)

Copy link

pkg-pr-new bot commented Nov 17, 2024

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
Copy link
Member

/ecosystem-ci run

@vue-bot
Copy link
Contributor

vue-bot commented Nov 18, 2024

📝 Ran ecosystem CI: Open

suite result latest scheduled
language-tools success success
nuxt success success
pinia success success
primevue success success
quasar success success
radix-vue success success
router success success
test-utils success success
vant success success
vite-plugin-vue success success
vitepress success success
vue-i18n success success
vue-macros success success
vuetify success success
vueuse success success
vue-simple-compiler success success

@edison1105
Copy link
Member

edison1105 commented Nov 18, 2024

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

@edison1105 edison1105 added scope: reactivity need discussion 🍰 p2-nice-to-have Priority 2: this is not breaking anything but nice to have it addressed. labels Nov 18, 2024
@skirtles-code
Copy link
Contributor Author

@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
Copy link
Member

@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)

@edison1105 edison1105 added ready to merge The PR is ready to be merged. and removed need discussion labels Nov 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🍰 p2-nice-to-have Priority 2: this is not breaking anything but nice to have it addressed. ready to merge The PR is ready to be merged. scope: reactivity
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants