import { isObject } from '@goldfishjs/utils' ; import { IWatchOptions } from './watch' ; import { ChangeOptions , getCurrent , call } from './dep' ; import isRaw from './isRaw' ; import generateKeyPathString from './generateKeyPathString' ; export interface IWatchDeepCallback { ( obj , keyPathList , newV , oldV , options ? ) : void ; } export interface IWatchDeepOptions extends Omit < IWatchOptions , 'deep' > { result ? : any ; } type KeyPathList = ( string | number ) [ ] ; class StopFns { private fns : Record < string , Record < string , ( ) => void > > = { } ; public add ( keyPathList , curKey , fn ) { const keyPathString = generateKeyPathString ( keyPathList ) ; if ( ! this . fns [ keyPathString ] ) { this . fns [ keyPathString ] = { } ; } if ( this . fns [ keyPathString ] [ curKey ] ) { throw new Error ( `Duplicate stop function for key: ${ generateKeyPathString ( [ ... keyPathList , curKey ] ) }` ) ; } this . fns [ keyPathString ] [ curKey ] = fn ; } param keyPathList public remove ( keyPathList ) { const keyPathString = generateKeyPathString ( keyPathList ) ; for ( const kps in this . fns ) { if ( kps . indexOf ( keyPathString ) === 0 ) { for ( const k in this . fns [ kps ] ) { this . fns [ kps ] [ k ] ( ) ; } this . fns [ kps ] = { } ; } } } public removeChildren ( keyPathList ) { const keyPathString = generateKeyPathString ( keyPathList ) ; for ( const kps in this . fns ) { if ( kps !== keyPathString && kps . indexOf ( keyPathString ) === 0 ) { for ( const k in this . fns [ kps ] ) { this . fns [ kps ] [ k ] ( ) ; } this . fns [ kps ] = { } ; } } } public removeSingleLayer ( keyPathList ) { const keyPathString = generateKeyPathString ( keyPathList ) ; for ( const kps in this . fns ) { if ( kps === keyPathString ) { for ( const k in this . fns [ kps ] ) { this . fns [ kps ] [ k ] ( ) ; } this . fns [ kps ] = { } ; } } } private callAll ( ) { for ( const k1 in this . fns ) { for ( const k2 in this . fns [ k1 ] ) { this . fns [ k1 ] [ k2 ] ( ) ; } } } public removeAll ( ) { this . callAll ( ) ; this . fns = { } ; } } class Watcher { private obj : any ; private options ? : IWatchDeepOptions ; private callback : IWatchDeepCallback ; private stopFns = new StopFns ( ) ; public constructor ( obj , callback , options ? ) { this . obj = obj ; this . callback = callback ; this . options = options ; } private watchObj ( obj , keyPathList , options ? ) { if ( isRaw ( obj ) ) { return ; } if ( Array . isArray ( obj ) ) { if ( options ?. isArray ) { const method = options . method ! ; const oldV = options . oldV ! ; const args = options . args ! ; if ( method === 'sort' || method === 'reverse' ) { for ( let i = 0 , il = obj . length ; i < il ; i ++ ) { this . stopFns . removeChildren ( [ ... keyPathList , i ] ) ; this . watchSingleKey ( obj , i , keyPathList ) ; } } else { const start = { push : oldV . length , splice : args [ 0 ] , unshift : 0 , pop : oldV . length - 1 , shift : 0 , } [ method ] ; const deletedCount = { push : 0 , splice : args [ 1 ] , unshift : 0 , pop : 1 , shift : 1 , } [ method ] ; const values = { push : args , splice : args . slice ( 2 ) , unshift : args , pop : [ ] , shift : [ ] , } [ method ] ; if ( deletedCount === values . length ) { for ( let i = start , il = deletedCount ; i < il ; i ++ ) { this . stopFns . removeChildren ( [ ... keyPathList , i ] ) ; this . watchSingleKey ( obj , i , keyPathList ) ; } } else { for ( let i = start , il = oldV . length ; i < il ; i ++ ) { this . stopFns . removeChildren ( [ ... keyPathList , i ] ) ; this . watchObj ( obj [ i ] , [ ... keyPathList , i ] ) ; } for ( let i = oldV . length , il = obj . length ; i < il ; i ++ ) { this . watchSingleKey ( obj , i , keyPathList ) ; } for ( let i = obj . length , il = oldV . length ; i < il ; i ++ ) { this . stopFns . removeSingleLayer ( [ ... keyPathList , i ] ) ; this . stopFns . removeChildren ( [ ... keyPathList , i ] ) ; } } } } else { this . stopFns . remove ( keyPathList ) ; obj . forEach ( ( _ , index ) => { this . watchSingleKey ( obj , index , keyPathList ) ; } ) ; } } else if ( isObject ( obj ) ) { this . stopFns . remove ( keyPathList ) ; for ( const key in obj ) { this . watchSingleKey ( obj , key , keyPathList ) ; } } } private watchCurrentKeyOnly ( obj , key , keyPathList ) { const nextKeyPathList = [ ... keyPathList , key ] ; call ( ( ) => { obj [ key ] ; const stopList = getCurrent ( ) . addChangeListener ( ( newV , oldV , options ) => { this . watchObj ( newV , nextKeyPathList , options ) ; this . callback ( this . obj , nextKeyPathList , newV , oldV , options ) ; } , false ) ; this . stopFns . add ( keyPathList , key , ( ) => stopList . forEach ( s => s ( ) ) ) ; } , e => { if ( this . options ?. onError ) { this . options . onError ( e ) ; } else { throw e ; } } ) ; return nextKeyPathList ; } private watchSingleKey ( obj , key , keyPathList ) { const nextKeyPathList = this . watchCurrentKeyOnly ( obj , key , keyPathList ) ; this . watchObj ( obj [ key ] , nextKeyPathList ) ; } public watch ( ) { if ( this . options ?. immediate ) { this . callback ( this . obj , [ ] , this . obj , undefined ) ; } return this . watchObj ( this . obj , [ ] ) ; } public stop ( ) { this . stopFns . removeAll ( ) ; } } export default function watchDeep ( obj , callback , options ? ) { const watcher = new Watcher ( obj , callback , options ) ; watcher . watch ( ) ; return ( ) => watcher . stop ( ) ; }