patch()
Syntax: patch(element, newVNode?, prevVNode?, hook = (el?, newVNode?, oldVNode?) => boolean, effects = [])
Example: patch(el, m('div'), m('div', undefined, ['Hello World']))
The patch
function updates the DOM content by determining pinpoint changes through diffing a new VNode with an old VNode. It accepts a HTMLElement or Text, a new VNode, and an optional previous VNode. It also returns the resulting HTMLElement or Text.
You can leverage Flags and Delta to improve the performance of patch calls by reducing the need to diff children by improving time complexity.
import { m, patch, createElement } from 'million';
const vnode0 = m('div');
const el = createElement(vnode0);
document.body.appendChild(el);
const vnode1 = m('div', { id: 'app' }, ['Hello World']);
patch(el, vnode1);
// document.body.innerHTML = '' -> '<div id="app">Hello World</div>'
const vnode2 = m('div', { id: 'app' }, ['Goodbye World']);
patch(el, vnode2);
// document.body.innerHTML = '<div id="app">Hello World</div>' -> '<div id="app">Goodbye World</div>'
Hook & effects
Sometimes, we want to apply our own functionality across the entire VNode tree, yet we want to keep the API usage simple. There are two areas where you can do this:
-
VNode-by-VNode diffing: You can pass a callback into the
hook
parameter (seepatch
) syntax to deal with each VNode (excluding special diffing). You can construct acommit
callback like this:patch( el, newVNode, oldVNode, (el, newVNode, oldVNode) => { console.log('Currently diffing:', el); return true; }, [], );
You can return
true
to indicate that the VNode should be committed orfalse
to indicate that the VNode should be skipped. -
Operation-by-Operation patching: Once diffing is completed, you can deal with effects or DOM operations. Normally, effects are batched all at once, but if you want to modify this, you can check out custom patch functions. If you want to add your own custom effects, you can pass callbacks into the
effects
parameter like this:patch(el, newVNode, oldVNode, () => true, [ () => console.log('Hello World!'), ]);
Custom patch functions
You can use drivers to create your own custom patch functions. The useNode()
driver accepts an array of drivers, which runs after the sweeping modifications of an element are patched, and more pinpoint modifications may be necessary.
Driver Syntax: useNode([useChildren(), useProps(), useDriver([useAnotherDriver()])])
Driver
Signature: (el, newVNode, oldVNode, commit, effects, driver) => { ...; return { el, newVNode, oldVNode, commit, effects, driver } }
If you use an IDE like VSCode, you can look into the implementations of how to create a Driver
and create your own drivers.
import { m, useNode, useChildren, useProps, createElement } from 'million';
const diff = useNode([useChildren(), useProps()]);
const customPatch = (el, newVNode, oldVNode, commit, effects = []) => {
const data = diff(el, newVNode, oldVNode, commit, effects);
for (let i = 0; i < effects.length; i++) {
// Effect -> { type, flush() }
effects[i].flush();
}
return data.el;
};
const vnode0 = m('div');
const el = createElement(vnode0);
document.body.appendChild(el);
const vnode1 = m('div', { id: 'app' }, ['Hello World']);
customPatch(el, vnode1);
Writing your own drivers
Below is an implementation of a rudimentary virtual DOM with reference-based diffing of an existing el
.
import { createElement, EffectTypes } from 'million';
const useNodeReplace =
(drivers = []) =>
(el, newVNode, oldVNode, commit, effects = [], driver) => {
/**
* `drivers` can add another optional layer of composability, you can run the drivers
* by passing the same `drivers[i](el, newVNode, oldVNode, commit, effects, driver)`, or a manipulated
* version downstream `drivers[i](el.childNodes[j], newVNode.children[j], undefined, commit, effects, driver)`.
* The great thing about sub-drivers is you can run them anywhere you want inside the driver!
*/
const data = {
el,
newVNode,
oldVNode,
effects,
commit,
driver,
};
commit(() => {
if (!newVNode) {
effects.push({
type: EffectTypes.REMOVE,
flush: () => el.remove(),
});
} else if (oldVNode !== newVNode) {
effects.push({
type: EffectTypes.REPLACE,
flush: () => el.replaceWith(createElement(newVNode)),
});
}
}, data);
return data;
};