Improving the Reactivity System (feat. TOAST UI Grid)

TOAST UI
TOAST UI
Jan 14 · 13 min read
Image for post
Image for postImage for post

Improving the Reactivity System (feat. TOAST UI Grid)

Lazy Observable

Image for post
Image for postImage for post

1. Get the List of Objects to be Transformed into Observable Data.

function createOriginData(data, rowRange) {
const [start, end] = rowRange;
return data.gridRows.slice(start, end).reduce(
(acc, row, index) => {
// Do not include the data that is already observable
if (!isObservable(row)) {
acc.rows.push(row);
acc.targetIndexes.push(start + index);
}
return acc;
},
{
rows: [],
targetIndexes: []
}
);
}

2. Transform the Original Data to be Observable.

function createObservableData({ column, data, viewport }) {
const originData = createOriginData(data, viewport.rowRange);
if (!originData.rows.length) {
return;
}
changeToObservableData(column, data, originData);
}
function changeToObservableData(column, data, originData) {
const { targetIndexes, rows } = originData;
// Generate observable data
const gridRows = createData(rows, column);
for (let i = 0, end = gridRows.length; i < end; i += 1) {
const targetIndex = targetIndexes[i];
data.gridRows.splice(targetIndex, 1, gridRows[i]);
}
}

3. Detect the Changes in Rendering Range.


observe(() => createObservableData(store));
/**
* Get the List of Objects to be Transformed into Observable Data.
*/
function createOriginData(data, rowRange) {
const [start, end] = rowRange;
return data.gridRows.slice(start, end).reduce(
(acc, row, index) => {
// Do not include the data that is already observable
if (!isObservable(row)) {
acc.rows.push(row);
acc.targetIndexes.push(start + index);
}
return acc;
},
{
rows: [],
targetIndexes: []
}
);
}
/**
* Transform the Original Data to be Observable.
*/
function changeToObservableData(column, data, originData) {
const { targetIndexes, rows } = originData;
// Generate observable data
const gridRows = createData(rows, column);
for (let i = 0, end = gridRows.length; i < end; i += 1) {
const targetIndex = targetIndexes[i];
data.gridRows.splice(targetIndex, 1, gridRows[i]);
}
}
export function createObservableData({ column, data, viewport }) {
const originData = createOriginData(data, viewport.rowRange);
if (!originData.rows.length) {
return;
}
changeToObservableData(column, data, originData);
}
/**
* Detect the changes in rendering range and
* dynamically modify the data that is not observable automatically.
*/
observe(() => createObservableData(store));

Batch Processing

Effective Update Management

Eliminating Duplicate Updates

function callObserver(observerId) {
observerIdStack.push(observerId);
observerInfoMap[observerId].fn();
observerIdStack.pop();
}
function run(observerId) {
callObserver(observerId);
}
function observe(fn) {
// do something
run(observerId);
}
let queue = [];
let observerIdMap = {};
function batchUpdate(observerId) {
if (!observerIdMap[observerId]) {
observerIdMap[observerId] = true;
queue.push(observerId);
}
}
function run(observerId) {
batchUpdate(observerId);
}
let queue = [];
let observerIdMap = {};
let pending = false;
function batchUpdate(observerId) {
if (!observerIdMap[observerId]) {
observerIdMap[observerId] = true;
queue.push(observerId);
}
if (!pending) {
flush();
}
}
function callObserver(observerId) {
observerIdStack.push(observerId);
observerInfoMap[observerId].fn();
observerIdStack.pop();
}
function clearQueue() {
queue = [];
observerIdMap = {};
pending = false;
}
function flush() {
pending = true;
for (let i = 0; i < queue.length; i += 1) {
const observerId = queue[i];
observerIdMap[observerId] = false;
callObserver(observerId);
}
clearQueue();
}

Monkey Patch Array

1. Array Data Update Code Before Monkey Patching

function appendRow(store, row) {
// do something
rawData.splice(at, 0, rawRow);
viewData.splice(at, 0, viewRow);
heights.splice(at, 0, getRowHeight(rawRow, dimension.rowHeight));
// notify function call
notify(data, 'rawData');
notify(data, 'viewData');
notify(data, 'filteredRawData');
notify(data, 'filteredViewData');
notify(rowCoords, 'heights');
}

2. Monkey Patch Code that Wraps the Array Methods

const methods = ['splice', 'push', 'pop', 'shift', 'unshift', 'sort'];export function patchArrayMethods(arr, obj, key) {
methods.forEach(method => {
const patchedMethods = Array.prototype[method];
// Monkey patch original methods with the patch function
arr[method] = function patch(...args) {
const result = patchedMethods.apply(this, args);
notify(obj, key);
return result;
};
});
return arr;
}

3. Monkey Patching Code that Wraps Array Methods

function setValue(storage, resultObj, observerIdSet, key, value) {
if (storage[key] !== value) {
if (Array.isArray(value)) {
patchArrayMethods(value, resultObj, key);
}
storage[key] = value;
Object.keys(observerIdSet).forEach(observerId => {
run(observerId);
});
}
}
export function observable(obj) {
// do something
Object.keys(obj).forEach(key => {
// do something
if (isFunction(getter)) {
observe(() => {
const value = getter.call(resultObj);
setValue(storage, resultObj, observerIdSet, key, value);
});
} else {
storage[key] = obj[key];
if (Array.isArray(storage[key])) {
patchArrayMethods(storage[key], resultObj, key);
}
Object.defineProperty(resultObj, key, {
set(value) {
setValue(storage, resultObj, observerIdSet, key, value);
}
});
}
});
return resultObj;
}
function appendRow(store, row) {
// do something
rawData.splice(at, 0, rawRow);
viewData.splice(at, 0, viewRow);
heights.splice(at, 0, getRowHeight(rawRow, dimension.rowHeight));
}

📝 Summary

function changeToObservableData(column, data, originData) {
const { targetIndexes, rows } = originData;
// Generate observable data
const gridRows = createData(rows, column);
for (let i = 0, end = gridRows.length; i < end; i += 1) {
const targetIndex = targetIndexes[i];
data.gridRows.splice(targetIndex, 1, gridRows[i]);
}
}
export function createObservableData({ column, data, viewport }) {
const originData = createOriginData(data, viewport.rowRange);
if (!originData.rows.length) {
return;
}
changeToObservableData(column, data, originData);
}
observe(() => createObservableData(store));
for (let i = 0, end = gridRows.length; i < end; i += 1) {
const targetIndex = targetIndexes[i];
data.gridRows.splice(targetIndex, 1, gridRows[i]);
}
observe(() => createObservableData(store));

Warning❗

const obj = observable({
start: 0,
end: 0,
get expensiveCalculation() {
let result = this.start + this.end;
// ... do expensive calculation
return result;
}
});
obj.start = 1;
obj.end = 1;

🎀 Closing Remarks

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store