@@ -34,6 +34,17 @@ const HAS_PENDING_UPDATE = 1 << 0;
34
34
const HAS_HOOK_STATE = 1 << 1 ;
35
35
const HAS_COMPUTEDS = 1 << 2 ;
36
36
37
+ let oldNotify : ( this : Effect ) => void ,
38
+ effectsQueue : Array < Effect > = [ ] ,
39
+ domQueue : Array < Effect > = [ ] ;
40
+
41
+ // Capture the original `Effect.prototype._notify` method so that we can install
42
+ // custom `._notify`s for each different use-case but still call the original
43
+ // implementation in the end. Dispose the temporary effect immediately afterwards.
44
+ effect ( function ( this : Effect ) {
45
+ oldNotify = this . _notify ;
46
+ } ) ( ) ;
47
+
37
48
// Install a Preact options hook
38
49
function hook < T extends OptionsTypes > ( hookName : T , hookFn : HookFn < T > ) {
39
50
// @ts -ignore-next-line private options hooks usage
@@ -80,7 +91,7 @@ function SignalValue(this: AugmentedComponent, { data }: { data: Signal }) {
80
91
const currentSignal = useSignal ( data ) ;
81
92
currentSignal . value = data ;
82
93
83
- const s = useMemo ( ( ) => {
94
+ const [ isText , s ] = useMemo ( ( ) => {
84
95
let self = this ;
85
96
// mark the parent component as having computeds so it gets optimized
86
97
let v = this . __v ;
@@ -91,38 +102,51 @@ function SignalValue(this: AugmentedComponent, { data }: { data: Signal }) {
91
102
}
92
103
}
93
104
94
- const wrappedSignal = computed ( function ( this : Effect ) {
95
- let data = currentSignal . value ;
96
- let s = data . value ;
105
+ const wrappedSignal = computed ( ( ) => {
106
+ let s = currentSignal . value . value ;
97
107
return s === 0 ? 0 : s === true ? "" : s || "" ;
98
108
} ) ;
99
109
100
- const isText = computed (
101
- ( ) => isValidElement ( wrappedSignal . value ) || this . base ?. nodeType !== 3
102
- ) ;
110
+ const isText = computed ( ( ) => ! isValidElement ( wrappedSignal . value ) ) ;
103
111
104
- this . _updater ! . _callback = ( ) => {
105
- if ( isValidElement ( s . peek ( ) ) || this . base ?. nodeType !== 3 ) {
106
- this . _updateFlags |= HAS_PENDING_UPDATE ;
107
- this . setState ( { } ) ;
108
- return ;
109
- }
110
- ( this . base as Text ) . data = s . peek ( ) ;
111
- } ;
112
-
113
- effect ( function ( this : Effect ) {
114
- if ( ! oldNotify ) oldNotify = this . _notify ;
112
+ // Update text nodes directly without rerendering when the new value
113
+ // is also text.
114
+ const dispose = effect ( function ( this : Effect ) {
115
115
this . _notify = notifyDomUpdates ;
116
- const val = wrappedSignal . value ;
117
- if ( isText . value && self . base ) {
118
- ( self . base as Text ) . data = val ;
116
+
117
+ // Subscribe to wrappedSignal updates only when its values are text...
118
+ if ( isText . value ) {
119
+ // ...but regardless of `self.base`'s current value, as it can be
120
+ // undefined before mounting or a non-text node. In both of those cases
121
+ // the update gets handled by a full rerender.
122
+ const value = wrappedSignal . value ;
123
+ if ( self . base && self . base . nodeType === 3 ) {
124
+ ( self . base as Text ) . data = value ;
125
+ }
119
126
}
120
127
} ) ;
121
128
122
- return wrappedSignal ;
129
+ // Piggyback this._updater's disposal to ensure that the text updater effect
130
+ // above also gets disposed on unmount.
131
+ const oldDispose = this . _updater ! . _dispose ;
132
+ this . _updater ! . _dispose = function ( ) {
133
+ dispose ( ) ;
134
+ oldDispose . call ( this ) ;
135
+ } ;
136
+
137
+ return [ isText , wrappedSignal ] ;
123
138
} , [ ] ) ;
124
139
125
- return s . value ;
140
+ // Rerender the component whenever `data.value` changes from a VNode
141
+ // to another VNode, from text to a VNode, or from a VNode to text.
142
+ // That is, everything else except text-to-text updates.
143
+ //
144
+ // This also ensures that the backing DOM node types gets updated to
145
+ // text nodes and back when needed.
146
+ //
147
+ // For text-to-text updates, `.peek()` is used to skip full rerenders,
148
+ // leaving them to the optimized path above.
149
+ return isText . value ? s . peek ( ) : s . value ;
126
150
}
127
151
SignalValue . displayName = "_st" ;
128
152
@@ -255,7 +279,6 @@ function createPropUpdater(
255
279
props = newProps ;
256
280
} ,
257
281
_dispose : effect ( function ( this : Effect ) {
258
- if ( ! oldNotify ) oldNotify = this . _notify ;
259
282
this . _notify = notifyDomUpdates ;
260
283
const value = changeSignal . value . value ;
261
284
// If Preact just rendered this value, don't render it again:
@@ -321,38 +344,28 @@ Component.prototype.shouldComponentUpdate = function (
321
344
const updater = this . _updater ;
322
345
const hasSignals = updater && updater . _sources !== undefined ;
323
346
324
- // let reason;
325
- // if (!hasSignals && !hasComputeds.has(this)) {
326
- // reason = "no signals or computeds";
327
- // } else if (hasPendingUpdate.has(this)) {
328
- // reason = "has pending update";
329
- // } else if (hasHookState.has(this)) {
330
- // reason = "has hook state";
331
- // }
332
- // if (reason) {
333
- // if (!this) reason += " (`this` bug)";
334
- // console.log("not optimizing", this?.constructor?.name, ": ", reason, {
335
- // details: {
336
- // hasSignals,
337
- // hasComputeds: hasComputeds.has(this),
338
- // hasPendingUpdate: hasPendingUpdate.has(this),
339
- // hasHookState: hasHookState.has(this),
340
- // deps: Array.from(updater._deps),
341
- // updater,
342
- // },
343
- // });
344
- // }
345
-
346
- // if this component used no signals or computeds, update:
347
- if ( ! hasSignals && ! ( this . _updateFlags & HAS_COMPUTEDS ) ) return true ;
348
-
349
- // if there is a pending re-render triggered from Signals,
350
- // or if there is hook or class state, update:
351
- if ( this . _updateFlags & ( HAS_PENDING_UPDATE | HAS_HOOK_STATE ) ) return true ;
352
-
347
+ // If this is a component using state, rerender
353
348
// @ts -ignore
354
349
for ( let i in state ) return true ;
355
350
351
+ if ( this . __f || ( typeof this . u == "boolean" && this . u === true ) ) {
352
+ const hasHooksState = this . _updateFlags & HAS_HOOK_STATE ;
353
+ // if this component used no signals or computeds and no hooks state, update:
354
+ if ( ! hasSignals && ! hasHooksState && ! ( this . _updateFlags & HAS_COMPUTEDS ) )
355
+ return true ;
356
+
357
+ // if there is a pending re-render triggered from Signals,
358
+ // or if there is hooks state, update:
359
+ if ( this . _updateFlags & HAS_PENDING_UPDATE ) return true ;
360
+ } else {
361
+ // if this component used no signals or computeds, update:
362
+ if ( ! hasSignals && ! ( this . _updateFlags & HAS_COMPUTEDS ) ) return true ;
363
+
364
+ // if there is a pending re-render triggered from Signals,
365
+ // or if there is hooks state, update:
366
+ if ( this . _updateFlags & ( HAS_PENDING_UPDATE | HAS_HOOK_STATE ) ) return true ;
367
+ }
368
+
356
369
// if any non-Signal props changed, update:
357
370
for ( let i in props ) {
358
371
if ( i !== "__source" && props [ i ] !== this . props [ i ] ) return true ;
@@ -379,10 +392,6 @@ export function useComputed<T>(compute: () => T, options?: SignalOptions<T>) {
379
392
return useMemo ( ( ) => computed < T > ( ( ) => $compute . current ( ) , options ) , [ ] ) ;
380
393
}
381
394
382
- let oldNotify : ( this : Effect ) => void ,
383
- effectsQueue : Array < Effect > = [ ] ,
384
- domQueue : Array < Effect > = [ ] ;
385
-
386
395
const deferEffects =
387
396
typeof requestAnimationFrame === "undefined"
388
397
? setTimeout
@@ -430,7 +439,6 @@ export function useSignalEffect(cb: () => void | (() => void)) {
430
439
431
440
useEffect ( ( ) => {
432
441
return effect ( function ( this : Effect ) {
433
- if ( ! oldNotify ) oldNotify = this . _notify ;
434
442
this . _notify = notifyEffects ;
435
443
return callback . current ( ) ;
436
444
} ) ;
0 commit comments