How did this got into my mind??
Virtual DOM can be referenced as the thing which just "introduced" me to the title of this post. What if we keep aside all that diffing, state stuff and focus on one thing: reactivity between the JavaScript and the DOM. Well, most of us are using libraries just to achieve this reactivity in their apps. But most of them implement a virtual DOM which keeps track of all the tags, states, variables, objects and whatnot and then syncs them with the real DOM. As said, things might get a little crazy doing all this stuff. So I just decided, why not just implement a crude example of all this "virtual DOM" thing without virtual DOM. Is this even achievable?? The answer is (0.5 * yes)!! For the sake of this post, lets call it "Reactivity Anywhere"
Disclaimer
This post might have things that seem vague and senseless. Also don't take things too seriously here, take them as just a thought. Reader discretion is advised.
Let's Start!!
Prerequisites
- A web browser
- JavaScript
Defining the global variables(precisely, stores)
In order to keep track of what goes here and there, we need some global variables to preserve and mutate all the state.
const __refs__ = {};
const __reactives__ = [];
const __reactiveEl__ = document.querySelectorAll("[reactive]");
const reactive = (obj) => {
/*Add logic*/
};
const __updateDOM__ = (ref) => {
/*Add logic*/
};
This is just everything we need for the logic. The variables whose names start and end with double underscores are __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.
We will only store two things: references to the elements, and, of course, the reactive variables.
But this seems like virtual DOM!!!
But I'm sorry, this is not the virtual DOM you think:
- We will not be diffing the whole element tree for every single change; only and only the affected element will be mutated (less carbon dioxide)
Determining reactive
elements
In order to keep specificity and refrain from scanning the whole DOM, we will just handpick special elements that work with our module. So, any element that has the reactive
attribute (<element reactive></element>
) , will only be able to use the special reactive powers.
In order to access the reactive elements from the store, we will use the ES6 string interpolation syntax. So, to access the count
, we will write
<h1 reactive>The time is ${count}</h1>
The __refs__
Here, we will store the values of the object passed in the reactive
function.
The __reactives__
This is just an array containing live references of the DOM Elements.
The reactive()
This function is basically a store where you'd be storing all the reactive stuff.
The definition of the function is surprisingly simple:
const reactive = (obj) => {
//Loop through the string
Object.keys(obj).forEach((key) => {
// defineProperty, anyone??
// We want to maintain reactivity, so we are using custom
// getters and setters
Object.defineProperty(__refs__, key, {
get() {
return obj[key];
},
// This shows an interesting aspect of the logic.
// This will update the target element everytime
// something changes.
set(val) {
obj[key] = val;
__updateDOM__(key);
},
});
// Think of this as an initial render
__updateDOM__(key);
});
// This is an important step otherwise
// everything is useless
return __refs__;
};
The __updateDOM__()
This is the Rosetta for the reactive
DOM Elements and the __refs__
. This function is also relative simple in its definition:
// Ref can be any key from the __refs__ store
const __updateDOM__ = (ref) => {
// This is to check whether we want to update a specific ref value
if (ref) {
__reactives__
// filter out the elements we need
.filter((reactive) => reactive.dataset.ref === ref)
.forEach((reactive) => {
let ref = reactive.dataset.ref;
// Interacting with the DOM
// Nullish coalescing, anybody?
reactive.textContent = __refs__[ref] ?? "";
});
}
// UPDATE ALL!!
else
__reactives__.forEach((reactive) => {
let ref = reactive.dataset.ref;
// Interacting with the DOM
// Nullish coalescing, anybody?
reactive.textContent = __refs__[ref] ?? "";
});
};
Finding all the reactive variables and bootstrapping them
This can basically be wrapped as an IIFE (Immediately Invoked Function Expression) but I don't consider doing it for the sake of simplicity. So, here we go!!
// Get live elements
const __reactiveEl__ = document.querySelectorAll("[reactive]");
__reactiveEl__.forEach((el) => {
// Match the `count` between <h1 reactive>${count}</h1>
const refs = el.innerHTML.match(/\${([^}]+)}/g);
// If the refs exist
if (refs) {
refs.forEach((ref) => {
// extract it
const dataRef = ref.split("{")[1].split("}")[0].trim();
// insert a special span element with the element
// and store the key name in the `data-ref` attribute
el.innerHTML = el.innerHTML.replace(
ref,
`<span class="reactivity-anywhere" data-ref="${dataRef}"></span>`
);
});
// Push the span element in __reactives__
__reactives__.push(...el.querySelectorAll("span.reactivity-anywhere"));
}
});
// Initialize all the magic!!
__updateDOM__();
Making <input>
and <textarea>
work with reactives
Of course, we need this if user input is needed for our code to run.
The supercharged textareas and input elements will bear the ref
attribute
A lot of things, harsh things are going to be done in this section, so brace yourself and hold on tight.
const parseDefaultRefValue = (el) => {
let parsed = null;
try {
// If the passed key is a function, run it
// and store the value
// I'm sorry, but we need to use eval here
parsed = eval(`(${el.getAttribute("ref-default")})()`);
} catch (e) {
parsed = el.getAttribute("ref-default");
}
return parsed;
};
const assignDefaultRefsToInputs = (el, ref) => {
__refs__[ref] = parseDefaultRefValue(el);
};
// Select input and textarea elements containing the
// 'ref' attribute, where the attr. value refers to any
// key in the __refs__ store.
// The `ref-default` contains the default value for the `ref`
// eg.
// <textarea ref="name"></textarea>
document.querySelectorAll("input[ref], textarea[ref]").forEach((el) => {
// Keep a ref to the ref!! Because we don't want to
// lose it in event listeners
const ref = el.getAttribute("ref");
if (ref) {
// lazily update default values
window.addEventListener("load", () => assignDefaultRefsToInputs(el, ref));
el.addEventListener("input", () => {
// again, a dumb reference to the ref
const elRef = ref;
// preserve default value
const defaultVal = parseDefaultRefValue(el);
// Set whatever value is typed as the ref value
// else, the default value
__refs__[elRef] = el.value !== "" ? el.value : defaultVal;
if (__refs__[elRef] !== defaultVal) {
// Keep rest of the input/textarea elements in sync
Array.from(document.querySelectorAll("input[ref], textarea[ref]"))
// Find input/textareas with same refs
.filter((el) => el.getAttribute("ref") === elRef)
// Keep their value in sync
.forEach((el) => (el.value = __refs__[elRef]));
}
});
}
});
We're almost done!
Now the only thing that remains is to write some HTML to check whether everything works!
So, here we go!!
Some more things to note here:
- You can use multiple stores!! However, if you redeclare a key in the latter store, it will take precedence, not the first one
Why something like this would be great to use (according to me)
- It will allow HTML to do its work and JS to do its own. Its not like "All HTML!" or "All JS!" but a harmony between the two (not to mention CSS here) that will appreciate the job these languages have to do.
- Minimal overhead. As I said earlier, no virtual DOM, only real DOM (credits to Svelte) with some objects in memory
Limitations
You're going to think over this :), because this is just a crude implementation of an idea. So, feel free to critically think over it.
Ending notes
If you seem to be intrested in creating some sort of framwework with this, you're ready to go (some frameworks, using this idea, might even exist)! I'd also be happy to help you! Thank you for bearing with me in such a long post!
Top comments (10)
And this is why we use compilers and templates on top of this. Challenging implementation to follow for a beginner, but I like the sentiment. I wouldn't worry about it being long. I wrote a similar article and it was a 17 min read with a precursor 15 min read. 5 mins feels breakneck speed for the subject matter. But I always love to see people writing and evangelizing the topic.
Link posted is 404 now, here's the new one angularindepth.com/posts/1289/soli...
Edit:
Ryan, thanks for this beautiful framework first and foremost, but please move the article to somewhere else. First the 301 redirect to their homepage (not even to posts index) and then there's no any information about the article; no author, no date and time, let alone the estimated reading time. IMHO your writings deserves better.
Edit 2:
I'm very well aware that it's been more than 3 years now, but still... the links are still active on some posts.
I think mine is a 5 min read because I haven't spoke much about the concepts revolving around all this. But I'm just starting out in writing posts, so it might take a while for me to improve :)
But virtual DOM has nothing to do with reactivity. Virtual DOM needed for declarative style programing - you don't need to manually change state of each element instead you return new JSX and VDOM will figure out what actually changed and apply the difference to the DOM
@stereobooster Actually, virtual DOM has different meanings that vary with context. So basically, I'm talking about the reconciliation that happens after there is some state change happening in the app. So I basically wanted to remove the extra reconciliation step and to keep states directly in sync. I hope you understand what I mean. So instead of returning a new element each time a state change is triggered, the existing element can be mutated; this will certainly avoid unnecessary logic and speed things up. If you have a good title for this post, please feel free to tell me :)
This is your article you can name it what you want (obviously). Alternatives: "You may not need Virtual DOM" π€·ββοΈ
I'm just saying that React is not reactive and Virtual DOM is not for reactivity it is to make declarative style (return of the whole JSX) possible.
Shawn Swyx Wang - Why React is not Reactive
Oh, now I get what you want to say, @stereobooster . But anyways, it was just an experiment :)
Hi Pranav,
Thanks for the article.
Could you have a look on meiosis.js.org/ ?
Regards
Hi @artydev , I liked the concept of meiosis.js. Nice one!
Niceπ
Look at this other exemple
dev.to/artydev/meiosis-a-simple-st...