Skip to content

Commit a7fd66a

Browse files
committed
[ Refactor ] Async render Sub Views based on requestAnimationFrame()
1 parent d480a5e commit a7fd66a

File tree

8 files changed

+158
-69
lines changed

8 files changed

+158
-69
lines changed

ReadMe.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
**Template engine** based on **HTML 5** & **ECMAScript 6**
44

5-
[![NPM Dependency](https://david-dm.org/EasyWebApp/DOMRenderer.svg)](https://david-dm.org/EasyWebApp/DOMRenderer)
5+
[![NPM Dependency](https://david-dm.org/EasyWebApp/DOM-Renderer.svg)](https://david-dm.org/EasyWebApp/DOM-Renderer)
66

7-
[![Build Status](https://travis-ci.com/EasyWebApp/DOMRenderer.svg?branch=master)](https://travis-ci.com/EasyWebApp/DOMRenderer)
7+
[![Build Status](https://travis-ci.com/EasyWebApp/DOM-Renderer.svg?branch=master)](https://travis-ci.com/EasyWebApp/DOM-Renderer)
88

99
[![NPM](https://nodei.co/npm/dom-renderer.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/dom-renderer/)

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dom-renderer",
3-
"version": "0.4.0",
3+
"version": "0.5.0",
44
"description": "Template engine based on HTML 5 & ECMAScript 6",
55
"keywords": [
66
"template",
@@ -10,13 +10,13 @@
1010
],
1111
"license": "LGPL-3.0",
1212
"author": "shiy2008@gmail.com",
13-
"homepage": "https://web-cell.tk/DOMRenderer/",
13+
"homepage": "https://web-cell.tk/DOM-Renderer/",
1414
"repository": {
1515
"type": "git",
16-
"url": "git+https://github.com/EasyWebApp/DOMRenderer.git"
16+
"url": "git+https://github.com/EasyWebApp/DOM-Renderer.git"
1717
},
1818
"bugs": {
19-
"url": "https://github.com/EasyWebApp/DOMRenderer/issues"
19+
"url": "https://github.com/EasyWebApp/DOM-Renderer/issues"
2020
},
2121
"module": "source/index.js",
2222
"browser": "dist/dom-renderer.js",
@@ -44,13 +44,13 @@
4444
},
4545
"devDependencies": {
4646
"@babel/polyfill": "^7.2.5",
47-
"@babel/preset-env": "^7.2.3",
47+
"@babel/preset-env": "^7.3.1",
4848
"@babel/register": "^7.0.0",
4949
"amd-bundle": "^1.7.4",
5050
"esdoc": "^1.1.0",
5151
"esdoc-external-webapi-plugin": "^1.0.0",
5252
"esdoc-standard-plugin": "^1.0.0",
53-
"eslint": "^5.12.0",
53+
"eslint": "^5.13.0",
5454
"fs-extra": "^7.0.1",
5555
"husky": "^1.3.1",
5656
"jsdom": "^13.2.0",

source/Model.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,19 @@ export default class Model {
2828
return Object.getPrototypeOf(view_data.get(this));
2929
}
3030

31+
/**
32+
* @return {Object}
33+
*/
34+
valueOf() {
35+
const { data } = this,
36+
value = {};
37+
38+
for (let key in data)
39+
if (data.hasOwnProperty(key)) value[key] = data[key];
40+
41+
return value;
42+
}
43+
3144
/**
3245
* @param {Object} data
3346
*

source/View.js

Lines changed: 107 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
import Model from './Model';
22

3-
import { parseDOM, scanTemplate, stringifyDOM, attributeMap } from './utility';
3+
import {
4+
parseDOM,
5+
scanTemplate,
6+
stringifyDOM,
7+
attributeMap,
8+
nextTick
9+
} from './utility';
410

511
import Template from './Template';
612

713
const { forEach, push } = Array.prototype;
814

15+
const iterator = [][Symbol.iterator];
16+
917
const view_template = Symbol('View template'),
10-
view_top = new Map(),
18+
view_top = new WeakMap(),
1119
view_injection = Symbol('View injection'),
12-
view_varible = ['view', 'scope'];
20+
view_varible = ['view', 'scope'],
21+
element_view = new WeakMap();
1322

1423
export default class View extends Model {
1524
/**
@@ -23,22 +32,28 @@ export default class View extends Model {
2332
(this[view_template] = template + ''),
2433
(this[view_injection] = injection);
2534

35+
const top = [];
36+
37+
view_top.set(this, top);
38+
2639
forEach.call(parseDOM(template).childNodes, node => {
27-
view_top.set(node, this);
40+
top.push(node);
2841

2942
if (node.nodeType === 1) this.parseTree(node);
3043
});
44+
45+
Object.freeze(top);
46+
}
47+
48+
[Symbol.iterator]() {
49+
return iterator.call(this);
3150
}
3251

3352
/**
3453
* @type {Node[]}
3554
*/
3655
get topNodes() {
37-
const list = [];
38-
39-
view_top.forEach((view, node) => view === this && list.push(node));
40-
41-
return list;
56+
return view_top.get(this);
4257
}
4358

4459
/**
@@ -48,15 +63,6 @@ export default class View extends Model {
4863
return stringifyDOM(this.topNodes);
4964
}
5065

51-
/**
52-
* @param {Element} root
53-
*/
54-
static clear(root) {
55-
Array.from(view_top.keys())
56-
.filter(node => root.compareDocumentPosition(node) & 16)
57-
.forEach(node => (view_top.delete(node), node.remove()));
58-
}
59-
6066
/**
6167
* @return {View}
6268
*/
@@ -80,6 +86,31 @@ export default class View extends Model {
8086
return (root.innerHTML = '') || raw;
8187
}
8288

89+
/**
90+
* @protected
91+
*
92+
* @param {String} type
93+
* @param {Element} element
94+
* @param {Template|View} renderer
95+
*/
96+
addNode(type, element, renderer) {
97+
const name = element.dataset.view;
98+
99+
push.call(this, { type, element, renderer, name });
100+
101+
if (type !== 'View') return;
102+
103+
const sub_view = [];
104+
105+
element_view.set(element, sub_view);
106+
107+
if (!(name in this))
108+
Object.defineProperty(this, name, {
109+
get: () => (sub_view[1] ? sub_view : sub_view[0]),
110+
enumerable: true
111+
});
112+
}
113+
83114
/**
84115
* @protected
85116
*
@@ -94,39 +125,39 @@ export default class View extends Model {
94125
attribute: ({ ownerElement, name, value }) => {
95126
name = attributeMap[name] || name;
96127

97-
push.call(this, {
98-
type: 'Attr',
99-
element: ownerElement,
100-
renderer: new Template(
128+
this.addNode(
129+
'Attr',
130+
ownerElement,
131+
new Template(
101132
value,
102133
injection,
103134
name in ownerElement
104135
? value => (ownerElement[name] = value)
105136
: value => ownerElement.setAttribute(name, value)
106137
)
107-
});
138+
);
108139
},
109140
text: node => {
110141
const { parentNode } = node;
111142

112-
push.call(this, {
113-
type: 'Text',
114-
element: parentNode,
115-
renderer: new Template(
143+
this.addNode(
144+
'Text',
145+
parentNode,
146+
new Template(
116147
node.nodeValue,
117148
injection,
118149
parentNode.firstElementChild
119150
? value => (node.nodeValue = value)
120151
: value => (parentNode.innerHTML = value)
121152
)
122-
});
153+
);
123154
},
124155
view: node =>
125-
push.call(this, {
126-
type: 'View',
127-
element: node,
128-
renderer: new View(View.getTemplate(node).trim(), this.data)
129-
})
156+
this.addNode(
157+
'View',
158+
node,
159+
new View(View.getTemplate(node).trim(), this.data)
160+
)
130161
});
131162
}
132163

@@ -135,42 +166,63 @@ export default class View extends Model {
135166
*
136167
* @return {View}
137168
*/
138-
render(data) {
169+
async render(data) {
139170
data = this.patch(data);
140171

141172
const injection = [data, this.scope].concat(
142173
Object.values(this[view_injection])
143174
);
144175

145176
forEach.call(this, ({ type, element, renderer }) => {
146-
switch (type) {
147-
case 'Attr':
148-
case 'Text':
149-
return renderer.evaluate.apply(
150-
renderer,
151-
[element].concat(injection)
152-
);
177+
if (type !== 'View')
178+
renderer.evaluate.apply(renderer, [element].concat(injection));
179+
});
180+
181+
for (let { type, element, renderer, name } of this)
182+
if (type === 'View') {
183+
await nextTick();
184+
185+
await this.renderSub(data[name], element, renderer);
153186
}
187+
}
188+
189+
destroy() {
190+
view_top.get(this).forEach(node => node.remove());
191+
192+
view_top.delete(this);
193+
}
194+
195+
/**
196+
* @protected
197+
*
198+
* @param {Object} data
199+
* @param {Element} element
200+
* @param {View} renderer
201+
*/
202+
async renderSub(data, element, renderer) {
203+
if (!data && data !== null) return;
154204

155-
var _data_ = data[element.dataset.view];
205+
const sub = element_view.get(element);
156206

157-
if (!_data_ && _data_ !== null) return;
207+
if (!data) {
208+
sub.forEach(view => view.destroy());
158209

159-
View.clear(element);
210+
return (sub.length = 0);
211+
}
160212

161-
if (!_data_) return;
213+
if (!(data instanceof Array)) data = [data];
162214

163-
if (!(_data_ instanceof Array)) _data_ = [_data_];
215+
sub.splice(data.length, Infinity).forEach(view => view.destroy());
164216

165-
element.append.apply(
166-
element,
167-
[].concat.apply(
168-
[],
169-
_data_.map(item => renderer.clone().render(item).topNodes)
170-
)
171-
);
172-
});
217+
for (let i = 0; data[i]; i++) {
218+
sub[i] = sub[i] || renderer.clone();
173219

174-
return this;
220+
await sub[i].render(data[i]);
221+
}
222+
223+
element.append.apply(
224+
element,
225+
[].concat.apply([], sub.map(view => view.topNodes))
226+
);
175227
}
176228
}

source/polyfill.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,11 @@ const { window } = new JSDOM('', {
77
pretendToBeVisual: true
88
});
99

10-
for (let key of ['document', 'DocumentFragment', 'DOMParser', 'NodeFilter'])
10+
for (let key of [
11+
'self',
12+
'document',
13+
'DocumentFragment',
14+
'DOMParser',
15+
'NodeFilter'
16+
])
1117
global[key] = window[key];

source/utility.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,10 @@ export function scanTemplate(
8989
}
9090
});
9191
}
92+
93+
/**
94+
* @return {Promise<Number>} https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp
95+
*/
96+
export function nextTick() {
97+
return new Promise(resolve => self.requestAnimationFrame(resolve));
98+
}

0 commit comments

Comments
 (0)