add gs
This commit is contained in:
@@ -0,0 +1,435 @@
|
||||
--[[
|
||||
The base implementation of a stateful component in Roact.
|
||||
|
||||
Stateful components handle most of their own mounting and reconciliation
|
||||
process. Many of the private methods here are invoked by the reconciler.
|
||||
|
||||
Stateful components expose a handful of lifecycle events:
|
||||
- didMount
|
||||
- willUnmount
|
||||
- willUpdate
|
||||
- didUpdate
|
||||
- (static) getDerivedStateFromProps
|
||||
|
||||
These lifecycle events line up with their semantics in React, and more
|
||||
information (and a diagram) is available in the Roact documentation.
|
||||
]]
|
||||
|
||||
local Reconciler = require(script.Parent.Reconciler)
|
||||
local Core = require(script.Parent.Core)
|
||||
local GlobalConfig = require(script.Parent.GlobalConfig)
|
||||
local Instrumentation = require(script.Parent.Instrumentation)
|
||||
|
||||
local invalidSetStateMessages = require(script.Parent.invalidSetStateMessages)
|
||||
|
||||
local Component = {}
|
||||
|
||||
-- Locally cache tick so we can minimize impact of calling it for instrumentation
|
||||
local tick = tick
|
||||
|
||||
Component.__index = Component
|
||||
|
||||
--[[
|
||||
Merge any number of dictionaries into a new dictionary, overwriting keys.
|
||||
|
||||
If a value of `Core.None` is encountered, the key will be removed instead.
|
||||
This is necessary because Lua doesn't differentiate between a key being
|
||||
missing and a key being set to nil.
|
||||
]]
|
||||
local function merge(...)
|
||||
local result = {}
|
||||
|
||||
for i = 1, select("#", ...) do
|
||||
local entry = select(i, ...)
|
||||
|
||||
for key, value in pairs(entry) do
|
||||
if value == Core.None then
|
||||
result[key] = nil
|
||||
else
|
||||
result[key] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--[[
|
||||
Create a new stateful component.
|
||||
|
||||
Not intended to be a general OO implementation, this function only intends
|
||||
to let users extend Component and PureComponent.
|
||||
|
||||
Instead of using inheritance, use composition and props to extend
|
||||
components.
|
||||
]]
|
||||
function Component:extend(name)
|
||||
assert(type(name) == "string", "A name must be provided to create a Roact Component")
|
||||
|
||||
local class = {}
|
||||
|
||||
for key, value in pairs(self) do
|
||||
-- We don't want users using 'extend' to create component inheritance
|
||||
-- see https://reactjs.org/docs/composition-vs-inheritance.html
|
||||
if key ~= "extend" then
|
||||
class[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
class.__index = class
|
||||
|
||||
setmetatable(class, {
|
||||
__tostring = function(self)
|
||||
return name
|
||||
end
|
||||
})
|
||||
|
||||
function class._new(props, context)
|
||||
local self = {}
|
||||
|
||||
-- When set to a value, setState will fail, using the given reason to
|
||||
-- create a detailed error message.
|
||||
-- You can see a list of reasons in invalidSetStateMessages.
|
||||
self._setStateBlockedReason = nil
|
||||
|
||||
if class.defaultProps == nil then
|
||||
self.props = props
|
||||
else
|
||||
self.props = merge(class.defaultProps, props)
|
||||
end
|
||||
|
||||
self._context = {}
|
||||
|
||||
-- Shallow copy all context values from our parent element.
|
||||
if context then
|
||||
for key, value in pairs(context) do
|
||||
self._context[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
setmetatable(self, class)
|
||||
|
||||
-- Call the user-provided initializer, where state and _props are set.
|
||||
if class.init then
|
||||
self._setStateBlockedReason = "init"
|
||||
class.init(self, props)
|
||||
self._setStateBlockedReason = nil
|
||||
end
|
||||
|
||||
-- The user constructer might not set state, so we can.
|
||||
if not self.state then
|
||||
self.state = {}
|
||||
end
|
||||
|
||||
if class.getDerivedStateFromProps then
|
||||
local partialState = class.getDerivedStateFromProps(props, self.state)
|
||||
|
||||
if partialState then
|
||||
self.state = merge(self.state, partialState)
|
||||
end
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
return class
|
||||
end
|
||||
|
||||
--[[
|
||||
render is intended to describe what a UI should look like at the current
|
||||
point in time.
|
||||
|
||||
The default implementation throws an error, since forgetting to define
|
||||
render is usually a mistake.
|
||||
|
||||
The simplest implementation for render is:
|
||||
|
||||
function MyComponent:render()
|
||||
return nil
|
||||
end
|
||||
|
||||
You should explicitly return nil from functions in Lua to avoid edge cases
|
||||
related to none versus nil.
|
||||
]]
|
||||
function Component:render()
|
||||
local message = (
|
||||
"The component %q is missing the 'render' method.\n" ..
|
||||
"render must be defined when creating a Roact component!"
|
||||
):format(
|
||||
tostring(getmetatable(self))
|
||||
)
|
||||
|
||||
error(message, 0)
|
||||
end
|
||||
|
||||
--[[
|
||||
Used to tell Roact whether this component *might* need to be re-rendered
|
||||
given a new set of props and state.
|
||||
|
||||
This method is an escape hatch for when the Roact element creation and
|
||||
reconciliation algorithms are not fast enough for specific cases. Poorly
|
||||
written shouldUpdate methods *will* cause hard-to-trace bugs.
|
||||
|
||||
If you're thinking of writing a shouldUpdate function, consider using
|
||||
PureComponent instead, which provides a good implementation given that your
|
||||
data is immutable.
|
||||
|
||||
This function must be faster than the render method in order to be a
|
||||
performance improvement.
|
||||
]]
|
||||
function Component:shouldUpdate(newProps, newState)
|
||||
return true
|
||||
end
|
||||
|
||||
--[[
|
||||
Applies new state to the component.
|
||||
|
||||
partialState may be one of two things:
|
||||
- A table, which will be merged onto the current state.
|
||||
- A function, returning a table to merge onto the current state.
|
||||
|
||||
The table variant generally looks like:
|
||||
|
||||
self:setState({
|
||||
foo = "bar",
|
||||
})
|
||||
|
||||
The function variant generally looks like:
|
||||
|
||||
self:setState(function(prevState, props)
|
||||
return {
|
||||
foo = prevState.count + 1,
|
||||
})
|
||||
end)
|
||||
|
||||
The function variant may also return nil in the callback, which allows Roact
|
||||
to cancel updating state and abort the render.
|
||||
|
||||
Future versions of Roact will potentially batch or delay state merging, so
|
||||
any state updates that depend on the current state should use the function
|
||||
variant.
|
||||
]]
|
||||
function Component:setState(partialState)
|
||||
-- If setState was disabled, we should check for a detailed message and
|
||||
-- display it.
|
||||
if self._setStateBlockedReason ~= nil then
|
||||
local messageSource = invalidSetStateMessages[self._setStateBlockedReason]
|
||||
|
||||
if messageSource == nil then
|
||||
messageSource = invalidSetStateMessages["default"]
|
||||
end
|
||||
|
||||
-- We assume that each message has a formatting placeholder for a component name.
|
||||
local formattedMessage = string.format(messageSource, tostring(getmetatable(self)))
|
||||
|
||||
error(formattedMessage, 2)
|
||||
end
|
||||
|
||||
-- If the partial state is a function, invoke it to get the actual partial state.
|
||||
if type(partialState) == "function" then
|
||||
partialState = partialState(self.state, self.props)
|
||||
|
||||
-- If partialState is nil, abort the render.
|
||||
if partialState == nil then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local newState = merge(self.state, partialState)
|
||||
self:_update(nil, newState)
|
||||
end
|
||||
|
||||
--[[
|
||||
Returns the current stack trace for this component, or nil if the
|
||||
elementTracing configuration flag is set to false.
|
||||
]]
|
||||
function Component:getElementTraceback()
|
||||
return self._handle._element.source
|
||||
end
|
||||
|
||||
--[[
|
||||
Notifies the component that new props and state are available. This function
|
||||
is invoked by the reconciler.
|
||||
|
||||
If shouldUpdate returns true, this method will trigger a re-render and
|
||||
reconciliation step.
|
||||
]]
|
||||
function Component:_update(newProps, newState)
|
||||
self._setStateBlockedReason = "shouldUpdate"
|
||||
|
||||
local doUpdate
|
||||
if GlobalConfig.getValue("componentInstrumentation") then
|
||||
local startTime = tick()
|
||||
|
||||
doUpdate = self:shouldUpdate(newProps or self.props, newState or self.state)
|
||||
|
||||
local elapsed = tick() - startTime
|
||||
Instrumentation.logShouldUpdate(self._handle, doUpdate, elapsed)
|
||||
else
|
||||
doUpdate = self:shouldUpdate(newProps or self.props, newState or self.state)
|
||||
end
|
||||
|
||||
self._setStateBlockedReason = nil
|
||||
|
||||
if doUpdate then
|
||||
self:_forceUpdate(newProps, newState)
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Forces the component to re-render itself and its children.
|
||||
|
||||
This is essentially the inner portion of _update.
|
||||
|
||||
newProps and newState are optional.
|
||||
]]
|
||||
function Component:_forceUpdate(newProps, newState)
|
||||
-- Compute new derived state.
|
||||
-- Get the class - getDerivedStateFromProps is static.
|
||||
local class = getmetatable(self)
|
||||
|
||||
-- If newProps are passed, compute derived state and default props
|
||||
if newProps then
|
||||
if class.getDerivedStateFromProps then
|
||||
local derivedState = class.getDerivedStateFromProps(newProps, newState or self.state)
|
||||
|
||||
-- getDerivedStateFromProps can return nil if no changes are necessary.
|
||||
if derivedState ~= nil then
|
||||
newState = merge(newState or self.state, derivedState)
|
||||
end
|
||||
end
|
||||
|
||||
if class.defaultProps then
|
||||
-- We only allocate another prop table if there are props that are
|
||||
-- falling back to their default.
|
||||
local replacementProps
|
||||
|
||||
for key in pairs(class.defaultProps) do
|
||||
if newProps[key] == nil then
|
||||
replacementProps = merge(class.defaultProps, newProps)
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if replacementProps then
|
||||
newProps = replacementProps
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if self.willUpdate then
|
||||
self._setStateBlockedReason = "willUpdate"
|
||||
self:willUpdate(newProps or self.props, newState or self.state)
|
||||
self._setStateBlockedReason = nil
|
||||
end
|
||||
|
||||
local oldProps = self.props
|
||||
local oldState = self.state
|
||||
|
||||
if newProps then
|
||||
self.props = newProps
|
||||
end
|
||||
|
||||
if newState then
|
||||
self.state = newState
|
||||
end
|
||||
|
||||
self._setStateBlockedReason = "render"
|
||||
|
||||
local newChildElement
|
||||
if GlobalConfig.getValue("componentInstrumentation") then
|
||||
local startTime = tick()
|
||||
|
||||
newChildElement = self:render()
|
||||
|
||||
local elapsed = tick() - startTime
|
||||
Instrumentation.logRenderTime(self._handle, elapsed)
|
||||
else
|
||||
newChildElement = self:render()
|
||||
end
|
||||
|
||||
self._setStateBlockedReason = nil
|
||||
|
||||
self._setStateBlockedReason = "reconcile"
|
||||
if self._handle._child ~= nil then
|
||||
-- We returned an element during our last render, update it.
|
||||
self._handle._child = Reconciler._reconcileInternal(
|
||||
self._handle._child,
|
||||
newChildElement
|
||||
)
|
||||
elseif newChildElement then
|
||||
-- We returned nil during our last render, construct a new child.
|
||||
self._handle._child = Reconciler._mountInternal(
|
||||
newChildElement,
|
||||
self._handle._parent,
|
||||
self._handle._key,
|
||||
self._context
|
||||
)
|
||||
end
|
||||
self._setStateBlockedReason = nil
|
||||
|
||||
if self.didUpdate then
|
||||
self:didUpdate(oldProps, oldState)
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Initializes the component instance and attaches it to the given
|
||||
instance handle, created by Reconciler._mount.
|
||||
]]
|
||||
function Component:_mount(handle)
|
||||
self._handle = handle
|
||||
|
||||
self._setStateBlockedReason = "render"
|
||||
|
||||
local virtualTree
|
||||
if GlobalConfig.getValue("componentInstrumentation") then
|
||||
local startTime = tick()
|
||||
|
||||
virtualTree = self:render()
|
||||
|
||||
local elapsed = tick() - startTime
|
||||
Instrumentation.logRenderTime(self._handle, elapsed)
|
||||
else
|
||||
virtualTree = self:render()
|
||||
end
|
||||
|
||||
self._setStateBlockedReason = nil
|
||||
|
||||
if virtualTree then
|
||||
self._setStateBlockedReason = "reconcile"
|
||||
handle._child = Reconciler._mountInternal(
|
||||
virtualTree,
|
||||
handle._parent,
|
||||
handle._key,
|
||||
self._context
|
||||
)
|
||||
self._setStateBlockedReason = nil
|
||||
end
|
||||
|
||||
if self.didMount then
|
||||
self:didMount()
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Destructs the component and invokes all necessary lifecycle methods.
|
||||
]]
|
||||
function Component:_unmount()
|
||||
local handle = self._handle
|
||||
|
||||
if self.willUnmount then
|
||||
self._setStateBlockedReason = "willUnmount"
|
||||
self:willUnmount()
|
||||
self._setStateBlockedReason = nil
|
||||
end
|
||||
|
||||
-- Stateful components can return nil from render()
|
||||
if handle._child then
|
||||
Reconciler.unmount(handle._child)
|
||||
end
|
||||
|
||||
self._handle = nil
|
||||
end
|
||||
|
||||
return Component
|
||||
Reference in New Issue
Block a user