@@ -140,8 +140,15 @@ const err = (e, expr, el = currentEl) => {
140140 * @property {Record<string, Signal> } [_signals] - Internal signals map
141141 */
142142
143- /** Symbol for cached directive scan on clone masters */
144- const _dirs = Symbol ( 'dirs' )
143+ /** Symbol for the flat directive program on clone masters: [{p: childNodes-index path, d: dirs}] in DFS order */
144+ const _prog = Symbol ( 'program' )
145+
146+ /** Is q a strict descendant path of p */
147+ const inside = ( p , q ) => {
148+ if ( q . length <= p . length ) return false
149+ for ( let i = 0 ; i < p . length ; i ++ ) if ( p [ i ] !== q [ i ] ) return false
150+ return true
151+ }
145152
146153/**
147154 * Applies directives to an HTML element and manages its reactive state.
@@ -196,39 +203,27 @@ const sprae = (root = document.body, state, master) => {
196203 )
197204 let start
198205
206+ // recording state: prog collects (path, dirs) per directive-bearing element while the first clone is scanned
207+ let prog = master ? master [ _prog ] : undefined , path = prog === undefined && master ? [ ] : null
208+
199209 const add = el [ _add ] = ( el , mel ) => {
200- let dirs = mel ?. [ _dirs ]
210+ let _attrs = el . attributes , rec = mel && [ ]
201211
202- // replay master's recorded scan — no attribute reads, no parsing
203- if ( dirs !== undefined ) {
204- // clones from the recording batch still carry directive attrs (more than master's residual) — strip them;
205- // later clones come from the stripped master and skip removeAttribute entirely
206- let strip = dirs . length && el . attributes . length > dirs . n , d
207- for ( let i = 0 ; i < dirs . length ; i ++ ) {
208- d = dirs [ i ]
209- if ( strip ) el . removeAttribute ( d [ 0 ] )
210- if ( apply ( el , d [ 0 ] , d [ 1 ] , d [ 2 ] ) ) return
211- }
212- }
213- else {
214- let _attrs = el . attributes , rec = mel && ( mel [ _dirs ] = [ ] )
215-
216- if ( _attrs ) for ( let i = 0 ; i < _attrs . length ; ) {
217- let { name, value } = _attrs [ i ]
218-
219- if ( name . startsWith ( prefix ) ) {
220- el . removeAttribute ( name )
221- // strip master too: later clones come out clean; unprocessed tail (after a stop) stays for subsprae
222- mel ?. removeAttribute ( name )
223- let short = name . slice ( prefix . length )
224- rec ?. push ( [ name , short , value ] )
225-
226- // n = master's residual attr count (directives may add attrs to el, so el.attributes can't be the reference)
227- if ( apply ( el , name , short , value ) ) return rec && ( rec . n = mel . attributes . length ) , undefined
228- } else i ++
229- }
230- if ( rec ) rec . n = mel . attributes . length
212+ if ( _attrs ) for ( let i = 0 ; i < _attrs . length ; ) {
213+ let { name, value } = _attrs [ i ]
214+
215+ if ( name . startsWith ( prefix ) ) {
216+ el . removeAttribute ( name )
217+ // strip master too: later clones come out clean; unprocessed tail (after a stop) stays for subsprae
218+ mel ?. removeAttribute ( name )
219+ let short = name . slice ( prefix . length )
220+ rec ?. push ( [ name , short , value ] )
221+
222+ // n = master's residual attr count (directives may add attrs to el, so el.attributes can't be the reference)
223+ if ( apply ( el , name , short , value ) ) return rec ?. length && ( rec . n = mel . attributes . length , record ( rec ) ) , undefined
224+ } else i ++
231225 }
226+ if ( rec ?. length ) rec . n = mel . attributes . length , record ( rec )
232227
233228 // custom elements own their children — don't descend
234229 if ( el !== root && isCE ( el ) ) return
@@ -237,13 +232,49 @@ const sprae = (root = document.body, state, master) => {
237232 // real DOM: firstChild/nextSibling avoids array copy; frag.childNodes is already snapshot array
238233 if ( el . firstChild !== undefined ) {
239234 // master is never mutated structurally, so its pointers stay aligned with pre-captured clone pointers
240- let child = el . firstChild , mchild = mel ?. firstChild , next
241- while ( child ) ( next = child . nextSibling , child . nodeType == 1 && add ( child , mchild ) , mchild &&= mchild . nextSibling , child = next )
235+ let child = el . firstChild , mchild = mel ?. firstChild , next , idx = 0
236+ while ( child ) (
237+ next = child . nextSibling ,
238+ child . nodeType == 1 && ( path ?. push ( idx ) , add ( child , mchild ) , path ?. pop ( ) ) ,
239+ mchild &&= mchild . nextSibling , child = next , idx ++
240+ )
242241 }
243242 else for ( let child of el . childNodes ) child . nodeType == 1 && add ( child )
244243 } ;
245244
246- add ( el , master ) ;
245+ const record = rec => path && prog . push ( { p : path . slice ( ) , d : rec } )
246+
247+ // replay master's flat program — no attribute scans, no tree walk, dir-less elements never visited
248+ const replay = prog => {
249+ let m = prog . length , nodes = Array ( m ) , node , p , d , e , h , i
250+ // resolve all nodes upfront: applying a directive may move nodes (:portal) and shift later paths
251+ for ( e = 0 ; e < m ; e ++ ) {
252+ p = prog [ e ] . p , node = el
253+ for ( h = 0 ; h < p . length ; h ++ ) node = node . childNodes [ p [ h ] ]
254+ nodes [ e ] = node
255+ }
256+ for ( e = 0 ; e < m ; e ++ ) {
257+ p = prog [ e ] . p , d = prog [ e ] . d , node = nodes [ e ]
258+ // clones from the recording batch still carry directive attrs (more than master's residual) — strip them;
259+ // later clones come from the stripped master and skip removeAttribute entirely
260+ let strip = node . attributes . length > d . n
261+ for ( i = 0 ; i < d . length ; i ++ ) {
262+ if ( strip ) node . removeAttribute ( d [ i ] [ 0 ] )
263+ if ( apply ( node , d [ i ] [ 0 ] , d [ i ] [ 1 ] , d [ i ] [ 2 ] ) ) {
264+ // stop: skip this element's recorded descendants (subsprae owns them)
265+ while ( e + 1 < m && inside ( p , prog [ e + 1 ] . p ) ) e ++
266+ break
267+ }
268+ }
269+ }
270+ }
271+
272+ if ( prog !== undefined ) replay ( prog )
273+ else {
274+ if ( path ) prog = master [ _prog ] = [ ]
275+ add ( el , master )
276+ path = null // MO-added nodes reuse `add` later — no recording for those
277+ }
247278
248279 currentDir = currentEl = null ;
249280
0 commit comments