Skip to content

Commit

Permalink
fix(reactive): fix Tracker memo leak in StrictMode (#1715)
Browse files Browse the repository at this point in the history
* fix(reactive): fix Tracker memo leak in StrictMode

* fix(reactive): fix ci
  • Loading branch information
janryWang authored Jul 4, 2021
1 parent 6a8e2d0 commit e9f23c3
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 43 deletions.
29 changes: 12 additions & 17 deletions packages/reactive-react/src/hooks/useForceUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,26 @@ const RENDER_COUNT = { value: 0 }
const RENDER_QUEUE = new Set<() => void>()

export function useForceUpdate() {
const [, setTick] = useState(0)
const unmountRef = useRef(false)
const [, setState] = useState([])
const unMountRef = useRef(false)

useEffect(() => {
unMountRef.current = false
return () => {
unMountRef.current = true
}
}, EMPTY_ARRAY)

const update = useCallback(() => {
if (unmountRef.current) return
setTick((tick) => {
return tick + 1
})
if (unMountRef.current) return
setState([])
}, EMPTY_ARRAY)

const scheduler = useCallback(() => {
if (RENDER_COUNT.value === 0) {
update()
} else {
if (!RENDER_QUEUE.has(update)) {
RENDER_QUEUE.add(update)
}
RENDER_QUEUE.add(update)
}
}, EMPTY_ARRAY)

Expand All @@ -31,20 +34,12 @@ export function useForceUpdate() {
useDidUpdate(() => {
RENDER_COUNT.value--
if (RENDER_COUNT.value === 0) {
if (unmountRef.current) return
RENDER_QUEUE.forEach((update) => {
RENDER_QUEUE.delete(update)
update()
})
}
})

useEffect(() => {
unmountRef.current = false
return () => {
unmountRef.current = true
}
}, [])

return scheduler
}
38 changes: 23 additions & 15 deletions packages/reactive-react/src/hooks/useObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,54 @@ import { GarbageCollector } from '../shared'
import { IObserverOptions } from '../types'
import { useForceUpdate } from './useForceUpdate'

class AutoCollector {}
class ObjectToBeRetainedByReact {}

function objectToBeRetainedByReactFactory() {
return new ObjectToBeRetainedByReact()
}

export const useObserver = <T extends () => any>(
view: T,
options?: IObserverOptions
): ReturnType<T> => {
const forceUpdate = useForceUpdate()
const unmountRef = React.useRef(false)
const unMountRef = React.useRef(false)
const trackerRef = React.useRef<Tracker>(null)
const gcRef = React.useRef<GarbageCollector>()

const tracker = React.useMemo(() => {
return new Tracker(() => {
const [objectRetainedByReact] = React.useState(
objectToBeRetainedByReactFactory
)
if (!trackerRef.current) {
trackerRef.current = new Tracker(() => {
if (typeof options?.scheduler === 'function') {
options.scheduler(forceUpdate)
} else {
forceUpdate()
}
}, options?.displayName)
}, [])
}

//StrictMode/ConcurrentMode会导致组件无法正确触发Unmount,所以只能自己做垃圾回收
//StrictMode/ConcurrentMode会导致组件无法正确触发UnMount,所以只能自己做垃圾回收
if (!gcRef.current) {
gcRef.current = new GarbageCollector(() => {
if (tracker) {
tracker.dispose()
if (trackerRef.current) {
trackerRef.current.dispose()
}
})
gcRef.current.open(new AutoCollector())
gcRef.current.open(objectRetainedByReact)
}

React.useEffect(() => {
unmountRef.current = false
unMountRef.current = false
gcRef.current.close()
return () => {
unmountRef.current = true
if (tracker) {
tracker.dispose()
unMountRef.current = true
if (trackerRef.current) {
trackerRef.current.dispose()
trackerRef.current = null
}
}
}, [])

return tracker.track(view)
return trackerRef.current.track(view)
}
9 changes: 6 additions & 3 deletions packages/reactive-react/src/shared/immediate.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
export const immediate = (callback?: () => void) => {
let desposed = false
let disposed = false
Promise.resolve(0).then(() => {
if (desposed) return
if (disposed) {
disposed = false
return
}
callback()
})
return () => {
desposed = true
disposed = true
}
}
13 changes: 5 additions & 8 deletions packages/reactive/src/tracker.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import { ReactionStack } from './environment'
import { isFn } from './checkers'
import { Reaction } from './types'
import {
batchEnd,
batchStart,
disposeBindingReactions,
releaseBindingReactions,
} from './reaction'
import { batchEnd, batchStart, disposeBindingReactions } from './reaction'

export class Tracker {
private results: any
constructor(
scheduler?: (reaction: Reaction) => void,
name = 'TrackerReaction'
) {
this.track._scheduler = scheduler
this.track._scheduler = (callback) => {
if (this.track._boundary === 0) this.dispose()
if (isFn(callback)) scheduler(callback)
}
this.track._name = name
this.track._boundary = 0
}
Expand All @@ -23,7 +21,6 @@ export class Tracker {
if (!isFn(tracker)) return this.results
if (this.track._boundary > 0) return
if (ReactionStack.indexOf(this.track) === -1) {
releaseBindingReactions(this.track)
try {
batchStart()
ReactionStack.push(this.track)
Expand Down

0 comments on commit e9f23c3

Please sign in to comment.