Skip to content

Commit 28c1319

Browse files
authored
[add] Async Render mode based on Scheduler API (#11)
1 parent c63a890 commit 28c1319

File tree

6 files changed

+198
-44
lines changed

6 files changed

+198
-44
lines changed

ReadMe.md

Lines changed: 77 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,40 +11,69 @@ A light-weight DOM Renderer supports [Web components][1] standard & [TypeScript]
1111

1212
## Feature
1313

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

1718
## Usage
1819

1920
### JavaScript
2021

22+
#### Sync Rendering
23+
2124
```js
22-
import { DOMRenderer } from 'dom-renderer';
25+
import { DOMRenderer, VNode } from 'dom-renderer';
2326

2427
const newVNode = new DOMRenderer().patch(
25-
{
28+
new VNode({
2629
tagName: 'body',
2730
node: document.body
28-
},
29-
{
31+
}),
32+
new VNode({
3033
tagName: 'body',
3134
children: [
32-
{
35+
new VNode({
3336
tagName: 'a',
3437
props: { href: 'https://idea2.app/' },
3538
style: { color: 'red' },
36-
children: [{ text: 'idea2app' }]
37-
}
39+
children: [new VNode({ text: 'idea2app' })]
40+
})
3841
]
39-
}
42+
})
4043
);
41-
4244
console.log(newVNode);
4345
```
4446

47+
#### Async Rendering (experimental)
48+
49+
```diff
50+
import { DOMRenderer, VNode } from 'dom-renderer';
51+
52+
-const newVNode = new DOMRenderer().patch(
53+
+const newVNode = new DOMRenderer().patchAsync(
54+
new VNode({
55+
tagName: 'body',
56+
node: document.body
57+
}),
58+
new VNode({
59+
tagName: 'body',
60+
children: [
61+
new VNode({
62+
tagName: 'a',
63+
props: { href: 'https://idea2.app/' },
64+
style: { color: 'red' },
65+
children: [new VNode({ text: 'idea2app' })]
66+
})
67+
]
68+
})
69+
);
70+
-console.log(newVNode);
71+
+newVNode.then(console.log);
72+
```
73+
4574
### TypeScript
4675

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

4978
#### `tsconfig.json`
5079

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

6089
#### `index.tsx`
6190

91+
##### Sync Rendering
92+
6293
```tsx
6394
import { DOMRenderer } from 'dom-renderer';
6495

@@ -67,10 +98,26 @@ const newVNode = new DOMRenderer().render(
6798
idea2app
6899
</a>
69100
);
70-
71101
console.log(newVNode);
72102
```
73103

104+
##### Async Rendering (experimental)
105+
106+
```diff
107+
import { DOMRenderer } from 'dom-renderer';
108+
109+
const newVNode = new DOMRenderer().render(
110+
<a href="https://idea2.app/" style={{ color: 'red' }}>
111+
idea2app
112+
- </a>
113+
+ </a>,
114+
+ document.body,
115+
+ 'async'
116+
);
117+
-console.log(newVNode);
118+
+newVNode.then(console.log);
119+
```
120+
74121
### Node.js & Bun
75122

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

106153
### Web components
107154

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

110157
## Original
111158

112159
### Inspiration
113160

114-
[![SnabbDOM](https://github.com/snabbdom.png)][9]
161+
[![SnabbDOM](https://github.com/snabbdom.png)][19]
115162

116163
### Prototype
117164

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

120167
[1]: https://www.webcomponents.org/
121168
[2]: https://www.typescriptlang.org/
122169
[3]: https://libraries.io/npm/dom-renderer
123170
[4]: https://github.com/EasyWebApp/DOM-Renderer/actions/workflows/main.yml
124171
[5]: https://nodei.co/npm/dom-renderer/
125172
[6]: https://gitpod.io/?autostart=true#https://github.com/EasyWebApp/DOM-Renderer
126-
[7]: https://codesandbox.io/s/dom-renderer-example-pmcsvs?autoresize=1&expanddevtools=1&fontsize=14&hidenavigation=1&module=%2Fsrc%2Findex.tsx&theme=dark
127-
[8]: https://codesandbox.io/s/mobx-web-components-pvn9rf?autoresize=1&fontsize=14&hidenavigation=1&module=%2Fsrc%2FWebComponent.ts&moduleview=1&theme=dark
128-
[9]: https://github.com/snabbdom/snabbdom
129-
[10]: https://codesandbox.io/s/dom-renderer-pglxkx?autoresize=1&expanddevtools=1&fontsize=14&hidenavigation=1&module=%2Fsrc%2Findex.ts&theme=dark
173+
[7]: https://en.wikipedia.org/wiki/Virtual_DOM
174+
[8]: https://facebook.github.io/jsx/
175+
[9]: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model
176+
[10]: https://developer.mozilla.org/en-US/docs/Web/XML
177+
[11]: https://developer.mozilla.org/en-US/docs/Web/HTML
178+
[12]: https://developer.mozilla.org/en-US/docs/Web/SVG
179+
[13]: https://developer.mozilla.org/en-US/docs/Web/MathML
180+
[14]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
181+
[15]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*
182+
[16]: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream
183+
[17]: https://codesandbox.io/s/dom-renderer-example-pmcsvs?autoresize=1&expanddevtools=1&fontsize=14&hidenavigation=1&module=%2Fsrc%2Findex.tsx&theme=dark
184+
[18]: https://codesandbox.io/s/mobx-web-components-pvn9rf?autoresize=1&fontsize=14&hidenavigation=1&module=%2Fsrc%2FWebComponent.ts&moduleview=1&theme=dark
185+
[19]: https://github.com/snabbdom/snabbdom
186+
[20]: https://codesandbox.io/s/dom-renderer-pglxkx?autoresize=1&expanddevtools=1&fontsize=14&hidenavigation=1&module=%2Fsrc%2Findex.ts&theme=dark

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dom-renderer",
3-
"version": "2.5.1",
3+
"version": "2.6.0",
44
"license": "LGPL-3.0-or-later",
55
"author": "[email protected]",
66
"description": "A light-weight DOM Renderer supports Web components standard & TypeScript language",
@@ -25,6 +25,7 @@
2525
"main": "dist/index.js",
2626
"dependencies": {
2727
"declarative-shadow-dom-polyfill": "^0.4.0",
28+
"scheduler-polyfill": "^1.3.0",
2829
"tslib": "^2.8.1",
2930
"web-streams-polyfill": "^4.0.0",
3031
"web-utility": "^4.4.2"
@@ -37,14 +38,14 @@
3738
"@types/jest": "^29.5.14",
3839
"@types/node": "^20.17.6",
3940
"happy-dom": "^14.12.3",
40-
"husky": "^9.1.6",
41+
"husky": "^9.1.7",
4142
"jest": "^29.7.0",
4243
"lint-staged": "^15.2.10",
4344
"open-cli": "^8.0.0",
4445
"prettier": "^3.3.3",
4546
"ts-jest": "^29.2.5",
4647
"typedoc": "^0.26.11",
47-
"typedoc-plugin-mdn-links": "^3.3.7",
48+
"typedoc-plugin-mdn-links": "^3.3.8",
4849
"typescript": "~5.6.3"
4950
},
5051
"prettier": {

pnpm-lock.yaml

Lines changed: 18 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

source/dist/DOMRenderer.ts

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'scheduler-polyfill';
12
import { ReadableStream } from 'web-streams-polyfill';
23
import {
34
diffKeys,
@@ -11,6 +12,8 @@ import {
1112

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

15+
export type RenderMode = 'sync' | 'async';
16+
1417
export interface UpdateTask {
1518
index?: number;
1619
oldVNode?: VNode;
@@ -24,6 +27,7 @@ export class DOMRenderer {
2427
document = globalThis.document;
2528

2629
protected treeCache = new WeakMap<Node, VNode>();
30+
protected signalCache = new WeakMap<Node, AbortController>();
2731

2832
protected keyOf = ({ key, text, props, selector }: VNode, index?: number) =>
2933
key?.toString() || props?.id || (text || selector || '') + index;
@@ -160,13 +164,15 @@ export class DOMRenderer {
160164
(style, key, value) => style.setProperty(toHyphenCase(key), value)
161165
);
162166
newVNode.node ||= oldVNode.node;
167+
168+
return newVNode;
163169
}
164170

165-
patch(oldVRoot: VNode, newVRoot: VNode) {
171+
*generateDOM(oldVRoot: VNode, newVRoot: VNode) {
166172
if (VNode.isFragment(newVRoot))
167173
newVRoot = new VNode({ ...oldVRoot, children: newVRoot.children });
168174

169-
this.patchNode(oldVRoot, newVRoot);
175+
yield this.patchNode(oldVRoot, newVRoot);
170176

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

193199
if (inserting) newVNode.ref?.(newVNode.node);
194200
}
201+
yield newVNode;
202+
}
203+
}
204+
205+
patch(oldVRoot: VNode, newVRoot: VNode) {
206+
var count = 0;
207+
208+
for (const newVNode of this.generateDOM(oldVRoot, newVRoot))
209+
if (++count === 1) newVRoot = newVNode;
210+
211+
return newVRoot;
212+
}
213+
214+
async patchAsync(oldVRoot: VNode, newVRoot: VNode) {
215+
const oldController = this.signalCache.get(oldVRoot.node);
216+
217+
if (oldController) {
218+
oldController.abort();
219+
220+
oldVRoot = VNode.fromDOM(oldVRoot.node);
221+
}
222+
const controller = new AbortController();
223+
224+
this.signalCache.set(oldVRoot.node, controller);
225+
226+
var count = 0;
227+
228+
for (const newVNode of this.generateDOM(oldVRoot, newVRoot)) {
229+
if (++count === 1) newVRoot = newVNode;
230+
231+
await scheduler.yield();
232+
233+
if (controller.signal.aborted) {
234+
this.signalCache.delete(oldVRoot.node);
235+
236+
controller.signal.throwIfAborted();
237+
}
195238
}
239+
this.signalCache.delete(oldVRoot.node);
240+
196241
return newVRoot;
197242
}
198243

199-
render(vNode: VNode, node: ParentNode = globalThis.document?.body) {
244+
render(vNode: VNode, node?: ParentNode, mode?: 'sync'): VNode;
245+
render(vNode: VNode, node?: ParentNode, mode?: 'async'): Promise<VNode>;
246+
render(
247+
vNode: VNode,
248+
node: ParentNode = globalThis.document?.body,
249+
mode: RenderMode = 'sync'
250+
): VNode | Promise<VNode> {
200251
this.document = node.ownerDocument;
201252

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

204-
root = this.patch(root, new VNode({ ...root, children: [vNode] }));
205-
206-
this.treeCache.set(node, root);
255+
const done = (root: VNode) => this.treeCache.set(node, root) && root;
207256

208-
return root;
257+
return mode === 'sync'
258+
? done(this.patch(root, new VNode({ ...root, children: [vNode] })))
259+
: this.patchAsync(root, new VNode({ ...root, children: [vNode] })).then(done);
209260
}
210261

211262
renderToStaticMarkup(tree: VNode) {

0 commit comments

Comments
 (0)