From a1e72006d73be7fb0a4eca7a754114de935f3c28 Mon Sep 17 00:00:00 2001 From: janrywang Date: Fri, 18 Jun 2021 13:55:07 +0800 Subject: [PATCH] refactor(reactive): fix #1598 and support #1586 and super performance optimization --- packages/reactive/docs/api/reaction.md | 1 + .../reactive/src/__tests__/autorun.spec.ts | 25 ++++++ .../reactive/src/__tests__/externals.spec.ts | 2 +- .../reactive/src/__tests__/observe.spec.ts | 80 +++++++++++-------- packages/reactive/src/autorun.ts | 5 +- packages/reactive/src/checkers.ts | 3 + packages/reactive/src/datatree.ts | 77 +++++++++++++----- packages/reactive/src/environment.ts | 6 +- packages/reactive/src/externals.ts | 1 - packages/reactive/src/handlers.ts | 30 ++++--- packages/reactive/src/internals.ts | 52 ++++++++---- packages/reactive/src/observe.ts | 50 +++++------- packages/reactive/src/reaction.ts | 76 +++++------------- packages/reactive/src/types.ts | 8 +- 14 files changed, 234 insertions(+), 182 deletions(-) diff --git a/packages/reactive/docs/api/reaction.md b/packages/reactive/docs/api/reaction.md index 07147605515..17b9b9724ea 100644 --- a/packages/reactive/docs/api/reaction.md +++ b/packages/reactive/docs/api/reaction.md @@ -10,6 +10,7 @@ interface IReactionOptions { name?: string equals?: (oldValue: T, newValue: T) => boolean //脏检查 + fireImmediately?: boolean //是否第一次默认触发,绕过脏检查 } interface reaction { diff --git a/packages/reactive/src/__tests__/autorun.spec.ts b/packages/reactive/src/__tests__/autorun.spec.ts index 6f5d3a3a071..bb9b9d232cc 100644 --- a/packages/reactive/src/__tests__/autorun.spec.ts +++ b/packages/reactive/src/__tests__/autorun.spec.ts @@ -40,6 +40,31 @@ test('reaction', () => { expect(handler).toBeCalledTimes(1) }) +test('reaction fireImmediately', () => { + const obs = observable({ + aa: { + bb: 123, + }, + }) + const handler = jest.fn() + const dispose = reaction( + () => { + return obs.aa.bb + }, + handler, + { + fireImmediately: true, + } + ) + obs.aa.bb = 123 + expect(handler).toBeCalledTimes(1) + obs.aa.bb = 111 + expect(handler).toBeCalledTimes(2) + dispose() + obs.aa.bb = 222 + expect(handler).toBeCalledTimes(2) +}) + test('reaction dirty check', () => { const obs: any = { aa: 123, diff --git a/packages/reactive/src/__tests__/externals.spec.ts b/packages/reactive/src/__tests__/externals.spec.ts index 628261541a0..4321455f6bc 100644 --- a/packages/reactive/src/__tests__/externals.spec.ts +++ b/packages/reactive/src/__tests__/externals.spec.ts @@ -9,7 +9,7 @@ import { test('is support observable', () => { const obs = observable({ aa: 111 }) - expect(isSupportObservable(obs)).toBeFalsy() + expect(isSupportObservable(obs)).toBeTruthy() expect(isSupportObservable(null)).toBeFalsy() expect(isSupportObservable([])).toBeTruthy() expect(isSupportObservable({})).toBeTruthy() diff --git a/packages/reactive/src/__tests__/observe.spec.ts b/packages/reactive/src/__tests__/observe.spec.ts index d17aa63802a..8a86ceb924b 100644 --- a/packages/reactive/src/__tests__/observe.spec.ts +++ b/packages/reactive/src/__tests__/observe.spec.ts @@ -1,13 +1,5 @@ import { observable, observe } from '../' -import { ProxyRaw, RawNode } from '../environment' - -const getObservers = (target: any) => { - return RawNode.get(ProxyRaw.get(target))?.observers -} - -const getDeepObservers = (target: any) => { - return RawNode.get(ProxyRaw.get(target))?.deepObservers -} +//import { ProxyRaw, RawNode } from '../environment' test('deep observe', () => { const obs = observable({ @@ -45,30 +37,30 @@ test('shallow observe', () => { expect(handler).toHaveBeenCalledTimes(2) }) -test('auto dispose observe', () => { - const obs = observable({ - aa: { - bb: { - cc: [11, 22, 33], - }, - }, - }) - const handler = jest.fn() - observe(obs, handler) - observe(obs.aa, handler) - observe(obs.aa.bb, handler) - expect(getDeepObservers(obs.aa).length).toEqual(1) - expect(getDeepObservers(obs.aa.bb).length).toEqual(1) - obs.aa.bb = { kk: 'mm' } - expect(getDeepObservers(obs.aa.bb).length).toEqual(1) - expect(getDeepObservers(obs.aa).length).toEqual(1) - expect(getObservers(obs.aa).length).toEqual(0) - expect(handler).toBeCalledTimes(3) - observe(obs.aa, handler) - expect(getObservers(obs.aa).length).toEqual(0) - expect(getDeepObservers(obs.aa).length).toEqual(2) - delete obs.aa -}) +// test('auto dispose observe', () => { +// const obs = observable({ +// aa: { +// bb: { +// cc: [11, 22, 33], +// }, +// }, +// }) +// const handler = jest.fn() +// observe(obs, handler) +// observe(obs.aa, handler) +// observe(obs.aa.bb, handler) +// expect(getDeepObservers(obs.aa).length).toEqual(1) +// expect(getDeepObservers(obs.aa.bb).length).toEqual(1) +// obs.aa.bb = { kk: 'mm' } +// expect(getDeepObservers(obs.aa.bb).length).toEqual(1) +// expect(getDeepObservers(obs.aa).length).toEqual(1) +// expect(getObservers(obs.aa).length).toEqual(0) +// expect(handler).toBeCalledTimes(3) +// observe(obs.aa, handler) +// expect(getObservers(obs.aa).length).toEqual(0) +// expect(getDeepObservers(obs.aa).length).toEqual(2) +// delete obs.aa +// }) test('root replace observe', () => { const obs = observable({ @@ -138,3 +130,25 @@ test('dispose observe', () => { obs.aa = { mm: 444 } expect(handler).toBeCalledTimes(4) }) + +test('array delete', () => { + const array = observable([{ value: 1 }, { value: 2 }]) + + const fn = jest.fn() + + const dispose = observe(array, (change) => { + if (change.type === 'set' && change.key === 'value') { + fn(change.path?.join('.')) + } + }) + + array[0].value = 3 + expect(fn.mock.calls[0][0]).toBe('0.value') + + array.splice(0, 1) + + array[0].value = 3 + expect(fn.mock.calls[1][0]).toBe('0.value') + + dispose() +}) diff --git a/packages/reactive/src/autorun.ts b/packages/reactive/src/autorun.ts index 6bce7ef6ea8..615e130d198 100644 --- a/packages/reactive/src/autorun.ts +++ b/packages/reactive/src/autorun.ts @@ -64,7 +64,10 @@ export const reaction = ( return autorun(() => { value.currentValue = tracker() dirty.current = dirtyCheck() - if (dirty.current && tracked.current) { + if ( + (dirty.current && tracked.current) || + (!tracked.current && realOptions.fireImmediately) + ) { untracked(() => { if (isFn(subscriber)) subscriber(value.currentValue) }) diff --git a/packages/reactive/src/checkers.ts b/packages/reactive/src/checkers.ts index 048f59665ab..f50442b57d1 100644 --- a/packages/reactive/src/checkers.ts +++ b/packages/reactive/src/checkers.ts @@ -15,3 +15,6 @@ export const isCollectionType = (target: any) => { isMap(target) || isWeakMap(target) || isSet(target) || isWeakSet(target) ) } +export const isNormalType = (target: any) => { + return isPlainObj(target) || isArr(target) +} diff --git a/packages/reactive/src/datatree.ts b/packages/reactive/src/datatree.ts index 582d094fa39..6591adec72d 100644 --- a/packages/reactive/src/datatree.ts +++ b/packages/reactive/src/datatree.ts @@ -1,27 +1,62 @@ import { ProxyRaw, RawNode } from './environment' -import { PropertyKey } from './types' +import { ObservablePath, PropertyKey, IOperation } from './types' import { concat } from './concat' +export class DataChange { + path: ObservablePath + key: PropertyKey + type: string + value: any + oldValue: any + constructor(operation: IOperation, node: DataNode) { + this.key = operation.key + this.type = operation.type + this.value = operation.value + this.oldValue = operation.oldValue + this.path = concat(node.path, operation.key) + } +} +export class DataNode { + target: any + + key: PropertyKey + + constructor(target: any, key: PropertyKey) { + this.target = target + this.key = key + } + + get path() { + if (!this.parent) return this.key ? [this.key] : [] + return concat(this.parent.path, this.key) + } + + get targetRaw() { + return ProxyRaw.get(this.target) || this.target + } + + get parent() { + if (!this.target) return + return RawNode.get(this.targetRaw) + } + + isEqual(node: DataNode) { + return node.targetRaw === this.targetRaw && node.key === this.key + } + + contains(node: DataNode) { + if (node === this) return true + let parent = node.parent + while (!!parent) { + if (this.isEqual(parent)) return true + parent = parent.parent + } + return false + } +} + export const buildDataTree = (target: any, key: PropertyKey, value: any) => { - const raw = ProxyRaw.get(value) || value - const currentNode = RawNode.get(raw) + const currentNode = RawNode.get(ProxyRaw.get(value) || value) if (currentNode) return currentNode - const parentRaw = ProxyRaw.get(target) || target - const parentNode = RawNode.get(parentRaw) - if (parentNode) { - RawNode.set(value, { - get path() { - return concat(parentNode.path, key) - }, - parent: parentNode, - observers: [], - deepObservers: [], - }) - } else { - RawNode.set(value, { - path: [], - observers: [], - deepObservers: [], - }) - } + RawNode.set(value, new DataNode(target, key)) } diff --git a/packages/reactive/src/environment.ts b/packages/reactive/src/environment.ts index f578d70fee2..6566e63ae5e 100644 --- a/packages/reactive/src/environment.ts +++ b/packages/reactive/src/environment.ts @@ -1,9 +1,10 @@ -import { IRawNode, Reaction, ReactionsMap } from './types' +import { ObservableListener, Reaction, ReactionsMap } from './types' +import { DataNode } from './datatree' export const ProxyRaw = new WeakMap() export const RawProxy = new WeakMap() export const RawShallowProxy = new WeakMap() -export const RawNode = new WeakMap() +export const RawNode = new WeakMap() export const RawReactionsMap = new WeakMap() export const ReactionStack: Reaction[] = [] @@ -13,3 +14,4 @@ export const BatchScope = { value: false } export const PendingReactions = new Set() export const PendingScopeReactions = new Set() export const MakeObservableSymbol = Symbol('MakeObservableSymbol') +export const ObserverListeners = new Set() diff --git a/packages/reactive/src/externals.ts b/packages/reactive/src/externals.ts index 7f1ddd4bae5..f723aad2c20 100644 --- a/packages/reactive/src/externals.ts +++ b/packages/reactive/src/externals.ts @@ -24,7 +24,6 @@ export const isAnnotation = (target: any): target is Annotation => { } export const isSupportObservable = (target: any) => { - if (isObservable(target)) return false if (!isValid(target)) return false if (isArr(target)) return true if (isPlainObj(target)) { diff --git a/packages/reactive/src/handlers.ts b/packages/reactive/src/handlers.ts index 276087ab656..0840281f727 100644 --- a/packages/reactive/src/handlers.ts +++ b/packages/reactive/src/handlers.ts @@ -3,7 +3,7 @@ import { runReactionsFromTargetKey, } from './reaction' import { ProxyRaw, RawProxy } from './environment' -import { isSupportObservable } from './externals' +import { isObservable, isSupportObservable } from './externals' import { createObservable } from './internals' const wellKnownSymbols = new Set( @@ -16,13 +16,13 @@ const hasOwnProperty = Object.prototype.hasOwnProperty function findObservable(target: any, key: PropertyKey, value: any) { const observableObj = RawProxy.get(value) - if (isSupportObservable(value)) { - if (observableObj) { - return observableObj - } + if (observableObj) { + return observableObj + } + if (!isObservable(value) && isSupportObservable(value)) { return createObservable(target, key, value) } - return observableObj || value + return value } function patchIterator( @@ -171,10 +171,10 @@ export const baseHandlers: ProxyHandler = { } bindTargetKeyWithCurrentReaction({ target, key, receiver, type: 'get' }) const observableResult = RawProxy.get(result) - if (isSupportObservable(result)) { - if (observableResult) { - return observableResult - } + if (observableResult) { + return observableResult + } + if (!isObservable(result) && isSupportObservable(result)) { const descriptor = Reflect.getOwnPropertyDescriptor(target, key) if ( !descriptor || @@ -183,7 +183,7 @@ export const baseHandlers: ProxyHandler = { return createObservable(target, key, result) } } - return observableResult || result + return result }, has(target, key) { const result = Reflect.has(target, key) @@ -198,15 +198,13 @@ export const baseHandlers: ProxyHandler = { const hadKey = hasOwnProperty.call(target, key) const newValue = createObservable(target, key, value) const oldValue = target[key] - const result = Reflect.set(target, key, newValue, receiver) - if (target !== ProxyRaw.get(receiver)) { - return result - } + target[key] = newValue if (!hadKey) { runReactionsFromTargetKey({ target, key, value: newValue, + oldValue, receiver, type: 'add', }) @@ -220,7 +218,7 @@ export const baseHandlers: ProxyHandler = { type: 'set', }) } - return result + return true }, deleteProperty(target, key) { const res = Reflect.deleteProperty(target, key) diff --git a/packages/reactive/src/internals.ts b/packages/reactive/src/internals.ts index 0953695b66f..030a0d55b01 100644 --- a/packages/reactive/src/internals.ts +++ b/packages/reactive/src/internals.ts @@ -1,23 +1,18 @@ -import { isFn, isCollectionType } from './checkers' +import { isFn, isCollectionType, isNormalType } from './checkers' import { RawProxy, ProxyRaw, MakeObservableSymbol, RawShallowProxy, + RawNode, } from './environment' import { baseHandlers, collectionHandlers } from './handlers' import { buildDataTree } from './datatree' -import { isObservable, isSupportObservable } from './externals' +import { isSupportObservable } from './externals' import { PropertyKey, IVisitor } from './types' -export const createProxy = ( - target: T, - shallow?: boolean -): T => { - const proxy = new Proxy( - target, - isCollectionType(target) ? collectionHandlers : baseHandlers - ) +const createNormalProxy = (target: any, shallow?: boolean) => { + const proxy = new Proxy(target, baseHandlers) ProxyRaw.set(proxy, target) if (shallow) { RawShallowProxy.set(target, proxy) @@ -27,22 +22,49 @@ export const createProxy = ( return proxy } +const createCollectionProxy = (target: any, shallow?: boolean) => { + const proxy = new Proxy(target, collectionHandlers) + ProxyRaw.set(proxy, target) + if (shallow) { + RawShallowProxy.set(target, proxy) + } else { + RawProxy.set(target, proxy) + } + return proxy +} + +const createShallowProxy = (target: any) => { + if (isNormalType(target)) return createNormalProxy(target, true) + if (isCollectionType(target)) return createCollectionProxy(target, true) + return target +} + export const createObservable = ( target: any, key?: PropertyKey, value?: any, shallow?: boolean ) => { + if (typeof value !== 'object') return value + const raw = ProxyRaw.get(value) + if (!!raw) { + const node = RawNode.get(raw) + node.key = key + return value + } + + if (!isSupportObservable(value)) return value + if (target) { const parentRaw = ProxyRaw.get(target) || target const isShallowParent = RawShallowProxy.get(parentRaw) if (isShallowParent) return value } - if (isObservable(value)) return value - if (isSupportObservable(value)) { - buildDataTree(target, key, value) - return createProxy(value, shallow) - } + + buildDataTree(target, key, value) + if (shallow) return createShallowProxy(value) + if (isNormalType(value)) return createNormalProxy(value) + if (isCollectionType(value)) return createCollectionProxy(value) return value } diff --git a/packages/reactive/src/observe.ts b/packages/reactive/src/observe.ts index bc43292a571..bbe38ea39ca 100644 --- a/packages/reactive/src/observe.ts +++ b/packages/reactive/src/observe.ts @@ -1,45 +1,39 @@ -import { IChange } from './types' -import { RawNode, ProxyRaw } from './environment' +import { IOperation } from './types' +import { RawNode, ProxyRaw, ObserverListeners } from './environment' import { isFn } from './checkers' - -interface IListener { - (change: IChange): void - unobserve?(): void -} +import { DataChange } from './datatree' export const observe = ( target: object, - observer?: (change: IChange) => void, + observer?: (change: DataChange) => void, deep = true ) => { - const listener: IListener = (change: IChange) => { - if (isFn(observer)) { - observer(change) - } - } - const addListener = (target: any) => { const raw = ProxyRaw.get(target) || target const node = RawNode.get(raw) - if (node) { + + const listener = (operation: IOperation) => { + const targetRaw = ProxyRaw.get(operation.target) || operation.target + const targetNode = RawNode.get(targetRaw) if (deep) { - const id = node.deepObservers.length - node.deepObservers.push(listener) - listener.unobserve = () => { - node.deepObservers.splice(id, 1) - } - } else { - const id = node.observers.length - node.observers.push(listener) - listener.unobserve = () => { - node.observers.splice(id, 1) + if (node.contains(targetNode)) { + observer(new DataChange(operation, targetNode)) + return } } + if ( + node === targetNode || + (node.targetRaw === targetRaw && node.key === operation.key) + ) { + observer(new DataChange(operation, targetNode)) + } + } + + if (node && isFn(observer)) { + ObserverListeners.add(listener) } return () => { - if (listener.unobserve) { - listener.unobserve() - } + ObserverListeners.delete(listener) } } if (target && typeof target !== 'object') diff --git a/packages/reactive/src/reaction.ts b/packages/reactive/src/reaction.ts index 2a6013fed03..0f1f50d2514 100644 --- a/packages/reactive/src/reaction.ts +++ b/packages/reactive/src/reaction.ts @@ -7,11 +7,9 @@ import { PendingReactions, BatchCount, UntrackCount, - ProxyRaw, - RawNode, BatchScope, + ObserverListeners, } from './environment' -import { concat } from './concat' const ITERATION_KEY = Symbol('iteration key') @@ -38,7 +36,7 @@ const addRawReactionsMap = ( } } -const addReactionsMaptoReaction = ( +const addReactionsMapToReaction = ( reaction: Reaction, reactionsMap: ReactionsMap ) => { @@ -55,20 +53,24 @@ const addReactionsMaptoReaction = ( const getReactionsFromTargetKey = (target: any, key: PropertyKey) => { const reactionsMap = RawReactionsMap.get(target) - const reactions = new Set() + const reactions = [] if (reactionsMap) { - reactionsMap.get(key)?.forEach((reaction) => { - if (!reactions.has(reaction)) { - reactions.add(reaction) - } - }) + const map = reactionsMap.get(key) + if (map) { + map.forEach((reaction) => { + if (reactions.indexOf(reaction) === -1) { + reactions.push(reaction) + } + }) + } } return reactions } const runReactions = (target: any, key: PropertyKey) => { const reactions = getReactionsFromTargetKey(target, key) - reactions.forEach((reaction) => { + for (let i = 0, len = reactions.length; i < len; i++) { + const reaction = reactions[i] if (reaction._isComputed) { reaction._scheduler(reaction) } else if (isScopeBatching()) { @@ -86,51 +88,11 @@ const runReactions = (target: any, key: PropertyKey) => { reaction() } } - }) + } } const notifyObservers = (operation: IOperation) => { - const targetNode = RawNode.get( - ProxyRaw.get(operation.target) || operation.target - ) - const oldValueNode = RawNode.get( - ProxyRaw.get(operation.oldValue) || operation.oldValue - ) - const newValueNode = RawNode.get( - ProxyRaw.get(operation.value) || operation.value - ) - if (targetNode) { - const getChange = () => { - return { - get path() { - return concat(targetNode.path, operation.key) - }, - type: operation.type, - key: operation.key, - value: operation.value, - oldValue: operation.oldValue, - } - } - if (oldValueNode && operation.type === 'set') { - oldValueNode.observers.forEach((fn) => fn(getChange())) - oldValueNode.deepObservers.forEach((fn) => fn(getChange())) - if (newValueNode) { - newValueNode.observers = oldValueNode.observers - newValueNode.deepObservers = oldValueNode.deepObservers - } - } - if (oldValueNode && operation.type === 'delete') { - oldValueNode.observers = [] - oldValueNode.deepObservers = [] - } - targetNode.observers.forEach((fn) => fn(getChange())) - targetNode.deepObservers.forEach((fn) => fn(getChange())) - let parent = targetNode.parent - while (!!parent) { - parent.deepObservers.forEach((fn) => fn(getChange())) - parent = parent.parent - } - } + ObserverListeners.forEach((fn) => fn(operation)) } export const bindTargetKeyWithCurrentReaction = (operation: IOperation) => { @@ -142,7 +104,7 @@ export const bindTargetKeyWithCurrentReaction = (operation: IOperation) => { const current = ReactionStack[ReactionStack.length - 1] if (isUntracking()) return if (current) { - addReactionsMaptoReaction(current, addRawReactionsMap(target, key, current)) + addReactionsMapToReaction(current, addRawReactionsMap(target, key, current)) } } @@ -170,7 +132,7 @@ export const suspendComputedReactions = (reaction: Reaction) => { reaction._context, reaction._property ) - if (reactions.size === 0) { + if (reactions.length === 0) { disposeBindingReactions(reaction) reaction._dirty = true } @@ -189,8 +151,8 @@ export const runReactionsFromTargetKey = (operation: IOperation) => { runReactions(target, key) } if (type === 'add' || type === 'delete' || type === 'clear') { - key = Array.isArray(target) ? 'length' : ITERATION_KEY - runReactions(target, key) + const newKey = Array.isArray(target) ? 'length' : ITERATION_KEY + runReactions(target, newKey) } } diff --git a/packages/reactive/src/types.ts b/packages/reactive/src/types.ts index f8dc6738fbd..6b0a8f823a8 100644 --- a/packages/reactive/src/types.ts +++ b/packages/reactive/src/types.ts @@ -26,13 +26,6 @@ export interface IChange { type?: OperationType } -export interface IRawNode { - path?: ObservablePath - parent?: IRawNode - observers?: ObservableListener[] - deepObservers?: ObservableListener[] -} - export interface IVisitor { target?: Target key?: PropertyKey @@ -65,4 +58,5 @@ export type ReactionsMap = Map> export interface IReactionOptions { name?: string equals?: (oldValue: T, newValue: T) => boolean + fireImmediately?: boolean }