|
| 1 | +--- |
| 2 | +title: Deferred Engine Events |
| 3 | +description: Deferred engine events defer event handlers until certain resumption points. |
| 4 | +--- |
| 5 | + |
| 6 | +The `Class.Workspace.SignalBehavior` property controls whether event handlers are fired immediately or deferred. We recommend the `Enum.SignalBehavior.Deferred` option, which helps improve the performance and correctness of the engine. The event handlers for **deferred events** are resumed at the next [resumption point](#resumption-points), along with any newly triggered event handlers. |
| 7 | + |
| 8 | +<Alert severity="info"> |
| 9 | +The `Enum.SignalBehavior.Default` value of `Class.Workspace.SignalBehavior` is currently equivalent to `Enum.SignalBehavior.Immediate`, but will eventually switch to being equivalent to `Enum.SignalBehavior.Deferred`. Template places are directly set to `Enum.SignalBehavior.Deferred` by default. |
| 10 | +</Alert> |
| 11 | + |
| 12 | +The following diagram compares the `Enum.SignalBehavior.Immediate|Immediate` event behavior and the `Enum.SignalBehavior.Deferred|Deferred` event behavior. |
| 13 | + |
| 14 | +- With the `Immediate` behavior, if an event triggers another event, the second event handler fires immediately. |
| 15 | +- With the `Deferred` behavior, the second event is added to the back of a queue and run later. |
| 16 | + |
| 17 | +The total time taken does not change, but the ordering is different. |
| 18 | + |
| 19 | +<img alt="A comparison of three event handlers firing with Immediate and Deferred behavior" src="../../assets/scripting/scripts/ImmediateVsDeferredEvents.png" width="100%" /> |
| 20 | + |
| 21 | +"Re-entrancy" prevents events from continuously firing one another when they reach a certain depth. The current limit for this is 10. |
| 22 | + |
| 23 | +## Deferred Event Benefits |
| 24 | + |
| 25 | +The `Immediate` behavior has some disadvantages. For every instance added to your game, property that changes, or some other trigger that is invoked, the engine needs to run Lua code before anything else happens. |
| 26 | + |
| 27 | +- To change 1,000 properties, 1,000 snippets of code potentially need to run after each change. |
| 28 | +- Strange, hard-to-diagnose bugs can occur, such as a removing event firing before something was even added. |
| 29 | +- Performance-critical systems can fire events requiring them to yield back and forth to Lua. |
| 30 | +- Event handlers can make changes to the place or trigger other events any time an event is fired. |
| 31 | +- An event can fire multiple times despite being redundant, such as a property changing twice. |
| 32 | + |
| 33 | +By having specific portions of the engine life cycle in which Lua can run, the engine can gain improved performance by using a number of assumptions: |
| 34 | + |
| 35 | +- Performance-critical systems don't need to yield to Lua, which leads to performance gains. |
| 36 | +- Unless the engine itself changes it, the place never changes outside of a resumption point. |
| 37 | + |
| 38 | +## Resumption Points |
| 39 | + |
| 40 | +After being deferred, an event handler is resumed at the next resumption point. Currently, the set of resumption points includes: |
| 41 | + |
| 42 | +- Input processing (resumes once per input to be processed, see `Class.UserInputService`) |
| 43 | +- `Class.RunService.PreRender` |
| 44 | +- Legacy waiting script resumption such as `wait()`, `spawn()`, and `delay()` |
| 45 | +- `Class.RunService.PreAnimation` |
| 46 | +- `Class.RunService.PreSimulation` |
| 47 | +- `Class.RunService.PostSimulation` |
| 48 | +- Waiting script resumption such as `Library.task.wait()`, `Library.task.spawn()`, and `Library.task.delay()` |
| 49 | +- `Class.RunService.Heartbeat` |
| 50 | +- `Class.DataModel.BindToClose` |
| 51 | + |
| 52 | +## Common Affected Code Patterns |
| 53 | + |
| 54 | +With remote events, the following examples either stop working correctly or have subtly different behavior; they rely on events being resumed immediately. |
| 55 | + |
| 56 | +### Triggering and Catching Events Mid-Execution |
| 57 | + |
| 58 | +In this example, `false` is always returned when deferred events are enabled because the callback has not run. To work correctly, the thread must yield until at least when the event should have fired. |
| 59 | + |
| 60 | +```lua |
| 61 | +local success = false |
| 62 | +event:Connect(function () |
| 63 | + success = true |
| 64 | +end) |
| 65 | +doSomethingToTriggerEvent() -- Causes `event` to fire |
| 66 | +return success |
| 67 | +``` |
| 68 | + |
| 69 | +### Listening for the First Occurrence of an Event |
| 70 | + |
| 71 | +```lua |
| 72 | +connection = event:Connect(function () |
| 73 | + connection:Disconnect() |
| 74 | + -- do something |
| 75 | +end) |
| 76 | +``` |
| 77 | + |
| 78 | +With deferred events enabled, multiple event handler invocations can be queued before you disconnect from the event. Calling `Class.Instance.Disconnect()|Disconnect()` drops all pending event handler invocations—the same behavior that exists for immediate events. |
| 79 | + |
| 80 | +<Alert severity="warning"> |
| 81 | +Any other method of disconnection besides `Class.Instance.Disconnect()|Disconnect()`, such as calling `Class.Instance.Destroy()|Destroy()` on the `Class.Instance`, disconnects the signal immediately, but runs the associated event handler for any events that are still pending. |
| 82 | +</Alert> |
| 83 | + |
| 84 | +Alternatively, use `Datatype.RBXScriptSignal.Once()|Once()` as a more convenient method for connecting to an event that you only need the first invocation of. |
| 85 | + |
| 86 | +### Events That Change Ancestry or Properties |
| 87 | + |
| 88 | +Deferred events cause events that handle a change in ancestry or a property to fire after the ancestry or property is changed: |
| 89 | + |
| 90 | +```lua |
| 91 | +local part = Instance.new("Part", workspace) |
| 92 | + |
| 93 | +local function onPartDestroying() |
| 94 | + print("In signal:", part:GetFullName(), #part:GetChildren()) |
| 95 | +end |
| 96 | + |
| 97 | +part.Destroying:Connect(onPartDestroying) |
| 98 | +part:Destroy() |
| 99 | +``` |
| 100 | + |
| 101 | +Because `Class.Instance.Destroy()|Destroy()` works immediately after the script that called it yields, the instance has already been destroyed by the time `onPartDestroying()` is called. For more examples, see `Class.Instance.Destroying`. |
0 commit comments