Skip to content

Commit

Permalink
subrender - fix deferred updates
Browse files Browse the repository at this point in the history
  • Loading branch information
theKashey committed Feb 14, 2018
1 parent 31fb50f commit fcaed60
Show file tree
Hide file tree
Showing 8 changed files with 79 additions and 31 deletions.
2 changes: 1 addition & 1 deletion src/global/generation.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
let generation = 0
let generation = 1

export const increment = () => generation++
export const get = () => generation
1 change: 1 addition & 0 deletions src/proxy/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export const GENERATION = `${PREFIX}proxyGeneration`
export const REGENERATE_METHOD = `${PREFIX}regenerateByEval`
export const UNWRAP_PROXY = `${PREFIX}getCurrent`
export const CACHED_RESULT = `${PREFIX}cachedResult`
export const PROXY_IS_MOUNTED = `${PREFIX}isMounted`
50 changes: 37 additions & 13 deletions src/proxy/createClassProxy.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { Component } from 'react'
import transferStaticProps from './transferStaticProps'
import { GENERATION, PROXY_KEY, UNWRAP_PROXY, CACHED_RESULT } from './constants'
import {
GENERATION,
PROXY_KEY,
UNWRAP_PROXY,
CACHED_RESULT,
PROXY_IS_MOUNTED,
} from './constants'
import {
getDisplayName,
isReactClass,
Expand All @@ -16,8 +22,9 @@ const has = Object.prototype.hasOwnProperty
const proxies = new WeakMap()

const defaultRenderOptions = {
preRender: identity,
postRender: result => result,
componentWillReceiveProps: identity,
componentWillRender: identity,
componentDidRender: result => result,
}

const defineClassMember = (Class, methodName, methodBody) =>
Expand All @@ -28,6 +35,11 @@ const defineClassMember = (Class, methodName, methodBody) =>
value: methodBody,
})

const defineClassMembers = (Class, methods) =>
Object.keys(methods).forEach(methodName =>
defineClassMember(Class, methodName, methods[methodName]),
)

