Explorar o código

fix(reactivity): account for NaN in value change checks (#361)

Mayness %!s(int64=6) %!d(string=hai) anos
pai
achega
18a349ce8c

+ 8 - 0
packages/reactivity/__tests__/collections/Map.spec.ts

@@ -311,5 +311,13 @@ describe('reactivity/collections', () => {
       map.get(key)!.foo++
       map.get(key)!.foo++
       expect(dummy).toBe(2)
       expect(dummy).toBe(2)
     })
     })
+
+    it('should not be trigger when the value and the old value both are NaN', () => {
+      const map = reactive(new Map([['foo', NaN]]))
+      const mapSpy = jest.fn(() => map.get('foo'))
+      effect(mapSpy)
+      map.set('foo', NaN)
+      expect(mapSpy).toHaveBeenCalledTimes(1)
+    })
   })
   })
 })
 })

+ 10 - 0
packages/reactivity/__tests__/collections/WeakMap.spec.ts

@@ -107,5 +107,15 @@ describe('reactivity/collections', () => {
       observed.get(key).a = 2
       observed.get(key).a = 2
       expect(dummy).toBe(2)
       expect(dummy).toBe(2)
     })
     })
+
+    it('should not be trigger when the value and the old value both are NaN', () => {
+      const map = new WeakMap()
+      const key = {}
+      map.set(key, NaN)
+      const mapSpy = jest.fn(() => map.get(key))
+      effect(mapSpy)
+      map.set(key, NaN)
+      expect(mapSpy).toHaveBeenCalledTimes(1)
+    })
   })
   })
 })
 })

+ 10 - 0
packages/reactivity/__tests__/effect.spec.ts

@@ -691,4 +691,14 @@ describe('reactivity/effect', () => {
     obj.foo = { prop: 1 }
     obj.foo = { prop: 1 }
     expect(dummy).toBe(1)
     expect(dummy).toBe(1)
   })
   })
+
+  it('should not be trigger when the value and the old value both are NaN', () => {
+    const obj = reactive({
+      foo: NaN
+    })
+    const fnSpy = jest.fn(() => obj.foo)
+    effect(fnSpy)
+    obj.foo = NaN
+    expect(fnSpy).toHaveBeenCalledTimes(1)
+  })
 })
 })

+ 3 - 3
packages/reactivity/src/baseHandlers.ts

@@ -2,7 +2,7 @@ import { reactive, readonly, toRaw } from './reactive'
 import { OperationTypes } from './operations'
 import { OperationTypes } from './operations'
 import { track, trigger } from './effect'
 import { track, trigger } from './effect'
 import { LOCKED } from './lock'
 import { LOCKED } from './lock'
-import { isObject, hasOwn, isSymbol } from '@vue/shared'
+import { isObject, hasOwn, isSymbol, hasChanged } from '@vue/shared'
 import { isRef } from './ref'
 import { isRef } from './ref'
 
 
 const builtInSymbols = new Set(
 const builtInSymbols = new Set(
@@ -52,13 +52,13 @@ function set(
       const extraInfo = { oldValue, newValue: value }
       const extraInfo = { oldValue, newValue: value }
       if (!hadKey) {
       if (!hadKey) {
         trigger(target, OperationTypes.ADD, key, extraInfo)
         trigger(target, OperationTypes.ADD, key, extraInfo)
-      } else if (value !== oldValue) {
+      } else if (hasChanged(value, oldValue)) {
         trigger(target, OperationTypes.SET, key, extraInfo)
         trigger(target, OperationTypes.SET, key, extraInfo)
       }
       }
     } else {
     } else {
       if (!hadKey) {
       if (!hadKey) {
         trigger(target, OperationTypes.ADD, key)
         trigger(target, OperationTypes.ADD, key)
-      } else if (value !== oldValue) {
+      } else if (hasChanged(value, oldValue)) {
         trigger(target, OperationTypes.SET, key)
         trigger(target, OperationTypes.SET, key)
       }
       }
     }
     }

+ 3 - 3
packages/reactivity/src/collectionHandlers.ts

@@ -2,7 +2,7 @@ import { toRaw, reactive, readonly } from './reactive'
 import { track, trigger } from './effect'
 import { track, trigger } from './effect'
 import { OperationTypes } from './operations'
 import { OperationTypes } from './operations'
 import { LOCKED } from './lock'
 import { LOCKED } from './lock'
-import { isObject, capitalize, hasOwn } from '@vue/shared'
+import { isObject, capitalize, hasOwn, hasChanged } from '@vue/shared'
 
 
 export type CollectionTypes = IterableCollections | WeakCollections
 export type CollectionTypes = IterableCollections | WeakCollections
 
 
@@ -73,13 +73,13 @@ function set(this: MapTypes, key: unknown, value: unknown) {
     const extraInfo = { oldValue, newValue: value }
     const extraInfo = { oldValue, newValue: value }
     if (!hadKey) {
     if (!hadKey) {
       trigger(target, OperationTypes.ADD, key, extraInfo)
       trigger(target, OperationTypes.ADD, key, extraInfo)
-    } else if (value !== oldValue) {
+    } else if (hasChanged(value, oldValue)) {
       trigger(target, OperationTypes.SET, key, extraInfo)
       trigger(target, OperationTypes.SET, key, extraInfo)
     }
     }
   } else {
   } else {
     if (!hadKey) {
     if (!hadKey) {
       trigger(target, OperationTypes.ADD, key)
       trigger(target, OperationTypes.ADD, key)
-    } else if (value !== oldValue) {
+    } else if (hasChanged(value, oldValue)) {
       trigger(target, OperationTypes.SET, key)
       trigger(target, OperationTypes.SET, key)
     }
     }
   }
   }

+ 9 - 2
packages/runtime-core/src/apiWatch.ts

@@ -7,7 +7,14 @@ import {
   ReactiveEffectOptions
   ReactiveEffectOptions
 } from '@vue/reactivity'
 } from '@vue/reactivity'
 import { queueJob } from './scheduler'
 import { queueJob } from './scheduler'
-import { EMPTY_OBJ, isObject, isArray, isFunction, isString } from '@vue/shared'
+import {
+  EMPTY_OBJ,
+  isObject,
+  isArray,
+  isFunction,
+  isString,
+  hasChanged
+} from '@vue/shared'
 import { recordEffect } from './apiReactivity'
 import { recordEffect } from './apiReactivity'
 import {
 import {
   currentInstance,
   currentInstance,
@@ -144,7 +151,7 @@ function doWatch(
           return
           return
         }
         }
         const newValue = runner()
         const newValue = runner()
-        if (deep || newValue !== oldValue) {
+        if (deep || hasChanged(newValue, oldValue)) {
           // cleanup before running cb again
           // cleanup before running cb again
           if (cleanup) {
           if (cleanup) {
             cleanup()
             cleanup()

+ 4 - 0
packages/shared/src/index.ts

@@ -67,3 +67,7 @@ export const hyphenate = (str: string): string => {
 export const capitalize = (str: string): string => {
 export const capitalize = (str: string): string => {
   return str.charAt(0).toUpperCase() + str.slice(1)
   return str.charAt(0).toUpperCase() + str.slice(1)
 }
 }
+
+// compare whether a value has changed, accounting for NaN.
+export const hasChanged = (value: any, oldValue: any): boolean =>
+  value !== oldValue && (value === value || oldValue === oldValue)