Skip to content

Commit db0899d

Browse files
authored
Merge pull request #2 from FormidableLabs/event-loop-yielding
Yield to event loop when synchronous work takes too long
2 parents 76ce717 + b51f275 commit db0899d

File tree

10 files changed

+297
-53
lines changed

10 files changed

+297
-53
lines changed

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@
3535
"@babel/preset-react"
3636
]
3737
},
38+
"jest": {
39+
"globals": {
40+
"__DEV__": true
41+
}
42+
},
3843
"lint-staged": {
3944
"**/*.js": [
4045
"flow focus-check",
@@ -70,7 +75,8 @@
7075
"prettier": "^1.16.4",
7176
"react": "^16.8.4",
7277
"react-dom": "^16.8.4",
73-
"react-is": "^16.8.4"
78+
"react-is": "^16.8.4",
79+
"styled-components": "^4.2.0"
7480
},
7581
"dependencies": {
7682
"object-is": "^1.0.1"

src/__tests__/suspense.test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,40 @@ import React, {
1212
import renderPrepass from '..'
1313

1414
describe('renderPrepass', () => {
15+
describe('event loop yielding', () => {
16+
it('yields to the event loop when work is taking too long', () => {
17+
const Inner = jest.fn(() => null)
18+
19+
const Outer = () => {
20+
const start = Date.now()
21+
while (Date.now() - start < 21) {}
22+
return <Inner />
23+
}
24+
25+
const render$ = renderPrepass(<Outer />)
26+
27+
expect(Inner).toHaveBeenCalledTimes(0)
28+
29+
setImmediate(() => {
30+
setImmediate(() => {
31+
expect(Inner).toHaveBeenCalledTimes(1)
32+
})
33+
})
34+
35+
return render$.then(() => {
36+
expect(Inner).toHaveBeenCalledTimes(1)
37+
})
38+
})
39+
40+
it('does not yields when work is below the threshold', () => {
41+
const Inner = jest.fn(() => null)
42+
const Outer = () => <Inner />
43+
const render$ = renderPrepass(<Outer />)
44+
45+
expect(Inner).toHaveBeenCalledTimes(1)
46+
})
47+
})
48+
1549
describe('function components', () => {
1650
it('supports suspending subtrees', () => {
1751
const value = {}

src/__tests__/visitor.test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import React, {
1212
} from 'react'
1313

1414
import { createPortal } from 'react-dom'
15+
import styled from 'styled-components'
1516

1617
import {
1718
Dispatcher,
@@ -105,6 +106,25 @@ describe('visitElement', () => {
105106
expect(children[1].type).toBe(Noop)
106107
})
107108

109+
it('walks StyledComponent DOM elements', () => {
110+
const Comp = styled.div``
111+
const children = visitElement(
112+
<Comp>
113+
<Noop />
114+
</Comp>,
115+
[],
116+
() => {}
117+
)
118+
expect(children.length).toBe(1)
119+
expect(children[0].type).toBe(Noop)
120+
})
121+
122+
it('walks StyledComponent wrapper elements', () => {
123+
const Comp = styled(Noop)``
124+
const children = visitElement(<Comp />, [], () => {})
125+
expect(children.length).toBe(1)
126+
})
127+
108128
it('walks Providers and Consumers', () => {
109129
const Context = createContext('default')
110130
const leaf = jest.fn().mockReturnValue(null)

src/index.js

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// @flow
22

33
import React, { type Node, type Element } from 'react'
4-
import type { Visitor, Frame, AbstractElement } from './types'
5-
import { visitChildren } from './visitor'
4+
import type { Visitor, YieldFrame, Frame, AbstractElement } from './types'
5+
import { visitChildren, resumeVisitChildren } from './visitor'
66
import { getChildrenArray } from './element'
77

88
import {
@@ -28,13 +28,25 @@ let prevDispatcher = ReactCurrentDispatcher.current
2828
the queue. Hence we recursively look at suspended components in
2929
this queue, wait for their promises to resolve, and continue
3030
calling visitChildren on their children. */
31-
const flushFrames = (queue: Frame[], visitor: Visitor): Promise<void> => {
32-
if (queue.length === 0) {
33-
return Promise.resolve()
31+
const updateWithFrame = (
32+
frame: Frame,
33+
queue: Frame[],
34+
visitor: Visitor
35+
): Promise<void> => {
36+
if (frame.kind === 'frame.yield') {
37+
const yieldFrame: YieldFrame = frame
38+
39+
return new Promise(resolve => {
40+
setImmediate(() => {
41+
prevDispatcher = ReactCurrentDispatcher.current
42+
ReactCurrentDispatcher.current = Dispatcher
43+
resumeVisitChildren(yieldFrame, queue, visitor)
44+
ReactCurrentDispatcher.current = prevDispatcher
45+
resolve()
46+
})
47+
})
3448
}
3549

36-
const frame = queue.shift()
37-
3850
return frame.thenable.then(() => {
3951
prevDispatcher = ReactCurrentDispatcher.current
4052
ReactCurrentDispatcher.current = Dispatcher
@@ -55,11 +67,19 @@ const flushFrames = (queue: Frame[], visitor: Visitor): Promise<void> => {
5567
// children (which might also suspend)
5668
visitChildren(children, queue, visitor)
5769
ReactCurrentDispatcher.current = prevDispatcher
58-
59-
return flushFrames(queue, visitor)
6070
})
6171
}
6272

73+
const flushFrames = (queue: Frame[], visitor: Visitor): Promise<void> => {
74+
if (queue.length === 0) {
75+
return Promise.resolve()
76+
}
77+
78+
return updateWithFrame(queue.shift(), queue, visitor).then(() =>
79+
flushFrames(queue, visitor)
80+
)
81+
}
82+
6383
const defaultVisitor = () => {}
6484

6585
const renderPrepass = (element: Node, visitor?: Visitor): Promise<void> => {

src/internals/context.js

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import type {
44
AbstractContext,
55
UserElement,
66
ContextMap,
7-
ContextStore
7+
ContextStore,
8+
ContextEntry
89
} from '../types'
910

1011
/** The context is kept as a Map from a Context value to the current
@@ -17,8 +18,6 @@ import type {
1718
After walking the children they can be restored.
1819
This way the context recursively restores itself on the way up. */
1920

20-
type ContextEntry = [AbstractContext, mixed]
21-
2221
let currentContextStore: ContextStore = new Map()
2322
let currentContextMap: ContextMap = {}
2423

@@ -42,12 +41,16 @@ export const flushPrevContextStore = (): void | ContextEntry => {
4241
return prev
4342
}
4443

45-
export const restoreContextMap = (prev: ContextMap) => {
46-
Object.assign(currentContextMap, prev)
44+
export const restoreContextMap = (prev: void | ContextMap) => {
45+
if (prev !== undefined) {
46+
Object.assign(currentContextMap, prev)
47+
}
4748
}
4849

49-
export const restoreContextStore = (prev: ContextEntry) => {
50-
currentContextStore.set(prev[0], prev[1])
50+
export const restoreContextStore = (prev: void | ContextEntry) => {
51+
if (prev !== undefined) {
52+
currentContextStore.set(prev[0], prev[1])
53+
}
5154
}
5255

5356
export const setCurrentContextMap = (map: ContextMap) => {

src/types/element.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,10 @@ export type MemoElement = {
103103
export type ForwardRefElement = {
104104
type: {
105105
render: ComponentType<DefaultProps> & ComponentStatics,
106-
$$typeof: typeof REACT_FORWARD_REF_TYPE
106+
$$typeof: typeof REACT_FORWARD_REF_TYPE,
107+
// styled-components specific properties
108+
styledComponentId?: string,
109+
target?: ComponentType<mixed> | string
107110
},
108111
props: DefaultProps,
109112
$$typeof: typeof REACT_ELEMENT_TYPE

src/types/frames.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
import type { ComponentType } from 'react'
44
import type { Identity } from '../internals'
55
import type { LazyComponent } from '../types'
6-
import type { DefaultProps, ComponentStatics } from './element'
7-
import type { ContextMap, ContextStore, Hook } from './state'
6+
import type { ContextMap, ContextStore, ContextEntry, Hook } from './state'
7+
import type { AbstractElement, DefaultProps, ComponentStatics } from './element'
88

99
export type BaseFrame = {
1010
contextMap: ContextMap,
@@ -35,4 +35,13 @@ export type HooksFrame = BaseFrame & {
3535
hook: Hook | null
3636
}
3737

38-
export type Frame = ClassFrame | HooksFrame | LazyFrame
38+
/** Description of a pause to yield to the event loop */
39+
export type YieldFrame = BaseFrame & {
40+
kind: 'frame.yield',
41+
children: AbstractElement[][],
42+
index: number[],
43+
map: Array<void | ContextMap>,
44+
store: Array<void | ContextEntry>
45+
}
46+
47+
export type Frame = ClassFrame | HooksFrame | LazyFrame | YieldFrame

src/types/state.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { AbstractContext } from './element'
44

55
export type ContextStore = Map<AbstractContext, mixed>
66
export type ContextMap = { [name: string]: mixed }
7+
export type ContextEntry = [AbstractContext, mixed]
78

89
export type Dispatch<A> = A => void
910

0 commit comments

Comments
 (0)