function createClassProxy(InitialComponent, proxyKey, options) {
const renderOptions = {
...defaultRenderOptions,
Expand Down Expand Up @@ -63,9 +75,10 @@ function createClassProxy(InitialComponent, proxyKey, options) {
inject(this, proxyGeneration, injectedMembers)
}

function lifeCycleWrapperFactory(wrapperName) {
function lifeCycleWrapperFactory(wrapperName, sideEffect = identity) {
return function wrappedMethod(...rest) {
proxiedUpdate.call(this)
sideEffect(this)
return (
!isFunctionalComponent &&
CurrentComponent.prototype[wrapperName] &&
Expand All @@ -74,14 +87,26 @@ function createClassProxy(InitialComponent, proxyKey, options) {
}
}

const componentWillMount = lifeCycleWrapperFactory(
'componentWillMount',
target => {
target[PROXY_IS_MOUNTED] = true
},
)
const componentWillReceiveProps = lifeCycleWrapperFactory(
'componentWillReceiveProps',
renderOptions.componentWillReceiveProps,
)
const componentWillUnmount = lifeCycleWrapperFactory(
'componentWillUnmount',
target => {
target[PROXY_IS_MOUNTED] = false
},
)
const componentWillUpdate = lifeCycleWrapperFactory('componentWillUpdate')

function proxiedRender() {
proxiedUpdate.call(this)
renderOptions.preRender(this)
renderOptions.componentWillRender(this)

let result

Expand All @@ -96,17 +121,16 @@ function createClassProxy(InitialComponent, proxyKey, options) {
result = CurrentComponent.prototype.render.call(this)
}

return renderOptions.postRender(result)
return renderOptions.componentDidRender(result)
}

const defineProxyMethods = Proxy => {
defineClassMember(Proxy, 'render', proxiedRender)
defineClassMember(
Proxy,
'componentWillReceiveProps',
defineClassMembers(Proxy, {
render: proxiedRender,
componentWillMount,
componentWillReceiveProps,
)
defineClassMember(Proxy, 'componentWillUpdate', componentWillUpdate)
componentWillUnmount,
})
}

let ProxyFacade
Expand Down
6 changes: 4 additions & 2 deletions src/reconciler/hotReplacementRender.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import levenshtein from 'fast-levenshtein'
import { PROXY_KEY, UNWRAP_PROXY } from '../proxy'
import { PROXY_IS_MOUNTED, PROXY_KEY, UNWRAP_PROXY } from '../proxy'
import { getIdByType, updateProxyById } from './proxies'
import {
updateInstance,
Expand Down Expand Up @@ -206,7 +206,9 @@ export const flushScheduledUpdates = () => {
const instances = scheduledUpdates
scheduledUpdates = []
scheduledUpdate = 0
instances.forEach(instance => updateInstance(instance))
instances.forEach(
instance => instance[PROXY_IS_MOUNTED] && updateInstance(instance),
)
}

const scheduleInstanceUpdate = instance => {
Expand Down
2 changes: 1 addition & 1 deletion src/reconciler/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import getReactStack from '../internal/getReactStack'
import hotReplacementRender, {
flushScheduledUpdates,
flushScheduledUpdates
} from './hotReplacementRender'

const reconcileHotReplacement = ReactInstance =>
Expand Down
28 changes: 22 additions & 6 deletions src/reconciler/proxyAdapter.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,37 @@
import reactHotLoader from '../reactHotLoader'
import { get as getGeneration } from '../global/generation'
import { getProxyByType, setStandInOptions } from './proxies'
import reconcileHotReplacement from './index'
import reconcileHotReplacement, { flushScheduledUpdates } from './index'

export const RENDERED_GENERATION = 'REACT_HOT_LOADER_RENDERED_GENERATION'

export const renderReconciler = (target, force) => {
// we are not inside parent reconcilation
const currentGeneration = getGeneration()
const componentGeneration = target[RENDERED_GENERATION]

target[RENDERED_GENERATION] = currentGeneration

if (!reactHotLoader.disableProxyCreation) {
if (
(target[RENDERED_GENERATION] || force) &&
target[RENDERED_GENERATION] !== currentGeneration
(componentGeneration || force) &&
componentGeneration !== currentGeneration
) {
reconcileHotReplacement(target)
return true
}
}
target[RENDERED_GENERATION] = currentGeneration
return false
}

function asyncReconciledRender(target) {
renderReconciler(target, false)
}

function syncReconciledRender(target) {
if (renderReconciler(target, false)) {
flushScheduledUpdates()
}
}

export const proxyWrapper = element => {
Expand All @@ -40,6 +55,7 @@ export const proxyWrapper = element => {
}

setStandInOptions({
preRender: renderReconciler,
postRender: proxyWrapper,
componentWillReceiveProps: syncReconciledRender,
componentWillRender: asyncReconciledRender,
componentDidRender: proxyWrapper,
})
4 changes: 2 additions & 2 deletions test/global/generation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import * as generation from '../../src/global/generation'

describe('generation', () => {
it('should maintain generation counter', () => {
expect(generation.get()).toBe(0)
generation.increment()
expect(generation.get()).toBe(1)
generation.increment()
expect(generation.get()).toBe(2)
})
})
17 changes: 11 additions & 6 deletions test/reconciler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,13 +213,13 @@ describe('reconciler', () => {

incrementGeneration()
wrapper.setProps({ update: 'now' })
expect(First.rendered).toHaveBeenCalledTimes(3)
expect(Second.rendered).toHaveBeenCalledTimes(3)
expect(First.rendered).toHaveBeenCalledTimes(4)
expect(Second.rendered).toHaveBeenCalledTimes(4)

incrementGeneration()
wrapper.setProps({ second: false })
expect(First.rendered).toHaveBeenCalledTimes(5)
expect(Second.rendered).toHaveBeenCalledTimes(3)
expect(First.rendered).toHaveBeenCalledTimes(7)
expect(Second.rendered).toHaveBeenCalledTimes(4)

expect(First.unmounted).toHaveBeenCalledTimes(0)
expect(Second.unmounted).toHaveBeenCalledTimes(1)
Expand All @@ -242,13 +242,18 @@ describe('reconciler', () => {
)

const wrapper = mount(<App />)
expect(First.rendered).toHaveBeenCalledTimes(0)

incrementGeneration()
wrapper.setProps({ first: true })
expect(First.rendered).toHaveBeenCalledTimes(1) // 1. prev state was empty == no need to reconcile

incrementGeneration()
wrapper.setProps({ second: true })
incrementGeneration()
wrapper.setProps({ third: true })
expect(First.rendered).toHaveBeenCalledTimes(4) // +3 (reconcile + update + render)
expect(Second.rendered).toHaveBeenCalledTimes(2) // (update from first + render)

wrapper.setProps({ third: true })
expect(First.rendered).toHaveBeenCalledTimes(5)
expect(Second.rendered).toHaveBeenCalledTimes(3)
expect(Third.rendered).toHaveBeenCalledTimes(1)
Expand Down

0 comments on commit fcaed60

Please sign in to comment.