Skip to content

Commit 7a1137f

Browse files
authored
Merge branch 'master' into issue-118
2 parents 9930aff + 75fbb2e commit 7a1137f

File tree

4 files changed

+103
-52
lines changed

4 files changed

+103
-52
lines changed

packages/babel-plugin-htm/README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ A Babel plugin that compiles [htm] syntax to hyperscript, React.createElement, o
44

55
## Usage
66

7-
Basic usage:
7+
In your Babel configuration (`.babelrc`, `babel.config.js`, `"babel"` field in package.json, etc), add the plugin:
88

99
```js
10-
[
11-
["htm", {
12-
"pragma": "React.createElement"
13-
}]
14-
]
10+
{
11+
"plugins": [
12+
["htm", {
13+
"pragma": "React.createElement"
14+
}]
15+
]
16+
}
1517
```
1618

1719
```js

src/build.mjs

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ const MODE_COMMENT = 4;
88
const MODE_PROP_SET = 5;
99
const MODE_PROP_APPEND = 6;
1010

11-
const TAG_SET = 1;
1211
const CHILD_APPEND = 0;
1312
const CHILD_RECURSE = 2;
14-
const PROPS_ASSIGN = 3;
13+
const TAG_SET = 3;
14+
const PROPS_ASSIGN = 4;
1515
const PROP_SET = MODE_PROP_SET;
1616
const PROP_APPEND = MODE_PROP_APPEND;
1717

@@ -36,30 +36,30 @@ export const treeify = (built, fields) => {
3636
const children = [];
3737

3838
for (let i = 1; i < built.length; i++) {
39-
const field = built[i++];
40-
const value = typeof field === 'number' ? fields[field - 1] : field;
39+
const type = built[i++];
40+
const value = built[i] ? fields[built[i++]-1] : built[++i];
4141

42-
if (built[i] === TAG_SET) {
42+
if (type === TAG_SET) {
4343
tag = value;
4444
}
45-
else if (built[i] === PROPS_ASSIGN) {
45+
else if (type === PROPS_ASSIGN) {
4646
props.push(value);
4747
currentProps = null;
4848
}
49-
else if (built[i] === PROP_SET) {
49+
else if (type === PROP_SET) {
5050
if (!currentProps) {
5151
currentProps = Object.create(null);
5252
props.push(currentProps);
5353
}
5454
currentProps[built[++i]] = [value];
5555
}
56-
else if (built[i] === PROP_APPEND) {
56+
else if (type === PROP_APPEND) {
5757
currentProps[built[++i]].push(value);
5858
}
59-
else if (built[i] === CHILD_RECURSE) {
59+
else if (type === CHILD_RECURSE) {
6060
children.push(_treeify(value));
6161
}
62-
else if (built[i] === CHILD_APPEND) {
62+
else if (type === CHILD_APPEND) {
6363
children.push(value);
6464
}
6565
}
@@ -70,12 +70,20 @@ export const treeify = (built, fields) => {
7070
return children.length > 1 ? children : children[0];
7171
};
7272

73-
7473
export const evaluate = (h, built, fields, args) => {
74+
let tmp;
75+
76+
// `build()` used the first element of the operation list as
77+
// temporary workspace. Now that `build()` is done we can use
78+
// that space to track whether the current element is "dynamic"
79+
// (i.e. it or any of its descendants depend on dynamic values).
80+
built[0] = 0;
81+
7582
for (let i = 1; i < built.length; i++) {
76-
const field = built[i];
77-
const value = typeof field === 'number' ? fields[field] : field;
78-
const type = built[++i];
83+
const type = built[i++];
84+
85+
// Set `built[0]` to truthy if this element depends on a dynamic value.
86+
const value = built[i] ? fields[built[0] = built[i++]] : built[++i];
7987

8088
if (type === TAG_SET) {
8189
args[0] = value;
@@ -90,11 +98,26 @@ export const evaluate = (h, built, fields, args) => {
9098
args[1][built[++i]] += (value + '');
9199
}
92100
else if (type) {
93-
// code === CHILD_RECURSE
94-
args.push(h.apply(null, evaluate(h, value, fields, ['', null])));
101+
// type === CHILD_RECURSE
102+
tmp = h.apply(0, evaluate(h, value, fields, ['', null]));
103+
args.push(tmp);
104+
105+
if (value[0]) {
106+
// If the child element is dynamic, then so is the current element.
107+
built[0] = 1;
108+
}
109+
else {
110+
// Rewrite the operation list in-place if the child element is static.
111+
// The currently evaluated piece `CHILD_RECURSE, 0, [...]` becomes
112+
// `CHILD_APPEND, 0, tmp`.
113+
// Essentially the operation list gets optimized for potential future
114+
// re-evaluations.
115+
built[i-2] = CHILD_APPEND;
116+
built[i] = tmp;
117+
}
95118
}
96119
else {
97-
// code === CHILD_APPEND
120+
// type === CHILD_APPEND
98121
args.push(value);
99122
}
100123
}
@@ -118,15 +141,15 @@ export const build = function(statics) {
118141
current.push(field ? fields[field] : buffer);
119142
}
120143
else {
121-
current.push(field || buffer, CHILD_APPEND);
144+
current.push(CHILD_APPEND, field, buffer);
122145
}
123146
}
124147
else if (mode === MODE_TAGNAME && (field || buffer)) {
125148
if (MINI) {
126149
current[1] = field ? fields[field] : buffer;
127150
}
128151
else {
129-
current.push(field || buffer, TAG_SET);
152+
current.push(TAG_SET, field, buffer);
130153
}
131154
mode = MODE_WHITESPACE;
132155
}
@@ -135,15 +158,15 @@ export const build = function(statics) {
135158
current[2] = Object.assign(current[2] || {}, fields[field]);
136159
}
137160
else {
138-
current.push(field, PROPS_ASSIGN);
161+
current.push(PROPS_ASSIGN, field, 0);
139162
}
140163
}
141164
else if (mode === MODE_WHITESPACE && buffer && !field) {
142165
if (MINI) {
143166
(current[2] = current[2] || {})[buffer] = true;
144167
}
145168
else {
146-
current.push(true, PROP_SET, buffer);
169+
current.push(PROP_SET, 0, true, buffer);
147170
}
148171
}
149172
else if (mode >= MODE_PROP_SET) {
@@ -158,11 +181,11 @@ export const build = function(statics) {
158181
}
159182
else {
160183
if (buffer || (!field && mode === MODE_PROP_SET)) {
161-
current.push(buffer, mode, propName);
184+
current.push(mode, 0, buffer, propName);
162185
mode = MODE_PROP_APPEND;
163186
}
164187
if (field) {
165-
current.push(field, mode, propName);
188+
current.push(mode, field, 0, propName);
166189
mode = MODE_PROP_APPEND;
167190
}
168191
}
@@ -241,7 +264,7 @@ export const build = function(statics) {
241264
(current = current[0]).push(h.apply(null, mode.slice(1)));
242265
}
243266
else {
244-
(current = current[0]).push(mode, CHILD_RECURSE);
267+
(current = current[0]).push(CHILD_RECURSE, 0, mode);
245268
}
246269
mode = MODE_SLASH;
247270
}

src/index.mjs

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,16 @@
1414
import { MINI } from './constants.mjs';
1515
import { build, evaluate } from './build.mjs';
1616

17-
const getCacheMap = (statics) => {
18-
let tpl = CACHE.get(statics);
19-
if (!tpl) {
20-
CACHE.set(statics, tpl = build(statics));
21-
}
22-
return tpl;
23-
};
17+
const CACHES = new Map();
2418

25-
const getCacheKeyed = (statics) => {
26-
let key = '';
27-
for (let i = 0; i < statics.length; i++) {
28-
key += statics[i].length + '-' + statics[i];
19+
const regular = function(statics) {
20+
let tmp = CACHES.get(this);
21+
if (!tmp) {
22+
tmp = new Map();
23+
CACHES.set(this, tmp);
2924
}
30-
return CACHE[key] || (CACHE[key] = build(statics));
31-
};
32-
33-
const USE_MAP = !MINI && typeof Map === 'function';
34-
const CACHE = USE_MAP ? new Map() : {};
35-
const getCache = USE_MAP ? getCacheMap : getCacheKeyed;
36-
37-
const cached = function(statics) {
38-
const res = evaluate(this, getCache(statics), arguments, []);
39-
return res.length > 1 ? res : res[0];
25+
tmp = evaluate(this, tmp.get(statics) || (tmp.set(statics, tmp = build(statics)), tmp), arguments, []);
26+
return tmp.length > 1 ? tmp : tmp[0];
4027
};
4128

42-
export default MINI ? build : cached;
29+
export default MINI ? build : regular;

test/statics-caching.test.mjs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Copyright 2018 Google Inc. All Rights Reserved.
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*/
13+
14+
import htm from '../src/index.mjs';
15+
16+
const h = (tag, props, ...children) => ({ tag, props, children });
17+
const html = htm.bind(h);
18+
19+
describe('htm', () => {
20+
test('should cache static subtrees', () => {
21+
const x = () => html`<div>a</div>`;
22+
const a = x();
23+
const b = x();
24+
expect(a).toEqual({ tag: 'div', props: null, children: ['a'] });
25+
expect(b).toEqual({ tag: 'div', props: null, children: ['a'] });
26+
expect(a).toBe(b);
27+
});
28+
29+
test('should have a different cache for each h', () => {
30+
let tmp = htm.bind(() => 1);
31+
const x = () => tmp`<div>a</div>`;
32+
const a = x();
33+
tmp = htm.bind(() => 2);
34+
const b = x();
35+
36+
expect(a).toBe(1);
37+
expect(b).toBe(2);
38+
});
39+
});

0 commit comments

Comments
 (0)