Skip to content

Commit

Permalink
fix(engine): issue #858 to enable the ability to have setters reactive (
Browse files Browse the repository at this point in the history
#1038)

fix(engine): implementing reactive mechanism extracted into its own file/abstraction

fix(reactive-service): adding units

fix(engine): perf optimizations on VM declaration
  • Loading branch information
caridy authored Jun 28, 2019
1 parent cdc4558 commit c270594
Show file tree
Hide file tree
Showing 18 changed files with 498 additions and 133 deletions.
2 changes: 1 addition & 1 deletion packages/@lwc/engine/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ module.exports = {
global: {
functions: 65,
lines: 75,
branches: 70,
branches: 65,
},
},
};
8 changes: 5 additions & 3 deletions packages/@lwc/engine/src/framework/base-lightning-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@ import {
ComponentInterface,
getWrappedComponentsListener,
getComponentAsString,
getTemplateReactiveObserver,
} from './component';
import { setInternalField, setHiddenField } from '../shared/fields';
import { ViewModelReflection, EmptyObject } from './utils';
import { vmBeingConstructed, isBeingConstructed, isRendering, vmBeingRendered } from './invoker';
import { getComponentVM, VM } from './vm';
import { observeMutation, notifyMutation } from './watcher';
import { valueObserved, valueMutated } from '../libs/mutation-tracker';
import { dispatchEvent } from '../env/dom';
import { patchComponentWithRestrictions, patchShadowRootWithRestrictions } from './restrictions';
import { unlockAttribute, lockAttribute } from './attributes';
Expand Down Expand Up @@ -92,7 +93,7 @@ function createBridgeToElementDescriptor(
}
return;
}
observeMutation(this, propName);
valueObserved(this, propName);
return get.call(vm.elm);
},
set(this: ComponentInterface, newValue: any) {
Expand All @@ -119,7 +120,7 @@ function createBridgeToElementDescriptor(
vm.cmpProps[propName] = newValue;
if (isFalse(vm.isDirty)) {
// perf optimization to skip this step if not in the DOM
notifyMutation(this, propName);
valueMutated(this, propName);
}
}
return set.call(vm.elm, newValue);
Expand Down Expand Up @@ -165,6 +166,7 @@ export function BaseLightningElement(this: ComponentInterface) {
} = vm;
const component = this;
vm.component = component;
vm.tro = getTemplateReactiveObserver(vm as VM);
// interaction hooks
// We are intentionally hiding this argument from the formal API of LWCElement because
// we don't want folks to know about it just yet.
Expand Down
46 changes: 17 additions & 29 deletions packages/@lwc/engine/src/framework/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,13 @@ import {
vmBeingRendered,
invokeEventListener,
} from './invoker';
import {
isArray,
ArrayIndexOf,
ArraySplice,
isFunction,
isUndefined,
StringToLowerCase,
} from '../shared/language';
import { isArray, isFunction, isUndefined, StringToLowerCase, isFalse } from '../shared/language';
import { invokeServiceHook, Services } from './services';
import { VM, getComponentVM, UninitializedVM } from './vm';
import { VM, getComponentVM, UninitializedVM, scheduleRehydration } from './vm';
import { VNodes } from '../3rdparty/snabbdom/types';
import { tagNameGetter } from '../env/element';
import { Template } from './template';
import { ReactiveObserver } from '../libs/mutation-tracker';

export type ErrorCallback = (error: any, stack: string) => void;
export interface ComponentInterface {
Expand Down Expand Up @@ -99,26 +93,20 @@ export function linkComponent(vm: VM) {
}
}

export function clearReactiveListeners(vm: VM) {
if (process.env.NODE_ENV !== 'production') {
assert.isTrue(vm && 'cmpRoot' in vm, `${vm} is not a vm.`);
}
const { deps } = vm;
const len = deps.length;
if (len > 0) {
for (let i = 0; i < len; i += 1) {
const set = deps[i];
const pos = ArrayIndexOf.call(deps[i], vm);
if (process.env.NODE_ENV !== 'production') {
assert.invariant(
pos > -1,
`when clearing up deps, the vm must be part of the collection.`
);
}
ArraySplice.call(set, pos, 1);
export function getTemplateReactiveObserver(vm: VM): ReactiveObserver {
return new ReactiveObserver(() => {
if (process.env.NODE_ENV !== 'production') {
assert.invariant(
!isRendering,
`Mutating property is not allowed during the rendering life-cycle of ${vmBeingRendered}.`
);
}
deps.length = 0;
}
const { isDirty } = vm;
if (isFalse(isDirty)) {
markComponentAsDirty(vm);
scheduleRehydration(vm);
}
});
}

function clearChildLWC(vm: VM) {
Expand All @@ -134,7 +122,7 @@ export function renderComponent(vm: VM): VNodes {
assert.invariant(vm.isDirty, `${vm} is not dirty.`);
}

clearReactiveListeners(vm);
vm.tro.reset();
clearChildLWC(vm);
const vnodes = invokeComponentRenderMethod(vm);
vm.isDirty = false;
Expand Down
6 changes: 3 additions & 3 deletions packages/@lwc/engine/src/framework/decorators/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import assert from '../../shared/assert';
import { isRendering, vmBeingRendered, isBeingConstructed } from '../invoker';
import { isObject, toString, isFalse } from '../../shared/language';
import { observeMutation, notifyMutation } from '../watcher';
import { valueObserved, valueMutated } from '../../libs/mutation-tracker';
import { ComponentInterface, ComponentConstructor } from '../component';
import { getComponentVM } from '../vm';
import { isUndefined, isFunction } from '../../shared/language';
Expand Down Expand Up @@ -81,7 +81,7 @@ function createPublicPropertyDescriptor(
}
return;
}
observeMutation(this, key);
valueObserved(this, key);
return vm.cmpProps[key];
},
set(this: ComponentInterface, newValue: any) {
Expand All @@ -100,7 +100,7 @@ function createPublicPropertyDescriptor(
// avoid notification of observability if the instance is already dirty
if (isFalse(vm.isDirty)) {
// perf optimization to skip this step if the component is dirty already.
notifyMutation(this, key);
valueMutated(this, key);
}
},
enumerable: isUndefined(descriptor) ? true : descriptor.enumerable,
Expand Down
6 changes: 3 additions & 3 deletions packages/@lwc/engine/src/framework/decorators/track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import assert from '../../shared/assert';
import { isUndefined, isFalse } from '../../shared/language';
import { isRendering, vmBeingRendered } from '../invoker';
import { observeMutation, notifyMutation } from '../watcher';
import { valueObserved, valueMutated } from '../../libs/mutation-tracker';
import { getComponentVM } from '../vm';
import { reactiveMembrane } from '../membrane';
import { ComponentConstructor, ComponentInterface } from '../component';
Expand Down Expand Up @@ -66,7 +66,7 @@ export function createTrackedPropertyDescriptor(
if (process.env.NODE_ENV !== 'production') {
assert.isTrue(vm && 'cmpRoot' in vm, `${vm} is not a vm.`);
}
observeMutation(this, key);
valueObserved(this, key);
return vm.cmpTrack[key];
},
set(this: ComponentInterface, newValue: any) {
Expand All @@ -85,7 +85,7 @@ export function createTrackedPropertyDescriptor(
vm.cmpTrack[key] = reactiveOrAnyValue;
if (isFalse(vm.isDirty)) {
// perf optimization to skip this step if the track property is on a component that is already dirty
notifyMutation(this, key);
valueMutated(this, key);
}
}
},
Expand Down
6 changes: 4 additions & 2 deletions packages/@lwc/engine/src/framework/invoker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,10 @@ export function invokeComponentRenderMethod(vm: VM): VNodes {
},
() => {
// job
const html = callHook(component, render);
result = evaluateTemplate(vm, html);
vm.tro.observe(() => {
const html = callHook(component, render);
result = evaluateTemplate(vm, html);
});
},
() => {
establishContext(ctx);
Expand Down
6 changes: 3 additions & 3 deletions packages/@lwc/engine/src/framework/membrane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import ObservableMembrane from 'observable-membrane';
import { observeMutation, notifyMutation } from './watcher';
import { valueObserved, valueMutated } from '../libs/mutation-tracker';

function valueDistortion(value: any) {
return value;
}

export const reactiveMembrane = new ObservableMembrane({
valueObserved: observeMutation,
valueMutated: notifyMutation,
valueObserved,
valueMutated,
valueDistortion,
});

Expand Down
26 changes: 17 additions & 9 deletions packages/@lwc/engine/src/framework/vm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
createComponent,
linkComponent,
renderComponent,
clearReactiveListeners,
ComponentConstructor,
markComponentAsDirty,
} from './component';
Expand Down Expand Up @@ -56,6 +55,7 @@ import { tagNameGetter } from '../env/element';
import { parentElementGetter, parentNodeGetter } from '../env/node';
import { updateDynamicChildren, updateStaticChildren } from '../3rdparty/snabbdom/snabbdom';
import { hasDynamicChildren } from './hooks';
import { ReactiveObserver } from '../libs/mutation-tracker';

export interface SlotSet {
[key: string]: VNodes;
Expand Down Expand Up @@ -86,11 +86,9 @@ export interface UninitializedVM {
/** Adopted Children List */
aChildren: VNodes;
velements: VCustomElement[];
cmpTemplate?: Template;
cmpProps: any;
cmpSlots: SlotSet;
cmpTrack: any;
component?: ComponentInterface;
callHook: (
cmp: ComponentInterface | undefined,
fn: (...args: any[]) => any,
Expand All @@ -102,14 +100,21 @@ export interface UninitializedVM {
isDirty: boolean;
isRoot: boolean;
mode: 'open' | 'closed';
deps: VM[][];
toString(): string;

// perf optimization to avoid reshaping the uninitialized when initialized
cmpTemplate?: Template;
component?: ComponentInterface;
cmpRoot?: ShadowRoot;
tro?: ReactiveObserver;
}

export interface VM extends UninitializedVM {
cmpTemplate: Template;
component: ComponentInterface;
cmpRoot: ShadowRoot;
/** Template Reactive Observer to observe values used by the selected template */
tro: ReactiveObserver;
}

let idx: number = 0;
Expand Down Expand Up @@ -162,6 +167,9 @@ function resetComponentStateWhenRemoved(vm: VM) {
}
const { state } = vm;
if (state !== VMState.disconnected) {
const { tro } = vm;
// Making sure that any observing record will not trigger the rehydrated on this vm
tro.reset();
runDisconnectedCallback(vm);
// Spec: https://dom.spec.whatwg.org/#concept-node-remove (step 14-15)
runShadowChildNodesDisconnectedCallback(vm);
Expand Down Expand Up @@ -218,19 +226,20 @@ export function createVM(elm: HTMLElement, Ctor: ComponentConstructor, options:
elm,
data: EmptyObject,
context: create(null),
cmpTemplate: undefined,
cmpProps: create(null),
cmpTrack: create(null),
cmpSlots: useSyntheticShadow ? create(null) : undefined,
callHook,
setHook,
getHook,
component: undefined,
children: EmptyArray,
aChildren: EmptyArray,
velements: EmptyArray,
// used to track down all object-key pairs that makes this vm reactive
deps: [],
// Perf optimization to preserve the shape of this obj
cmpTemplate: undefined,
component: undefined,
cmpRoot: undefined,
tro: undefined,
};

if (process.env.NODE_ENV !== 'production') {
Expand Down Expand Up @@ -402,7 +411,6 @@ function runDisconnectedCallback(vm: VM) {
// of disconnected components.
markComponentAsDirty(vm);
}
clearReactiveListeners(vm);
vm.state = VMState.disconnected;
// reporting disconnection
const { disconnected } = Services;
Expand Down
80 changes: 0 additions & 80 deletions packages/@lwc/engine/src/framework/watcher.ts

This file was deleted.

Loading

0 comments on commit c270594

Please sign in to comment.