import { isObject , silent , deepVisit , DeepVisitBreak } from '@goldfishjs/utils' ; import { getCurrent , Dep , ChangeOptions } from './dep' ; import { genId , isArray } from './utils' ; import silentValue , { isSilentValue } from './silentValue' ; import { isRaw } from './raw' ; type ObservableBaseTypes = null | undefined | string | number | boolean ; type ObservableArrayElement = ObservableBaseTypes | IObservableObject ; export type ObservableArray = ObservableArrayElement [ ] ; export interface IObservableObject { [ key ] : IObservableObject | ObservableArray | ObservableBaseTypes ; } const OBSERVE_FLAG = { } ; const OBSERVE_KEY = '__reactive-ob__' ; const NOTIFY_KEY = '__notify' ; export function isObservable ( obj ) { return obj && Object . prototype . hasOwnProperty . call ( obj , OBSERVE_KEY ) && obj [ OBSERVE_KEY ] === OBSERVE_FLAG ; } export function definePropertySilently ( ... args ) { silent ( ( ) => { Object . defineProperty ( ... args ) ; } ) ( ) ; } export function markObservable ( obj ) { if ( isObject ( obj ) && obj [ OBSERVE_KEY ] !== OBSERVE_FLAG ) { definePropertySilently ( obj , OBSERVE_KEY , { value : OBSERVE_FLAG , configurable : false , enumerable : false , writable : false , } ) ; } } export type Methods = 'push' | 'splice' | 'unshift' | 'pop' | 'sort' | 'reverse' | 'shift' ; const methods = [ 'push' , 'splice' , 'unshift' , 'pop' , 'sort' , 'reverse' , 'shift' ] ; methods . forEach ( methodName => { const oldMethod = Array . prototype [ methodName ] as Function ; Array . prototype [ methodName ] = function ( this , ... args ) { const originLength = this . length ; const oldV = this . slice ( 0 ) ; const result = oldMethod . apply ( this , args ) ; if ( isObservable ( this ) ) { if ( originLength < this . length ) { for ( let i = originLength ; i < this . length ; i += 1 ) { defineProperty ( this , i ) ; isObject ( this [ i ] ) && createObserver ( this [ i ] ) ; } } callNotifyFns ( this , this , this , { args , result , oldV , isArray : true , method : methodName , } ) ; } return result ; } ; } ) ; function defineNotify ( value , dep , notifyId ) { if ( isObject ( value ) ) { if ( ! ( value as any ) [ NOTIFY_KEY ] ) { definePropertySilently ( value , NOTIFY_KEY , { configurable : true , enumerable : false , writable : false , value : { } , } ) ; } const notifyFns = ( value as any ) [ NOTIFY_KEY ] ; if ( notifyFns ) { const notifyFn = ( n , o , options ? ) => { dep . notifyChange ( n , o , { type : 'notify' , ... ( options || { } ) , } ) ; } ; notifyFns [ notifyId ] = notifyFn ; } } } function removeNotify ( value , notifyId ) { if ( isObject ( value ) ) { const notifyFns = ( value as any ) [ NOTIFY_KEY ] ; if ( notifyFns ) { notifyFns [ notifyId ] = null ; } } } function callNotifyFns ( value , ... args ) { if ( isObject ( value ) ) { const notifyFns = ( value as any ) [ NOTIFY_KEY ] ; if ( notifyFns ) { for ( const key in notifyFns ) { if ( notifyFns [ key ] ) { notifyFns [ key ] ( ... args ) ; } } } } } function defineProperty ( obj , key ) ; function defineProperty ( obj , key ) ; function defineProperty ( obj , key ) { const desc = Object . getOwnPropertyDescriptor ( obj , key ) ; if ( desc && ( ! desc . configurable || desc . get ) ) { return ; } let value = obj [ key ] ; const dep = new Dep ( obj , key ) ; const notifyId = genId ( `notify-${ key }-` ) ; defineNotify ( value , dep , notifyId ) ; definePropertySilently ( obj , key , { configurable : typeof key === 'number' , enumerable : true , get ( ) { const currentDepList = getCurrent ( ) ; if ( currentDepList ) { currentDepList . add ( dep ) ; } return isSilentValue ( value ) ? value . value : value ; } , set ( v ) { if ( isObject ( v ) ) { createObserver ( v ) ; } const newValue = v ; const oldValue = value ; value = v ; if ( oldValue !== value && isObject ( oldValue ) ) { removeNotify ( oldValue , notifyId ) ; } defineNotify ( value , dep , notifyId ) ; if ( ! isSilentValue ( v ) ) { dep . notifyChange ( newValue , oldValue ) ; } } , } ) ; } function createObserver ( obj ) { if ( isObservable ( obj ) || isRaw ( obj ) ) { return ; } const visitKeyPathDeepRecords = [ ] ; deepVisit ( obj , ( value , key , po , keyPathList ) => { if ( ! visitKeyPathDeepRecords [ keyPathList . length ] ) { visitKeyPathDeepRecords [ keyPathList . length ] = true ; if ( isObservable ( po ) || isRaw ( po ) ) { return DeepVisitBreak . CHILDREN ; } markObservable ( po ) ; } defineProperty ( po as any , key as any ) ; if ( ( isArray ( value ) && value . length === 0 ) || ( isObject ( value ) && Object . keys ( value ) . length === 0 ) ) { markObservable ( value ) ; } return DeepVisitBreak . NO ; } ) ; if ( ! visitKeyPathDeepRecords [ 0 ] ) { markObservable ( obj ) ; } } export function set ( obj , name , value , options ? ) ; export function set ( obj , name , value , options ? ) ; export function set ( obj , name , value , options ? ) { if ( ! isObservable ( obj ) ) { obj [ name ] = value ; return ; } const silent = ( options && options . silent ) || false ; if ( ! Object . prototype . hasOwnProperty . call ( obj , name ) ) { defineProperty ( obj , name ) ; isObject ( obj [ name ] ) && createObserver ( obj [ name ] ) ; obj [ name ] = silent ? silentValue ( value ) : value ; callNotifyFns ( obj , obj , obj ) ; } else { obj [ name ] = silent ? silentValue ( value ) : value ; } } export default function observable < T extends IObservableObject > ( obj ) { createObserver ( obj ) ; return obj ; }