Switch from ko.computed to ko.pureComputed

This should provide performance and memory benefits over regular computed observables
This commit is contained in:
djmaze 2021-12-31 13:30:05 +01:00
parent ac2238a23f
commit 954de06f86
18 changed files with 55 additions and 37 deletions

View file

@ -1,5 +1,6 @@
import ko from 'ko'; import ko from 'ko';
import { isArray } from 'Common/Utils'; import { isArray } from 'Common/Utils';
import { koComputable } from 'External/ko';
/* /*
oCallbacks: oCallbacks:
@ -28,8 +29,8 @@ export class Selector {
sItemFocusedSelector sItemFocusedSelector
) { ) {
this.list = koList; this.list = koList;
this.listChecked = ko.computed(() => this.list.filter(item => item.checked())).extend({ rateLimit: 0 }); this.listChecked = koComputable(() => this.list.filter(item => item.checked())).extend({ rateLimit: 0 });
this.isListChecked = ko.computed(() => 0 < this.listChecked().length); this.isListChecked = koComputable(() => 0 < this.listChecked().length);
this.focusedItem = koFocusedItem || ko.observable(null); this.focusedItem = koFocusedItem || ko.observable(null);
this.selectedItem = koSelectedItem || ko.observable(null); this.selectedItem = koSelectedItem || ko.observable(null);

View file

@ -2,6 +2,7 @@ import ko from 'ko';
import { pInt } from 'Common/Utils'; import { pInt } from 'Common/Utils';
import { SaveSettingsStep } from 'Common/Enums'; import { SaveSettingsStep } from 'Common/Enums';
import { AbstractComponent } from 'Component/Abstract'; import { AbstractComponent } from 'Component/Abstract';
import { koComputable } from 'External/ko';
class AbstractInput extends AbstractComponent { class AbstractInput extends AbstractComponent {
/** /**
@ -18,7 +19,7 @@ class AbstractInput extends AbstractComponent {
this.labeled = null != params.label; this.labeled = null != params.label;
let size = params.size || 0; let size = 0 < params.size ? 'span' + params.size : '';
if (this.trigger) { if (this.trigger) {
const const
classForTrigger = ko.observable(''), classForTrigger = ko.observable(''),
@ -38,13 +39,13 @@ class AbstractInput extends AbstractComponent {
setTriggerState(this.trigger()); setTriggerState(this.trigger());
this.className = ko.computed(() => this.className = koComputable(() =>
((0 < size ? 'span' + size : '') + ' settings-saved-trigger-input ' + classForTrigger()).trim() (size + ' settings-saved-trigger-input ' + classForTrigger()).trim()
); );
this.disposable.push(this.trigger.subscribe(setTriggerState, this)); this.disposable.push(this.trigger.subscribe(setTriggerState, this));
} else { } else {
this.className = ko.computed(() => 0 < size ? 'span' + size : ''); this.className = size;
} }
this.disposable.push(this.className); this.disposable.push(this.className);

7
dev/External/ko.js vendored
View file

@ -3,6 +3,13 @@ import { doc, createElement } from 'Common/Globals';
import { SaveSettingsStep } from 'Common/Enums'; import { SaveSettingsStep } from 'Common/Enums';
import { arrayLength, isFunction } from 'Common/Utils'; import { arrayLength, isFunction } from 'Common/Utils';
/**
* The value of the pureComputed observable shouldnt vary based on the
* number of evaluations or other hidden information. Its value should be
* based solely on the values of other observables in the application
*/
export const koComputable = ko.pureComputed;
ko.bindingHandlers.tooltipErrorTip = { ko.bindingHandlers.tooltipErrorTip = {
init: (element, fValueAccessor) => { init: (element, fValueAccessor) => {
doc.addEventListener('click', () => { doc.addEventListener('click', () => {

View file

@ -1,5 +1,5 @@
import ko from 'ko'; import ko from 'ko';
import { koComputable } from 'External/ko';
import { doc, $htmlCL, elementById } from 'Common/Globals'; import { doc, $htmlCL, elementById } from 'Common/Globals';
import { isFunction, forEachObjectValue, forEachObjectEntry } from 'Common/Utils'; import { isFunction, forEachObjectValue, forEachObjectEntry } from 'Common/Utils';
@ -271,9 +271,9 @@ export const
fResult.isCommand = true; fResult.isCommand = true;
if (isFunction(fCanExecute)) { if (isFunction(fCanExecute)) {
fResult.canExecute = ko.computed(() => fResult && fResult.enabled() && fCanExecute.call(null)); fResult.canExecute = koComputable(() => fResult && fResult.enabled() && fCanExecute.call(null));
} else { } else {
fResult.canExecute = ko.computed(() => fResult && fResult.enabled() && !!fCanExecute); fResult.canExecute = koComputable(() => fResult && fResult.enabled() && !!fCanExecute);
} }
return fResult; return fResult;
@ -355,8 +355,8 @@ export const
fn.enabled = ko.observable(true); fn.enabled = ko.observable(true);
fn.canExecute = (typeof canExecute === 'function') fn.canExecute = (typeof canExecute === 'function')
? ko.computed(() => fn.enabled() && canExecute.call(thisArg, thisArg)) ? koComputable(() => fn.enabled() && canExecute.call(thisArg, thisArg))
: ko.computed(() => fn.enabled()); : koComputable(() => fn.enabled());
thisArg[key] = fn; thisArg[key] = fn;
}); });

View file

@ -1,4 +1,4 @@
import ko from 'ko'; import { koComputable } from 'External/ko';
import { AbstractModel } from 'Knoin/AbstractModel'; import { AbstractModel } from 'Knoin/AbstractModel';
@ -43,7 +43,7 @@ export class FilterConditionModel extends AbstractModel {
valueSecondError: false valueSecondError: false
}); });
this.template = ko.computed(() => { this.template = koComputable(() => {
const template = 'SettingsFiltersCondition'; const template = 'SettingsFiltersCondition';
switch (this.field()) { switch (this.field()) {
case FilterConditionField.Body: case FilterConditionField.Body:

View file

@ -20,6 +20,8 @@ import { i18n, trigger as translatorTrigger } from 'Common/Translator';
import { AbstractModel } from 'Knoin/AbstractModel'; import { AbstractModel } from 'Knoin/AbstractModel';
import { koComputable } from 'External/ko';
//import { mailBox } from 'Common/Links'; //import { mailBox } from 'Common/Links';
const const
@ -265,7 +267,7 @@ export class FolderModel extends AbstractModel {
type && 'mail' != type && folder.kolabType(type); type && 'mail' != type && folder.kolabType(type);
folder.messageCountAll = ko.computed({ folder.messageCountAll = koComputable({
read: folder.privateMessageCountAll, read: folder.privateMessageCountAll,
write: (iValue) => { write: (iValue) => {
if (isPosNumeric(iValue)) { if (isPosNumeric(iValue)) {
@ -277,7 +279,7 @@ export class FolderModel extends AbstractModel {
}) })
.extend({ notify: 'always' }); .extend({ notify: 'always' });
folder.messageCountUnread = ko.computed({ folder.messageCountUnread = koComputable({
read: folder.privateMessageCountUnread, read: folder.privateMessageCountUnread,
write: (value) => { write: (value) => {
if (isPosNumeric(value)) { if (isPosNumeric(value)) {

View file

@ -1,4 +1,4 @@
import ko from 'ko'; import { koComputable } from 'External/ko';
import { AbstractModel } from 'Knoin/AbstractModel'; import { AbstractModel } from 'Knoin/AbstractModel';
@ -24,7 +24,7 @@ export class IdentityModel extends AbstractModel {
deleteAccess: false deleteAccess: false
}); });
this.canBeDeleted = ko.computed(() => !!this.id()); this.canBeDeleted = koComputable(() => !!this.id());
} }
/** /**

View file

@ -1,4 +1,5 @@
import ko from 'ko'; import ko from 'ko';
import { koComputable } from 'External/ko';
import { Notification } from 'Common/Enums'; import { Notification } from 'Common/Enums';
import { FolderMetadataKeys } from 'Common/EnumsUser'; import { FolderMetadataKeys } from 'Common/EnumsUser';
@ -25,7 +26,7 @@ const folderForDeletion = ko.observable(null).deleteAccessHelper();
export class FoldersUserSettings /*extends AbstractViewSettings*/ { export class FoldersUserSettings /*extends AbstractViewSettings*/ {
constructor() { constructor() {
this.showKolab = ko.computed(() => FolderUserStore.hasCapability('METADATA') && Settings.capa(Capa.Kolab)); this.showKolab = koComputable(() => FolderUserStore.hasCapability('METADATA') && Settings.capa(Capa.Kolab));
this.defaultOptionsAfterRender = defaultOptionsAfterRender; this.defaultOptionsAfterRender = defaultOptionsAfterRender;
this.kolabTypeOptions = ko.observableArray(); this.kolabTypeOptions = ko.observableArray();
let i18nFilter = key => i18n('SETTINGS_FOLDERS/TYPE_' + key); let i18nFilter = key => i18n('SETTINGS_FOLDERS/TYPE_' + key);
@ -48,7 +49,7 @@ export class FoldersUserSettings /*extends AbstractViewSettings*/ {
this.folderListError = FolderUserStore.folderListError; this.folderListError = FolderUserStore.folderListError;
this.hideUnsubscribed = SettingsUserStore.hideUnsubscribed; this.hideUnsubscribed = SettingsUserStore.hideUnsubscribed;
this.loading = ko.computed(() => { this.loading = koComputable(() => {
const loading = FolderUserStore.foldersLoading(), const loading = FolderUserStore.foldersLoading(),
creating = FolderUserStore.foldersCreating(), creating = FolderUserStore.foldersCreating(),
deleting = FolderUserStore.foldersDeleting(), deleting = FolderUserStore.foldersDeleting(),

View file

@ -1,4 +1,5 @@
import ko from 'ko'; import ko from 'ko';
import { koComputable } from 'External/ko';
import { pInt, settingsSaveHelperSimpleFunction } from 'Common/Utils'; import { pInt, settingsSaveHelperSimpleFunction } from 'Common/Utils';
import { Capa, SaveSettingsStep } from 'Common/Enums'; import { Capa, SaveSettingsStep } from 'Common/Enums';
@ -17,7 +18,7 @@ export class SecurityUserSettings /*extends AbstractViewSettings*/ {
this.autoLogoutTrigger = ko.observable(SaveSettingsStep.Idle); this.autoLogoutTrigger = ko.observable(SaveSettingsStep.Idle);
let i18nLogout = (key, params) => i18n('SETTINGS_SECURITY/AUTOLOGIN_' + key, params); let i18nLogout = (key, params) => i18n('SETTINGS_SECURITY/AUTOLOGIN_' + key, params);
this.autoLogoutOptions = ko.computed(() => { this.autoLogoutOptions = koComputable(() => {
translatorTrigger(); translatorTrigger();
return [ return [
{ id: 0, name: i18nLogout('NEVER_OPTION_NAME') }, { id: 0, name: i18nLogout('NEVER_OPTION_NAME') },

View file

@ -1,4 +1,5 @@
import ko from 'ko'; import ko from 'ko';
import { koComputable } from 'External/ko';
import { FolderType, FolderSortMode } from 'Common/EnumsUser'; import { FolderType, FolderSortMode } from 'Common/EnumsUser';
import { UNUSED_OPTION_VALUE } from 'Common/Consts'; import { UNUSED_OPTION_VALUE } from 'Common/Consts';
@ -100,7 +101,7 @@ export const FolderUserStore = new class {
archiveFolder: fSetSystemFolderType(FolderType.Archive) archiveFolder: fSetSystemFolderType(FolderType.Archive)
}); });
self.quotaPercentage = ko.computed(() => { self.quotaPercentage = koComputable(() => {
const quota = self.quotaLimit(), usage = self.quotaUsage(); const quota = self.quotaLimit(), usage = self.quotaUsage();
return 0 < quota ? Math.ceil((usage / quota) * 100) : 0; return 0 < quota ? Math.ceil((usage / quota) * 100) : 0;
}); });

View file

@ -1,4 +1,5 @@
import ko from 'ko'; import ko from 'ko';
import { koComputable } from 'External/ko';
import { Layout, EditorDefaultType } from 'Common/EnumsUser'; import { Layout, EditorDefaultType } from 'Common/EnumsUser';
import { pInt, addObservablesTo } from 'Common/Utils'; import { pInt, addObservablesTo } from 'Common/Utils';
@ -39,7 +40,7 @@ export const SettingsUserStore = new class {
self.init(); self.init();
self.usePreviewPane = ko.computed(() => Layout.NoPreview !== self.layout() && !ThemeStore.isMobile()); self.usePreviewPane = koComputable(() => Layout.NoPreview !== self.layout() && !ThemeStore.isMobile());
const toggleLayout = () => { const toggleLayout = () => {
const value = ThemeStore.isMobile() ? Layout.NoPreview : self.layout(); const value = ThemeStore.isMobile() ? Layout.NoPreview : self.layout();

View file

@ -1,4 +1,4 @@
import ko from 'ko'; import { koComputable } from 'External/ko';
import { i18n, trigger as translatorTrigger } from 'Common/Translator'; import { i18n, trigger as translatorTrigger } from 'Common/Translator';
@ -25,10 +25,10 @@ class AdvancedSearchPopupView extends AbstractViewPopup {
unseen: false unseen: false
}); });
this.showMultisearch = ko.computed(() => FolderUserStore.hasCapability('MULTISEARCH')); this.showMultisearch = koComputable(() => FolderUserStore.hasCapability('MULTISEARCH'));
let prefix = 'SEARCH/LABEL_ADV_DATE_'; let prefix = 'SEARCH/LABEL_ADV_DATE_';
this.selectedDates = ko.computed(() => { this.selectedDates = koComputable(() => {
translatorTrigger(); translatorTrigger();
return [ return [
{ id: -1, name: i18n(prefix + 'ALL') }, { id: -1, name: i18n(prefix + 'ALL') },
@ -42,7 +42,7 @@ class AdvancedSearchPopupView extends AbstractViewPopup {
}); });
prefix = 'SEARCH/LABEL_ADV_SUBFOLDERS_'; prefix = 'SEARCH/LABEL_ADV_SUBFOLDERS_';
this.selectedTree = ko.computed(() => { this.selectedTree = koComputable(() => {
translatorTrigger(); translatorTrigger();
return [ return [
{ id: '', name: i18n(prefix + 'NONE') }, { id: '', name: i18n(prefix + 'NONE') },

View file

@ -1,4 +1,5 @@
import ko from 'ko'; import ko from 'ko';
import { koComputable } from 'External/ko';
import { FilterAction } from 'Model/Filter'; import { FilterAction } from 'Model/Filter';
import { FilterConditionField, FilterConditionType } from 'Model/FilterCondition'; import { FilterConditionField, FilterConditionType } from 'Model/FilterCondition';
@ -27,7 +28,7 @@ class FilterPopupView extends AbstractViewPopup {
this.fTrueCallback = null; this.fTrueCallback = null;
this.defaultOptionsAfterRender = defaultOptionsAfterRender; this.defaultOptionsAfterRender = defaultOptionsAfterRender;
this.folderSelectList = ko.computed(() => this.folderSelectList = koComputable(() =>
folderListOptionsBuilder( folderListOptionsBuilder(
[SettingsGet('SieveAllowFileintoInbox') ? '' : 'INBOX'], [SettingsGet('SieveAllowFileintoInbox') ? '' : 'INBOX'],
[['', '']], [['', '']],

View file

@ -1,4 +1,4 @@
import ko from 'ko'; import { koComputable } from 'External/ko';
import { Notification } from 'Common/Enums'; import { Notification } from 'Common/Enums';
import { UNUSED_OPTION_VALUE } from 'Common/Consts'; import { UNUSED_OPTION_VALUE } from 'Common/Consts';
@ -26,7 +26,7 @@ class FolderCreatePopupView extends AbstractViewPopup {
selectedParentValue: UNUSED_OPTION_VALUE selectedParentValue: UNUSED_OPTION_VALUE
}); });
this.parentFolderSelectList = ko.computed(() => this.parentFolderSelectList = koComputable(() =>
folderListOptionsBuilder( folderListOptionsBuilder(
[], [],
[['', '']], [['', '']],

View file

@ -1,4 +1,5 @@
import ko from 'ko'; import ko from 'ko';
import { koComputable } from 'External/ko';
import { SetSystemFoldersNotification } from 'Common/EnumsUser'; import { SetSystemFoldersNotification } from 'Common/EnumsUser';
import { UNUSED_OPTION_VALUE } from 'Common/Consts'; import { UNUSED_OPTION_VALUE } from 'Common/Consts';
@ -24,7 +25,7 @@ class FolderSystemPopupView extends AbstractViewPopup {
this.notification = ko.observable(''); this.notification = ko.observable('');
this.folderSelectList = ko.computed(() => this.folderSelectList = koComputable(() =>
folderListOptionsBuilder( folderListOptionsBuilder(
FolderUserStore.folderListSystemNames(), FolderUserStore.folderListSystemNames(),
[ [

View file

@ -1,4 +1,5 @@
import ko from 'ko'; import ko from 'ko';
import { koComputable } from 'External/ko';
import { convertLangName } from 'Common/Translator'; import { convertLangName } from 'Common/Translator';
@ -13,7 +14,7 @@ class LanguagesPopupView extends AbstractViewPopup {
this.langs = ko.observableArray(); this.langs = ko.observableArray();
this.languages = ko.computed(() => { this.languages = koComputable(() => {
const userLanguage = this.userLanguage(); const userLanguage = this.userLanguage();
return this.langs.map(language => ({ return this.langs.map(language => ({
key: language, key: language,

View file

@ -64,10 +64,6 @@ export class MailMessageList extends AbstractViewRight {
this.messageList = MessageUserStore.list; this.messageList = MessageUserStore.list;
this.sortSupported = ko.computed(() =>
FolderUserStore.hasCapability('SORT') | FolderUserStore.hasCapability('ESORT')
);
this.composeInEdit = AppUserStore.composeInEdit; this.composeInEdit = AppUserStore.composeInEdit;
this.isMobile = ThemeStore.isMobile; this.isMobile = ThemeStore.isMobile;
@ -107,6 +103,9 @@ export class MailMessageList extends AbstractViewRight {
this.addComputables({ this.addComputables({
sortSupported: () =>
FolderUserStore.hasCapability('SORT') | FolderUserStore.hasCapability('ESORT'),
folderMenuForMove: () => folderMenuForMove: () =>
folderListOptionsBuilder( folderListOptionsBuilder(
[FolderUserStore.currentFolderFullName()], [FolderUserStore.currentFolderFullName()],

View file

@ -20,6 +20,7 @@ import { ThemeStore } from 'Stores/Theme';
import Remote from 'Remote/User/Fetch'; import Remote from 'Remote/User/Fetch';
import { getNotification } from 'Common/Translator'; import { getNotification } from 'Common/Translator';
//import { clearCache } from 'Common/Cache'; //import { clearCache } from 'Common/Cache';
//import { koComputable } from 'External/ko';
export class SystemDropDownUserView extends AbstractViewRight { export class SystemDropDownUserView extends AbstractViewRight {
constructor() { constructor() {
@ -32,8 +33,8 @@ export class SystemDropDownUserView extends AbstractViewRight {
this.accounts = AccountUserStore.accounts; this.accounts = AccountUserStore.accounts;
this.accountsLoading = AccountUserStore.loading; this.accountsLoading = AccountUserStore.loading;
/* /*
this.accountsUnreadCount = : ko.computed(() => 0); this.accountsUnreadCount = : koComputable(() => 0);
this.accountsUnreadCount = : ko.computed(() => AccountUserStore.accounts().reduce((result, item) => result + item.count(), 0)); this.accountsUnreadCount = : koComputable(() => AccountUserStore.accounts().reduce((result, item) => result + item.count(), 0));
*/ */
this.addObservables({ this.addObservables({