Skip to content

Commit

Permalink
[add] Async Render mode based on Scheduler API (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
TechQuery authored Nov 20, 2024
1 parent c63a890 commit 28c1319
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 44 deletions.
97 changes: 77 additions & 20 deletions ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,40 +11,69 @@ A light-weight DOM Renderer supports [Web components][1] standard & [TypeScript]

## Feature

- input: **Virtual DOM** object in **JSX** syntax
- output: **DOM** object or **XML** string of **HTML**, **SVG** & **MathML** languages
- input: [Virtual DOM][7] object in [JSX][8] syntax
- output: [DOM][9] object or [XML][10] string of [HTML][11], [SVG][12] & [MathML][13] languages
- run as: **Sync**, [Async][14], [Generator][15] functions & [Readable streams][16]

## Usage

### JavaScript

#### Sync Rendering

```js
import { DOMRenderer } from 'dom-renderer';
import { DOMRenderer, VNode } from 'dom-renderer';

const newVNode = new DOMRenderer().patch(
{
new VNode({
tagName: 'body',
node: document.body
},
{
}),
new VNode({
tagName: 'body',
children: [
{
new VNode({
tagName: 'a',
props: { href: 'https://idea2.app/' },
style: { color: 'red' },
children: [{ text: 'idea2app' }]
}
children: [new VNode({ text: 'idea2app' })]
})
]
}
})
);

console.log(newVNode);
```

#### Async Rendering (experimental)

```diff
import { DOMRenderer, VNode } from 'dom-renderer';

-const newVNode = new DOMRenderer().patch(
+const newVNode = new DOMRenderer().patchAsync(
new VNode({
tagName: 'body',
node: document.body
}),
new VNode({
tagName: 'body',
children: [
new VNode({
tagName: 'a',
props: { href: 'https://idea2.app/' },
style: { color: 'red' },
children: [new VNode({ text: 'idea2app' })]
})
]
})
);
-console.log(newVNode);
+newVNode.then(console.log);
```

### TypeScript

[![Edit DOM Renderer example](https://codesandbox.io/static/img/play-codesandbox.svg)][7]
[![Edit DOM Renderer example](https://codesandbox.io/static/img/play-codesandbox.svg)][17]

#### `tsconfig.json`

Expand All @@ -59,6 +88,8 @@ console.log(newVNode);

#### `index.tsx`

##### Sync Rendering

```tsx
import { DOMRenderer } from 'dom-renderer';

Expand All @@ -67,10 +98,26 @@ const newVNode = new DOMRenderer().render(
idea2app
</a>
);

console.log(newVNode);
```

##### Async Rendering (experimental)

```diff
import { DOMRenderer } from 'dom-renderer';

const newVNode = new DOMRenderer().render(
<a href="https://idea2.app/" style={{ color: 'red' }}>
idea2app
- </a>
+ </a>,
+ document.body,
+ 'async'
);
-console.log(newVNode);
+newVNode.then(console.log);
```

### Node.js & Bun

#### `view.tsx`
Expand Down Expand Up @@ -105,25 +152,35 @@ createServer((request, response) => {

### Web components

[![Edit MobX Web components](https://codesandbox.io/static/img/play-codesandbox.svg)][8]
[![Edit MobX Web components](https://codesandbox.io/static/img/play-codesandbox.svg)][18]

## Original

### Inspiration

[![SnabbDOM](https://github.com/snabbdom.png)][9]
[![SnabbDOM](https://github.com/snabbdom.png)][19]

### Prototype

[![Edit DOM Renderer](https://codesandbox.io/static/img/play-codesandbox.svg)][10]
[![Edit DOM Renderer](https://codesandbox.io/static/img/play-codesandbox.svg)][20]

[1]: https://www.webcomponents.org/
[2]: https://www.typescriptlang.org/
[3]: https://libraries.io/npm/dom-renderer
[4]: https://github.com/EasyWebApp/DOM-Renderer/actions/workflows/main.yml
[5]: https://nodei.co/npm/dom-renderer/
[6]: https://gitpod.io/?autostart=true#https://github.com/EasyWebApp/DOM-Renderer
[7]: https://codesandbox.io/s/dom-renderer-example-pmcsvs?autoresize=1&expanddevtools=1&fontsize=14&hidenavigation=1&module=%2Fsrc%2Findex.tsx&theme=dark
[8]: https://codesandbox.io/s/mobx-web-components-pvn9rf?autoresize=1&fontsize=14&hidenavigation=1&module=%2Fsrc%2FWebComponent.ts&moduleview=1&theme=dark
[9]: https://github.com/snabbdom/snabbdom
[10]: https://codesandbox.io/s/dom-renderer-pglxkx?autoresize=1&expanddevtools=1&fontsize=14&hidenavigation=1&module=%2Fsrc%2Findex.ts&theme=dark
[7]: https://en.wikipedia.org/wiki/Virtual_DOM
[8]: https://facebook.github.io/jsx/
[9]: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model
[10]: https://developer.mozilla.org/en-US/docs/Web/XML
[11]: https://developer.mozilla.org/en-US/docs/Web/HTML
[12]: https://developer.mozilla.org/en-US/docs/Web/SVG
[13]: https://developer.mozilla.org/en-US/docs/Web/MathML
[14]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
[15]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*
[16]: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream
[17]: https://codesandbox.io/s/dom-renderer-example-pmcsvs?autoresize=1&expanddevtools=1&fontsize=14&hidenavigation=1&module=%2Fsrc%2Findex.tsx&theme=dark
[18]: https://codesandbox.io/s/mobx-web-components-pvn9rf?autoresize=1&fontsize=14&hidenavigation=1&module=%2Fsrc%2FWebComponent.ts&moduleview=1&theme=dark
[19]: https://github.com/snabbdom/snabbdom
[20]: https://codesandbox.io/s/dom-renderer-pglxkx?autoresize=1&expanddevtools=1&fontsize=14&hidenavigation=1&module=%2Fsrc%2Findex.ts&theme=dark
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dom-renderer",
"version": "2.5.1",
"version": "2.6.0",
"license": "LGPL-3.0-or-later",
"author": "shiy2008@gmail.com",
"description": "A light-weight DOM Renderer supports Web components standard & TypeScript language",
Expand All @@ -25,6 +25,7 @@
"main": "dist/index.js",
"dependencies": {
"declarative-shadow-dom-polyfill": "^0.4.0",
"scheduler-polyfill": "^1.3.0",
"tslib": "^2.8.1",
"web-streams-polyfill": "^4.0.0",
"web-utility": "^4.4.2"
Expand All @@ -37,14 +38,14 @@
"@types/jest": "^29.5.14",
"@types/node": "^20.17.6",
"happy-dom": "^14.12.3",
"husky": "^9.1.6",
"husky": "^9.1.7",
"jest": "^29.7.0",
"lint-staged": "^15.2.10",
"open-cli": "^8.0.0",
"prettier": "^3.3.3",
"ts-jest": "^29.2.5",
"typedoc": "^0.26.11",
"typedoc-plugin-mdn-links": "^3.3.7",
"typedoc-plugin-mdn-links": "^3.3.8",
"typescript": "~5.6.3"
},
"prettier": {
Expand Down
28 changes: 18 additions & 10 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 58 additions & 7 deletions source/dist/DOMRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'scheduler-polyfill';
import { ReadableStream } from 'web-streams-polyfill';
import {
diffKeys,
Expand All @@ -11,6 +12,8 @@ import {

import { DataObject, VNode } from './VDOM';

export type RenderMode = 'sync' | 'async';

export interface UpdateTask {
index?: number;
oldVNode?: VNode;
Expand All @@ -24,6 +27,7 @@ export class DOMRenderer {
document = globalThis.document;

protected treeCache = new WeakMap<Node, VNode>();
protected signalCache = new WeakMap<Node, AbortController>();

protected keyOf = ({ key, text, props, selector }: VNode, index?: number) =>
key?.toString() || props?.id || (text || selector || '') + index;
Expand Down Expand Up @@ -160,13 +164,15 @@ export class DOMRenderer {
(style, key, value) => style.setProperty(toHyphenCase(key), value)
);
newVNode.node ||= oldVNode.node;

return newVNode;
}

patch(oldVRoot: VNode, newVRoot: VNode) {
*generateDOM(oldVRoot: VNode, newVRoot: VNode) {
if (VNode.isFragment(newVRoot))
newVRoot = new VNode({ ...oldVRoot, children: newVRoot.children });

this.patchNode(oldVRoot, newVRoot);
yield this.patchNode(oldVRoot, newVRoot);

for (let { index, oldVNode, newVNode } of this.diffVChildren(oldVRoot, newVRoot)) {
if (!newVNode) {
Expand All @@ -192,20 +198,65 @@ export class DOMRenderer {

if (inserting) newVNode.ref?.(newVNode.node);
}
yield newVNode;
}
}

patch(oldVRoot: VNode, newVRoot: VNode) {
var count = 0;

for (const newVNode of this.generateDOM(oldVRoot, newVRoot))
if (++count === 1) newVRoot = newVNode;

return newVRoot;
}

async patchAsync(oldVRoot: VNode, newVRoot: VNode) {
const oldController = this.signalCache.get(oldVRoot.node);

if (oldController) {
oldController.abort();

oldVRoot = VNode.fromDOM(oldVRoot.node);
}
const controller = new AbortController();

this.signalCache.set(oldVRoot.node, controller);

var count = 0;

for (const newVNode of this.generateDOM(oldVRoot, newVRoot)) {
if (++count === 1) newVRoot = newVNode;

await scheduler.yield();

if (controller.signal.aborted) {
this.signalCache.delete(oldVRoot.node);

controller.signal.throwIfAborted();
}
}
this.signalCache.delete(oldVRoot.node);

return newVRoot;
}

render(vNode: VNode, node: ParentNode = globalThis.document?.body) {
render(vNode: VNode, node?: ParentNode, mode?: 'sync'): VNode;
render(vNode: VNode, node?: ParentNode, mode?: 'async'): Promise<VNode>;
render(
vNode: VNode,
node: ParentNode = globalThis.document?.body,
mode: RenderMode = 'sync'
): VNode | Promise<VNode> {
this.document = node.ownerDocument;

var root = this.treeCache.get(node) || VNode.fromDOM(node);

root = this.patch(root, new VNode({ ...root, children: [vNode] }));

this.treeCache.set(node, root);
const done = (root: VNode) => this.treeCache.set(node, root) && root;

return root;
return mode === 'sync'
? done(this.patch(root, new VNode({ ...root, children: [vNode] })))
: this.patchAsync(root, new VNode({ ...root, children: [vNode] })).then(done);
}

renderToStaticMarkup(tree: VNode) {
Expand Down
Loading

0 comments on commit 28c1319

Please sign in to comment.