add gs
This commit is contained in:
@@ -0,0 +1,543 @@
|
||||
--[[
|
||||
The reconciler uses the virtual DOM generated by components to create a real
|
||||
tree of Roblox instances.
|
||||
|
||||
The reonciler has three basic operations:
|
||||
* mount (previously reify)
|
||||
* reconcile
|
||||
* unmount (previously teardown)
|
||||
|
||||
Mounting is the process of creating new components. This is first
|
||||
triggered when the user calls `Roact.mount` on an element. This is where the
|
||||
structure of the component tree is built, later used and modified by the
|
||||
reconciliation and unmounting steps.
|
||||
|
||||
Reconciliation accepts an existing concrete instance tree (created by mount)
|
||||
along with a new element that describes the desired tree. The reconciler
|
||||
will do the minimum amount of work required to update tree's components to
|
||||
match the new element, sometimes invoking mount to create new branches.
|
||||
|
||||
Unmounting destructs for the tree. It will crawl through the tree,
|
||||
destroying nodes from the bottom up.
|
||||
|
||||
Much of the reconciler's work is done by Component, which is the base for
|
||||
all stateful components in Roact. Components can trigger reconciliation (and
|
||||
implicitly, unmounting) via state updates that come with their own caveats.
|
||||
]]
|
||||
|
||||
local Core = require(script.Parent.Core)
|
||||
local Event = require(script.Parent.Event)
|
||||
local Change = require(script.Parent.Change)
|
||||
local getDefaultPropertyValue = require(script.Parent.getDefaultPropertyValue)
|
||||
local SingleEventManager = require(script.Parent.SingleEventManager)
|
||||
local Symbol = require(script.Parent.Symbol)
|
||||
|
||||
local isInstanceHandle = Symbol.named("isInstanceHandle")
|
||||
|
||||
local DEFAULT_SOURCE = "\n\t<Use Roact.setGlobalConfig with the 'elementTracing' key to enable detailed tracebacks>\n"
|
||||
|
||||
local function isPortal(element)
|
||||
if type(element) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
return element.component == Core.Portal
|
||||
end
|
||||
|
||||
--[[
|
||||
Sets the value of a reference to a new rendered object.
|
||||
Correctly handles both function-style and object-style refs.
|
||||
]]
|
||||
local function applyRef(ref, newRbx)
|
||||
if ref == nil then
|
||||
return
|
||||
end
|
||||
|
||||
if type(ref) == "table" then
|
||||
ref.current = newRbx
|
||||
else
|
||||
ref(newRbx)
|
||||
end
|
||||
end
|
||||
|
||||
local Reconciler = {}
|
||||
|
||||
Reconciler._singleEventManager = SingleEventManager.new()
|
||||
|
||||
--[[
|
||||
Is this element backed by a Roblox instance directly?
|
||||
]]
|
||||
local function isPrimitiveElement(element)
|
||||
if type(element) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
return type(element.component) == "string"
|
||||
end
|
||||
|
||||
--[[
|
||||
Is this element defined by a pure function?
|
||||
]]
|
||||
local function isFunctionalElement(element)
|
||||
if type(element) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
return type(element.component) == "function"
|
||||
end
|
||||
|
||||
--[[
|
||||
Is this element defined by a component class?
|
||||
]]
|
||||
local function isStatefulElement(element)
|
||||
if type(element) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
return type(element.component) == "table"
|
||||
end
|
||||
|
||||
--[[
|
||||
Destroy the given Roact instance, all of its descendants, and associated
|
||||
Roblox instances owned by the components.
|
||||
]]
|
||||
function Reconciler.unmount(instanceHandle)
|
||||
local element = instanceHandle._element
|
||||
|
||||
if isPrimitiveElement(element) then
|
||||
-- We're destroying a Roblox Instance-based object
|
||||
|
||||
-- Kill refs before we make changes, since any mutations past this point
|
||||
-- aren't relevant to components.
|
||||
applyRef(element.props[Core.Ref], nil)
|
||||
|
||||
for _, child in pairs(instanceHandle._children) do
|
||||
Reconciler.unmount(child)
|
||||
end
|
||||
|
||||
-- Necessary to make sure SingleEventManager doesn't leak references
|
||||
Reconciler._singleEventManager:disconnectAll(instanceHandle._rbx)
|
||||
|
||||
instanceHandle._rbx:Destroy()
|
||||
elseif isFunctionalElement(element) then
|
||||
-- Functional components can return nil
|
||||
if instanceHandle._child then
|
||||
Reconciler.unmount(instanceHandle._child)
|
||||
end
|
||||
elseif isStatefulElement(element) then
|
||||
instanceHandle._instance:_unmount()
|
||||
elseif isPortal(element) then
|
||||
for _, child in pairs(instanceHandle._children) do
|
||||
Reconciler.unmount(child)
|
||||
end
|
||||
else
|
||||
error(("Cannot unmount invalid Roact instance %q"):format(tostring(element)))
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Public interface to reifier. Hides parameters used when recursing down the
|
||||
component tree.
|
||||
]]
|
||||
function Reconciler.mount(element, parent, key)
|
||||
return Reconciler._mountInternal(element, parent, key)
|
||||
end
|
||||
|
||||
--[[
|
||||
Instantiates components to represent the given element.
|
||||
|
||||
Parameters:
|
||||
- `element`: The element to mount.
|
||||
- `parent`: The Roblox object to contain the contained instances
|
||||
- `key`: The Name to give the Roblox instance that gets created
|
||||
- `context`: Used to pass Roact context values down the tree
|
||||
|
||||
The structure created by this method is important to the functionality of
|
||||
the reconciliation methods; they depend on this structure being well-formed.
|
||||
]]
|
||||
function Reconciler._mountInternal(element, parent, key, context)
|
||||
if isPrimitiveElement(element) then
|
||||
-- Primitive elements are backed directly by Roblox Instances.
|
||||
|
||||
local rbx = Instance.new(element.component)
|
||||
|
||||
-- Update Roblox properties
|
||||
for key, value in pairs(element.props) do
|
||||
Reconciler._setRbxProp(rbx, key, value, element)
|
||||
end
|
||||
|
||||
-- Create children!
|
||||
local children = {}
|
||||
|
||||
if element.props[Core.Children] then
|
||||
for key, childElement in pairs(element.props[Core.Children]) do
|
||||
local childInstance = Reconciler._mountInternal(childElement, rbx, key, context)
|
||||
|
||||
children[key] = childInstance
|
||||
end
|
||||
end
|
||||
|
||||
-- This name can be passed through multiple components.
|
||||
-- Elements with the same key will be treated as the same
|
||||
-- element between reconciles; the old element will be
|
||||
-- reconciled to the new element with the same key.
|
||||
if key then
|
||||
rbx.Name = key
|
||||
end
|
||||
|
||||
rbx.Parent = parent
|
||||
|
||||
-- Attach ref values, since the instance is initialized now.
|
||||
applyRef(element.props[Core.Ref], rbx)
|
||||
|
||||
return {
|
||||
[isInstanceHandle] = true,
|
||||
_key = key,
|
||||
_parent = parent,
|
||||
_element = element,
|
||||
_context = context,
|
||||
_children = children,
|
||||
_rbx = rbx,
|
||||
}
|
||||
elseif isFunctionalElement(element) then
|
||||
-- Functional elements contain 0 or 1 children.
|
||||
|
||||
local instanceHandle = {
|
||||
[isInstanceHandle] = true,
|
||||
_key = key,
|
||||
_parent = parent,
|
||||
_element = element,
|
||||
_context = context,
|
||||
}
|
||||
|
||||
local vdom = element.component(element.props)
|
||||
if vdom then
|
||||
instanceHandle._child = Reconciler._mountInternal(vdom, parent, key, context)
|
||||
end
|
||||
|
||||
return instanceHandle
|
||||
elseif isStatefulElement(element) then
|
||||
-- Stateful elements have 0 or 1 children, and also have a backing
|
||||
-- instance that can keep state.
|
||||
|
||||
-- We separate the instance's implementation from our handle to it.
|
||||
local instanceHandle = {
|
||||
[isInstanceHandle] = true,
|
||||
_key = key,
|
||||
_parent = parent,
|
||||
_element = element,
|
||||
_child = nil,
|
||||
}
|
||||
|
||||
local instance = element.component._new(element.props, context)
|
||||
|
||||
instanceHandle._instance = instance
|
||||
instance:_mount(instanceHandle)
|
||||
|
||||
return instanceHandle
|
||||
elseif isPortal(element) then
|
||||
-- Portal elements have one or more children.
|
||||
|
||||
local target = element.props.target
|
||||
if not target then
|
||||
error(("Cannot mount Portal without specifying a target."):format(tostring(element)))
|
||||
elseif typeof(target) ~= "Instance" then
|
||||
error(("Cannot mount Portal with target of type %q."):format(typeof(target)))
|
||||
end
|
||||
|
||||
-- Create children!
|
||||
local children = {}
|
||||
|
||||
if element.props[Core.Children] then
|
||||
for key, childElement in pairs(element.props[Core.Children]) do
|
||||
local childInstance = Reconciler._mountInternal(childElement, target, key, context)
|
||||
|
||||
children[key] = childInstance
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
[isInstanceHandle] = true,
|
||||
_key = key,
|
||||
_parent = parent,
|
||||
_element = element,
|
||||
_context = context,
|
||||
_children = children,
|
||||
_rbx = target,
|
||||
}
|
||||
elseif typeof(element) == "boolean" then
|
||||
-- Ignore booleans of either value
|
||||
-- See https://github.com/Roblox/roact/issues/14
|
||||
return nil
|
||||
end
|
||||
|
||||
error(("Cannot mount invalid Roact element %q"):format(tostring(element)))
|
||||
end
|
||||
|
||||
--[[
|
||||
A public interface around _reconcileInternal
|
||||
]]
|
||||
function Reconciler.reconcile(instanceHandle, newElement)
|
||||
if instanceHandle == nil or not instanceHandle[isInstanceHandle] then
|
||||
local message = (
|
||||
"Bad argument #1 to Reconciler.reconcile, expected component instance handle, found %s"
|
||||
):format(
|
||||
typeof(instanceHandle)
|
||||
)
|
||||
|
||||
error(message, 2)
|
||||
end
|
||||
|
||||
return Reconciler._reconcileInternal(instanceHandle, newElement)
|
||||
end
|
||||
|
||||
--[[
|
||||
Applies the state given by newElement to an existing Roact instance.
|
||||
|
||||
reconcile will return the instance that should be used. This instance can
|
||||
be different than the one that was passed in.
|
||||
]]
|
||||
function Reconciler._reconcileInternal(instanceHandle, newElement)
|
||||
local oldElement = instanceHandle._element
|
||||
|
||||
-- Instance was deleted!
|
||||
if not newElement then
|
||||
Reconciler.unmount(instanceHandle)
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- If the element changes type, we assume its subtree will be substantially
|
||||
-- different. This lets us skip comparisons of a large swath of nodes.
|
||||
if oldElement.component ~= newElement.component then
|
||||
local parent = instanceHandle._parent
|
||||
local key = instanceHandle._key
|
||||
|
||||
local context
|
||||
if isStatefulElement(oldElement) then
|
||||
context = instanceHandle._instance._context
|
||||
else
|
||||
context = instanceHandle._context
|
||||
end
|
||||
|
||||
Reconciler.unmount(instanceHandle)
|
||||
|
||||
local newInstance = Reconciler._mountInternal(newElement, parent, key, context)
|
||||
|
||||
return newInstance
|
||||
end
|
||||
|
||||
if isPrimitiveElement(newElement) then
|
||||
-- Roblox Instance change
|
||||
|
||||
local oldRef = oldElement.props[Core.Ref]
|
||||
local newRef = newElement.props[Core.Ref]
|
||||
|
||||
-- Change the ref in one pass before applying any changes.
|
||||
-- Roact doesn't provide any guarantees with regards to the sequencing
|
||||
-- between refs and other changes in the commit phase.
|
||||
if newRef ~= oldRef then
|
||||
applyRef(oldRef, nil)
|
||||
applyRef(newRef, instanceHandle._rbx)
|
||||
end
|
||||
|
||||
-- Update properties and children of the Roblox object.
|
||||
Reconciler._reconcilePrimitiveProps(oldElement, newElement, instanceHandle._rbx)
|
||||
Reconciler._reconcilePrimitiveChildren(instanceHandle, newElement)
|
||||
|
||||
instanceHandle._element = newElement
|
||||
|
||||
return instanceHandle
|
||||
elseif isFunctionalElement(newElement) then
|
||||
instanceHandle._element = newElement
|
||||
|
||||
local rendered = newElement.component(newElement.props)
|
||||
local newChild
|
||||
|
||||
if instanceHandle._child then
|
||||
-- Transition from tree to tree, even if 'rendered' is nil
|
||||
newChild = Reconciler._reconcileInternal(instanceHandle._child, rendered)
|
||||
elseif rendered then
|
||||
-- Transition from nil to new tree
|
||||
newChild = Reconciler._mountInternal(
|
||||
rendered,
|
||||
instanceHandle._parent,
|
||||
instanceHandle._key,
|
||||
instanceHandle._context
|
||||
)
|
||||
end
|
||||
|
||||
instanceHandle._child = newChild
|
||||
|
||||
return instanceHandle
|
||||
elseif isStatefulElement(newElement) then
|
||||
instanceHandle._element = newElement
|
||||
|
||||
-- Stateful elements can take care of themselves.
|
||||
instanceHandle._instance:_update(newElement.props)
|
||||
|
||||
return instanceHandle
|
||||
elseif isPortal(newElement) then
|
||||
if instanceHandle._rbx ~= newElement.props.target then
|
||||
local parent = instanceHandle._parent
|
||||
local key = instanceHandle._key
|
||||
local context = instanceHandle._context
|
||||
|
||||
Reconciler.unmount(instanceHandle)
|
||||
|
||||
local newInstance = Reconciler._mountInternal(newElement, parent, key, context)
|
||||
|
||||
return newInstance
|
||||
end
|
||||
|
||||
Reconciler._reconcilePrimitiveChildren(instanceHandle, newElement)
|
||||
|
||||
instanceHandle._element = newElement
|
||||
|
||||
return instanceHandle
|
||||
end
|
||||
|
||||
error(("Cannot reconcile to match invalid Roact element %q"):format(tostring(newElement)))
|
||||
end
|
||||
|
||||
--[[
|
||||
Reconciles the children of an existing Roact instance and the given element.
|
||||
]]
|
||||
function Reconciler._reconcilePrimitiveChildren(instance, newElement)
|
||||
local elementChildren = newElement.props[Core.Children]
|
||||
|
||||
-- Reconcile existing children that were changed or removed
|
||||
for key, childInstance in pairs(instance._children) do
|
||||
local childElement = elementChildren and elementChildren[key]
|
||||
|
||||
childInstance = Reconciler._reconcileInternal(childInstance, childElement)
|
||||
|
||||
instance._children[key] = childInstance
|
||||
end
|
||||
|
||||
-- Create children that were just added!
|
||||
if elementChildren then
|
||||
for key, childElement in pairs(elementChildren) do
|
||||
-- Update if we didn't hit the child in the previous loop
|
||||
if not instance._children[key] then
|
||||
local childInstance = Reconciler._mountInternal(childElement, instance._rbx, key, instance._context)
|
||||
instance._children[key] = childInstance
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Reconciles the properties between two primitive Roact elements and applies
|
||||
the differences to the given Roblox object.
|
||||
]]
|
||||
function Reconciler._reconcilePrimitiveProps(fromElement, toElement, rbx)
|
||||
local seenProps = {}
|
||||
|
||||
-- Set properties that were set with fromElement
|
||||
for key, oldValue in pairs(fromElement.props) do
|
||||
seenProps[key] = true
|
||||
|
||||
local newValue = toElement.props[key]
|
||||
|
||||
-- Assume any property that can be set to nil has a default value of nil
|
||||
if newValue == nil then
|
||||
local _, value = getDefaultPropertyValue(rbx.ClassName, key)
|
||||
|
||||
-- We don't care if getDefaultPropertyValue fails, because
|
||||
-- _setRbxProp will catch the error below.
|
||||
newValue = value
|
||||
end
|
||||
|
||||
-- Roblox does this check for normal values, but we have special
|
||||
-- properties like events that warrant this.
|
||||
if oldValue ~= newValue then
|
||||
Reconciler._setRbxProp(rbx, key, newValue, toElement)
|
||||
end
|
||||
end
|
||||
|
||||
-- Set properties that are new in toElement
|
||||
for key, newValue in pairs(toElement.props) do
|
||||
if not seenProps[key] then
|
||||
seenProps[key] = true
|
||||
|
||||
local oldValue = fromElement.props[key]
|
||||
|
||||
if oldValue ~= newValue then
|
||||
Reconciler._setRbxProp(rbx, key, newValue, toElement)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Used in _setRbxProp to avoid creating a new closure for every property set.
|
||||
]]
|
||||
local function set(rbx, key, value)
|
||||
rbx[key] = value
|
||||
end
|
||||
|
||||
--[[
|
||||
Sets a property on a Roblox object, following Roact's rules for special
|
||||
case properties.
|
||||
|
||||
This function can throw a couple different errors. In the future, calls to
|
||||
_setRbxProp should be wrapped in a pcall to give better errors to the user.
|
||||
|
||||
For that to be useful, we'll need to attach a 'source' property on every
|
||||
element, created using debug.traceback(), that points to where the element
|
||||
was created.
|
||||
]]
|
||||
function Reconciler._setRbxProp(rbx, key, value, element)
|
||||
if type(key) == "string" then
|
||||
-- Regular property
|
||||
|
||||
local success, err = pcall(set, rbx, key, value)
|
||||
|
||||
if not success then
|
||||
local source = element.source or DEFAULT_SOURCE
|
||||
|
||||
local message = ("Failed to set property %s on primitive instance of class %s\n%s\n%s"):format(
|
||||
key,
|
||||
rbx.ClassName,
|
||||
err,
|
||||
source
|
||||
)
|
||||
|
||||
error(message, 0)
|
||||
end
|
||||
elseif type(key) == "table" then
|
||||
-- Special property with extra data attached.
|
||||
|
||||
if key.type == Event then
|
||||
Reconciler._singleEventManager:connect(rbx, key.name, value)
|
||||
elseif key.type == Change then
|
||||
Reconciler._singleEventManager:connectProperty(rbx, key.name, value)
|
||||
else
|
||||
local source = element.source or DEFAULT_SOURCE
|
||||
|
||||
-- luacheck: ignore 6
|
||||
local message = ("Failed to set special property on primitive instance of class %s\nInvalid special property type %q\n%s"):format(
|
||||
rbx.ClassName,
|
||||
tostring(key.type),
|
||||
source
|
||||
)
|
||||
|
||||
error(message, 0)
|
||||
end
|
||||
elseif type(key) ~= "userdata" then
|
||||
-- Userdata values are special markers, usually created by Symbol
|
||||
-- They have no data attached other than being unique keys
|
||||
|
||||
local source = element.source or DEFAULT_SOURCE
|
||||
|
||||
local message = ("Properties with a key type of %q are not supported\n%s"):format(
|
||||
type(key),
|
||||
source
|
||||
)
|
||||
|
||||
error(message, 0)
|
||||
end
|
||||
end
|
||||
|
||||
return Reconciler
|
||||
Reference in New Issue
Block a user