What are Signals?
In 2024, Signals is increasingly becoming the mainstream underlying technology dependence in the front-end framework. Probably which framework started to emerge, I have forgotten a little. The first one in my image to promote and popular was SolidJS
. Later, this technology was also introduced in Vue3
, followed by MobX
, Preact
, Qwik
, Svelte
, Angular
, etc. This technology has also been introduced one after another.
In fact, the proposed Signals
was much earlier than SolidJS
, as early as 2010, Knockout has a similar implementation.
Therefore, Signals
is not an "emerging, cutting-edge" front-end technology solution. On the contrary, the arguments, practices and applications of it are already quite mature. This is also the reason why Signals
is becoming more and more popular with various frameworks nowadays.
Description
TC39 Added a Stage-0 Proposal. It is planned to integrate Signals
into the JavaScript standard. And invited Angular, Bubble, Ember, FAST, MobX, Preact, Qwik, RxJS, Solid, Starbeam, Svelte, Vue, Wiz Authors or contributors from multiple frameworks will participate in the discussion and jointly promote the implementation of the proposal.
I was quite excited to hear this news, which means that when Signals
officially becomes the standard of ECMA
, it will bring great convenience to our front-end development. Especially for major frameworks, using native Signals
may lead to greater performance improvements, and even have the opportunity to share the same Signals
between different frameworks. Bring more possibilities.
Why Signals?
In a common scenario, we need to implement a counter and want to render the value of the current counter to the page, Whenever the value of the counter changes, we re-render the page.
When we use native JavaScript implementation:
let counter = 0
const setCounter = (value) => {
counter = value
render()
}
const isEven = () => (counter & 1) == 0
const parity = () => (isEven() ? 'even' : 'odd')
const render = () => (element.innerText = parity())
// Simulate external updates to the counter...
setInterval(() => setCounter(counter + 1), 1000)
This seems to achieve the requirements, however, there are many problems here:
counter
state is tightly coupled with the rendering system;- If
counter
changes, butparity
does not change (such ascounter
changes from 2 to 4), Then unnecessary calculations and unnecessary rendering of parity will be performed; - What should I do if another part of the UI just wants to re-render when the
counter
changes? - What if another part of the UI depends only on
isEven
orparity
?
Even in this relatively simple situation, many problems will soon arise. We can try to solve these issues by introducing Publish/Subscribe. This will allow other consumers of counter
to subscribe to add their own reactions to state changes.
However, we may still have more problems:
- The rendering function
render
only depends onparity
, however, it actually requires a subscription tocounter
. - If you do not interact directly with
counter
, you cannot update the UI based solely onisEven
andparity
. - Introduced Publish/Subscribe, the problem is no longer just calling functions and reading variables, but subscribing and where to update, The issue of how to manage unsubscribe has also become complicated.
We resolve several issues including counter
, isEven
and parity
by adding publish/subscribe
. We must subscribe isEven
to counter
, subscribe parity
to isEven
, and subscribe render
to parity
. Then, this sample code is getting bigger and bigger, and we are stuck in a lot of subscriptions, if we don't have the right way to clean up the content, This may cause memory disasters. Although we solved some problems, we wrote a lot of code and introduced more problems.
Introducing Signals
Info
For ease of understanding, the following example uses the solid-js API, where:
[getter, setter] = createSignal(initial)
: Creates a signal, returning an array containing getter and setter.createMemo(getter)
: Creates a read-only response value equal to the return value of the given function.createEffect(effect)
: Create a side effect that is executed when its dependency changes.
To understand Signals
, let's first make some modifications to the above example:
const [counter, setCounter] = createSignal(0)
const isEven = createMemo(() => counter() & (1 == 0))
const parity = createMemo(() => (isEven() ? 'even' : 'odd'))
createEffect(() => (element.innerText = parity()))
setInterval(() => setCounter(counter() + 1), 1000)
We can see immediately:
- We eliminate unnecessary coupling of
counter
- Use a unified API to process values, calculate and side effects
- The problem of no circular reference between
counter
andrender
- No manual subscription, no need to record dependencies
- Can control the call timing of side effects
At the same time, Signals brings us more than just what we see on the surface:
- Automatic dependency tracking: The calculated Signal will automatically discover any other Signal it depends on, whether these Signals are simple values or other calculated values.
- Delayed Evaluation: The calculation is not evaluated immediately upon declaration, nor is it reevaluated immediately upon dependency updates. They are evaluated only when the value is explicitly requested.
- Memory: Calculate the signal caches its last value so that no matter how many times it is visited, there is no need to reevaluate the calculations that have not changed in its dependencies.
What are Signals?
Signals
, i.e., a signal, which represents a data unit that may change over time. The signal can be "State" (just a manually set value) or "Computed" (a formula based on other signals). It is commonly known as Signals
, or it can be called Observables, Atoms, Subjects, and Refs.
Signals
is usually composed of getter
, setter
, value
:
const [count, setCount] = createSignal(0)
// Read value
console.log(count()) // 0
// Set value
setCount(5)
console.log(count()) //5
There seems to be nothing special about this, just a wrapper that can store values of any type. But its focus is that getter
and setter
can run in any code, which is very helpful for updated publish and subscriptions.
Maybe you already know that in Vue, it is implemented using Object getters or Proxies:
const count = ref(0)
// Read value
console.log(count.value) // 0
// Set value
count.value = 5
console.log(count.value) // 5
Or, like Svelte, hide behind the compiler:
let count = 0
// Read value
console.log(count) // 0
// Set value
count = 5
**In essence, signals are event transmitters. But the main difference lies in the way subscriptions are managed. **
Reactive
If other Reactives related to it are missing, it doesn't seem special to just look at Signals
. Reactive
, also known as Effects
, Autoruns
, Watches
, or Computed
. Reactive
is run again when it updates occur by observing Signals
.
console.log('1. Create Signal')
const [count, setCount] = createSignal(0)
console.log('2. Create Reaction')
createEffect(() => console.log('The count is', count()))
console.log('3. Set count to 5')
setCount(5)
console.log('4. Set count to 10')
setCount(10)
**Output: **
1. Create Signal
2. Create Reaction
The count is 0
3. Set count to 5
The count is 5
4. Set count to 10
The count is 10
This looks a bit magical, and the callback function of createEffect
was executed immediately after setCount()
. This doesn't seem to be directly related, but in fact, in the createEffect
callback function, it is automatically executed when count()
Subscribed to the createEffect
callback function, and the createEffect
callback function is re-run whenever count
changes.
How to implement Signals?
TODO...