[IMP] store: connect function takes hash function in param

With this commit, we can now decides precisely if a component should be
rerendered, by using a hash function.

By default, the hash function will deep observe each store element
coming from the result of the mapStateToProps.

closes #24
This commit is contained in:
Géry Debongnie
2019-04-14 21:37:05 +02:00
parent f5bbf25d4f
commit 7b87470760
5 changed files with 85 additions and 29 deletions
+1 -1
View File
@@ -50,7 +50,7 @@
</label>
<button class="destroy" t-on-click="removeTodo"></button>
</div>
<input class="edit" t-ref="input" t-if="state.isEditing" t-att-value="props.title" t-on-keyup="handleKeyup" t-on-blur="handleBlur"/>
<input class="edit" t-ref="'input'" t-if="state.isEditing" t-att-value="props.title" t-on-keyup="handleKeyup" t-on-blur="handleBlur"/>
</li>
</templates>
-4
View File
@@ -319,10 +319,6 @@ export class Component<
nextProps: Props,
forceUpdate: boolean = false
): Promise<void> {
if (nextProps === this.__owl__.renderProps && !forceUpdate) {
await this.__owl__.renderPromise;
return;
}
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
return shouldUpdate ? this._updateProps(nextProps) : Promise.resolve();
}
+58 -23
View File
@@ -1,4 +1,5 @@
import { EventBus } from "./event_bus";
import { shallowEqual } from "./utils";
import { Component } from "./component";
//------------------------------------------------------------------------------
@@ -235,17 +236,23 @@ export function makeObserver(): Observer {
// Connect function
//------------------------------------------------------------------------------
function setStoreProps(__owl__: any, storeProps: any) {
__owl__.currentStoreProps = storeProps;
__owl__.currentStoreRevs = {};
__owl__.currentStoreRev = storeProps.__owl__ && storeProps.__owl__.rev;
for (let key in storeProps) {
__owl__.currentStoreRevs[key] =
storeProps[key].__owl__ && storeProps[key].__owl__.rev;
function revNumber<T extends Object>(o: T): number {
if (!("__owl__" in o)) {
return 0;
}
return (<any>o).__owl__.rev;
}
export function connect(mapStateToProps) {
function deepRevNumber<T extends Object>(o: T): number {
if (!("__owl__" in o)) {
return 0;
}
return (<any>o).__owl__.deepRev;
}
export function connect(mapStateToProps, options: any = {}) {
let hashFunction = options.hashFunction || null;
return function(Comp) {
return class extends Comp {
constructor(parent, props?: any) {
@@ -254,32 +261,60 @@ export function connect(mapStateToProps) {
const storeProps = mapStateToProps(env.store.state, ownProps);
const mergedProps = Object.assign({}, props || {}, storeProps);
super(parent, mergedProps);
setStoreProps(this.__owl__, storeProps);
this.__owl__.ownProps = ownProps;
this.__owl__.currentStoreProps = storeProps;
if (!hashFunction) {
if ("__owl__" in storeProps) {
hashFunction = s => deepRevNumber(s.storeProps);
} else {
let areKeyObservable = false;
for (let key in storeProps) {
areKeyObservable =
areKeyObservable || "__owl__" in storeProps[key];
}
if (areKeyObservable) {
hashFunction = function({ storeProps }) {
return Object.values(storeProps).reduce(
(sum: number, val: any) => sum + deepRevNumber(val),
0
);
};
}
}
}
if (hashFunction) {
this.__owl__.storeHash = hashFunction({
state: env.store.state,
storeProps: storeProps,
revNumber,
deepRevNumber
});
}
}
mounted() {
this.env.store.on("update", this, () => {
const ownProps = this.__owl__.ownProps;
const storeProps = mapStateToProps(this.env.store.state, ownProps);
let didChange = false;
if (
this.__owl__.currentStoreRev &&
this.__owl__.currentStoreRev !== storeProps.__owl__.rev
) {
setStoreProps(this.__owl__, storeProps);
if (hashFunction) {
const storeHash = hashFunction({
state: this.env.store.state,
storeProps: storeProps,
revNumber,
deepRevNumber
});
if (storeHash !== this.__owl__.storeHash) {
didChange = true;
this.__owl__.storeHash = storeHash;
}
} else {
const revs = this.__owl__.currentStoreRevs;
for (let key in storeProps) {
const val = storeProps[key];
if (val.__owl__ && val.__owl__.rev !== revs[key]) {
didChange = true;
revs[key] = val.__owl__ && val.__owl__.rev;
this.__owl__.currentStoreProps[key] = val;
}
}
didChange = !shallowEqual(
storeProps,
this.__owl__.currentStoreProps
);
}
if (didChange) {
this.__owl__.currentStoreProps = storeProps;
this.updateProps(ownProps, false);
}
});
+13
View File
@@ -113,6 +113,19 @@ export function findInTree<T extends Tree<T>>(
return null;
}
export function shallowEqual(objA, objB) {
if (objA === objB) {
return true;
}
const keysA = Object.keys(objA);
for (let key of keysA) {
if (!(key in objB) || objA[key] !== objB[key]) {
return false;
}
}
return true;
}
export function patch(C: any, patchName: string, patch: any) {
const proto = C.prototype;
if (!proto.__patches) {
+12
View File
@@ -5,6 +5,7 @@ import {
memoize,
debounce,
findInTree,
shallowEqual,
patch,
unpatch
} from "../src/utils";
@@ -94,6 +95,17 @@ describe("findInTree", () => {
});
});
describe("shallowEqual", () => {
test("simple comparisons", () => {
const obj1 = {};
expect(shallowEqual(obj1, obj1)).toBe(true);
expect(shallowEqual({}, {})).toBe(true);
expect(shallowEqual({ a: 1 }, {})).toBe(false);
expect(shallowEqual({ a: 1 }, { a: 1 })).toBe(true);
expect(shallowEqual({ a: 1 }, ["a"])).toBe(false);
});
});
describe("patch/unpatch", () => {
test("can monkey patch a class", () => {
class Test {