Skip to content

Commit

Permalink
Issue #28: runtime performance improvement by generate react literals
Browse files Browse the repository at this point in the history
  • Loading branch information
Philippe Elsass committed Apr 17, 2016
1 parent e0b7af9 commit 6fee07c
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 57 deletions.
4 changes: 2 additions & 2 deletions samples/todoapp/bin/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<body>
<link rel="stylesheet" href="styles.css"/>
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.5/react-with-addons.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.5/react-dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react-with-addons.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react-dom.js"></script>
<script src="index.js"></script>
</body>
106 changes: 84 additions & 22 deletions samples/todoapp/bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ function $extend(from, fields) {
var Main = function() { };
Main.__name__ = true;
Main.main = function() {
ReactDOM.render(React.createElement(view_TodoApp,null),window.document.getElementById("app"));
ReactDOM.render({ '$$typeof' : $$tre, type : view_TodoApp, props : null},window.document.getElementById("app"));
};
Math.__name__ = true;
var Reflect = function() { };
Expand All @@ -21,18 +21,82 @@ Reflect.compareMethods = function(f1,f2) {
if(!Reflect.isFunction(f1) || !Reflect.isFunction(f2)) return false;
return f1.scope == f2.scope && f1.method == f2.method && f1.method != null;
};
var Std = function() { };
Std.__name__ = true;
Std.string = function(s) {
return js_Boot.__string_rec(s,"");
};
var api_react_ReactMacro = function() { };
api_react_ReactMacro.__name__ = true;
var js__$Boot_HaxeError = function(val) {
Error.call(this);
this.val = val;
this.message = String(val);
if(Error.captureStackTrace) Error.captureStackTrace(this,js__$Boot_HaxeError);
var js_Boot = function() { };
js_Boot.__name__ = true;
js_Boot.__string_rec = function(o,s) {
if(o == null) return "null";
if(s.length >= 5) return "<...>";
var t = typeof(o);
if(t == "function" && (o.__name__ || o.__ename__)) t = "object";
switch(t) {
case "object":
if(o instanceof Array) {
if(o.__enum__) {
if(o.length == 2) return o[0];
var str2 = o[0] + "(";
s += "\t";
var _g1 = 2;
var _g = o.length;
while(_g1 < _g) {
var i1 = _g1++;
if(i1 != 2) str2 += "," + js_Boot.__string_rec(o[i1],s); else str2 += js_Boot.__string_rec(o[i1],s);
}
return str2 + ")";
}
var l = o.length;
var i;
var str1 = "[";
s += "\t";
var _g2 = 0;
while(_g2 < l) {
var i2 = _g2++;
str1 += (i2 > 0?",":"") + js_Boot.__string_rec(o[i2],s);
}
str1 += "]";
return str1;
}
var tostr;
try {
tostr = o.toString;
} catch( e ) {
return "???";
}
if(tostr != null && tostr != Object.toString && typeof(tostr) == "function") {
var s2 = o.toString();
if(s2 != "[object Object]") return s2;
}
var k = null;
var str = "{\n";
s += "\t";
var hasp = o.hasOwnProperty != null;
for( var k in o ) {
if(hasp && !o.hasOwnProperty(k)) {
continue;
}
if(k == "prototype" || k == "__class__" || k == "__super__" || k == "__interfaces__" || k == "__properties__") {
continue;
}
if(str.length != 2) str += ", \n";
str += s + k + " : " + js_Boot.__string_rec(o[k],s);
}
s = s.substring(1);
str += "\n" + s + "}";
return str;
case "function":
return "<function>";
case "string":
return o;
default:
return String(o);
}
};
js__$Boot_HaxeError.__name__ = true;
js__$Boot_HaxeError.__super__ = Error;
js__$Boot_HaxeError.prototype = $extend(Error.prototype,{
});
var msignal_Signal = function(valueClasses) {
if(valueClasses == null) valueClasses = [];
this.valueClasses = valueClasses;
Expand Down Expand Up @@ -79,7 +143,6 @@ msignal_Signal.prototype = {
if(!this.slots.nonEmpty) return true;
var existingSlot = this.slots.find(listener);
if(existingSlot == null) return true;
if(existingSlot.once != once) throw new js__$Boot_HaxeError("You cannot addOnce() then add() the same listener without removing the relationship first.");
return false;
}
,createSlot: function(listener,once,priority) {
Expand Down Expand Up @@ -163,7 +226,6 @@ msignal_Slot.prototype = {
this.signal.remove(this.listener);
}
,set_listener: function(value) {
if(value == null) throw new js__$Boot_HaxeError("listener cannot be null");
return this.listener = value;
}
};
Expand Down Expand Up @@ -214,10 +276,8 @@ msignal_Slot2.prototype = $extend(msignal_Slot.prototype,{
});
var msignal_SlotList = function(head,tail) {
this.nonEmpty = false;
if(head == null && tail == null) {
if(msignal_SlotList.NIL != null) throw new js__$Boot_HaxeError("Parameters head and tail are null. Use the NIL element instead.");
this.nonEmpty = false;
} else if(head == null) throw new js__$Boot_HaxeError("Parameter head cannot be null."); else {
if(head == null && tail == null) this.nonEmpty = false; else if(head == null) {
} else {
this.head = head;
if(tail == null) this.tail = msignal_SlotList.NIL; else this.tail = tail;
this.nonEmpty = true;
Expand Down Expand Up @@ -358,7 +418,10 @@ view_TodoApp.prototype = $extend(React.Component.prototype,{
return !item.checked;
}).length;
var listProps = { data : this.state.items};
return React.createElement("div",{ style : { margin : "10px"}, className : "app"},React.createElement("div",{ className : "header"},React.createElement("input",{ ref : "input", placeholder : "Enter new task description"}),React.createElement("button",{ onClick : $bind(this,this.addItem), className : "button-add"},"+")),React.createElement(view_TodoList,React.__spread({ },listProps)),React.createElement("div",{ className : "footer"},unchecked," task(s) left"));
return { '$$typeof' : $$tre, type : "div", props : { style : { margin : "10px"}, className : "app", children : [{ '$$typeof' : $$tre, type : "div", props : { className : "header", children : [React.createElement("input",{ ref : "input", placeholder : "Enter new task description"}),{ '$$typeof' : $$tre, type : "button", props : { onClick : $bind(this,this.addItem), className : "button-add", children : ["+"]}}]}},{ '$$typeof' : $$tre, type : view_TodoList, props : Object.assign({ },listProps), ref : $bind(this,this.mountList)},{ '$$typeof' : $$tre, type : "div", props : { className : "footer", children : [unchecked," task(s) left"]}}]}};
}
,mountList: function(comp) {
console.log("List mounted " + Std.string(comp.props));
}
,addItem: function() {
var text = this.refs.input.value;
Expand All @@ -375,7 +438,7 @@ view_TodoList.__name__ = true;
view_TodoList.__super__ = React.Component;
view_TodoList.prototype = $extend(React.Component.prototype,{
render: function() {
return React.createElement("ul",{ onClick : $bind(this,this.toggleChecked), className : "list"},this.createChildren());
return { '$$typeof' : $$tre, type : "ul", props : { onClick : $bind(this,this.toggleChecked), className : "list", children : [this.createChildren()]}};
}
,createChildren: function() {
var _g = [];
Expand All @@ -384,7 +447,7 @@ view_TodoList.prototype = $extend(React.Component.prototype,{
while(_g1 < _g2.length) {
var entry = _g2[_g1];
++_g1;
_g.push(React.createElement(view_TodoListItem,{ data : entry, key : entry.id}));
_g.push({ '$$typeof' : $$tre, type : view_TodoListItem, props : { data : entry}, key : entry.id});
}
return _g;
}
Expand All @@ -409,7 +472,7 @@ view_TodoListItem.prototype = $extend(React.Component.prototype,{
var entry = this.props.data;
this.checked = entry.checked;
var id = "item-" + entry.id;
return React.createElement("li",{ id : id, className : this.checked?"checked":""},entry.label);
return { '$$typeof' : $$tre, type : "li", props : { id : id, className : this.checked?"checked":"", children : [entry.label]}};
}
});
var $_, $fid = 0;
Expand Down Expand Up @@ -437,6 +500,7 @@ if(Array.prototype.filter == null) Array.prototype.filter = function(f1) {
}
return a1;
};
var $$tre = (typeof Symbol === "function" && Symbol.for && Symbol.for("react.element")) || 0xeac7;
msignal_SlotList.NIL = new msignal_SlotList(null,null);
store_TodoActions.addItem = new msignal_Signal1();
store_TodoActions.toggleItem = new msignal_Signal1();
Expand All @@ -445,5 +509,3 @@ view_TodoList.displayName = "TodoList";
view_TodoListItem.displayName = "TodoListItem";
Main.main();
})(typeof console != "undefined" ? console : {log:function(){}});

//# sourceMappingURL=index.js.map
5 changes: 3 additions & 2 deletions samples/todoapp/build.hxml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
-js bin/index.js
-cp src
-main Main
-lib react
-lib msignal
-D react_global
-js bin/index.js
-debug
#-D react_no_inline
#-debug
7 changes: 6 additions & 1 deletion samples/todoapp/src/view/TodoApp.hx
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,17 @@ class TodoApp extends ReactComponentOfStateAndRefs<TodoAppState, TodoAppRefs>
<input ref="input" placeholder="Enter new task description" />
<button className="button-add" onClick=$addItem>+</button>
</div>
<$TodoList {...listProps}/>
<$TodoList ref={mountList} {...listProps}/>
<div className="footer">$unchecked task(s) left</div>
</div>
');
}

function mountList(comp:ReactComponent)
{
trace('List mounted ' + comp.props);
}

function addItem()
{
var text = refs.input.value;
Expand Down
127 changes: 97 additions & 30 deletions src/lib/api/react/ReactMacro.hx
Original file line number Diff line number Diff line change
Expand Up @@ -163,34 +163,107 @@ class ReactMacro

static function parseJsxNode(xml:Xml, pos:Position)
{
var args = [];

// parse type
var path = xml.nodeName.split('.');
var last = path[path.length - 1];
if (path.length == 1 && last.charAt(0) == last.charAt(0).toLowerCase()) args.push(macro $v{path[0]});
else args.push(macro $p{path});
var isHtml = path.length == 1 && last.charAt(0) == last.charAt(0).toLowerCase();
var type = isHtml ? macro $v{path[0]} : macro $p{path};

// parse attributes
var attrs = [];
var spread = [];
var key = null;
var ref = null;
for (attr in xml.attributes())
{
var value = xml.get(attr);
var expr = parseJsxExpr(value, pos);
if (attr.charAt(0) == '.') spread.push(expr);
if (attr == 'key') key = expr;
else if (attr == 'ref') ref = expr;
else if (attr.charAt(0) == '.') spread.push(expr);
else attrs.push({field:attr, expr:expr});
}
if (spread.length > 0)

// parse children
var children = parseChildren(xml, pos);

// inline declaration or createElement?
#if (!debug && !react_no_inline)
var useLiteral = ref == null || canUseLiteral(ref);
#else
var useLiteral = false;
#end

if (useLiteral)
{
args.push(makeSpread(spread, attrs, pos));
if (children.length > 0) attrs.push({field:'children', expr:macro $a{children}});

var props = makeProps(spread, attrs, pos);

// TODO could not get EObjectDecl to generare the $$typeof field
if (key == null && ref == null) return macro untyped {
"$$typeof": untyped __js__("$$tre"),
type: $type,
props: $props
}
else if (ref == null) return macro untyped {
"$$typeof": untyped __js__("$$tre"),
type: $type,
props: $props,
key: $key
}
else if (key == null) return macro untyped {
"$$typeof": untyped __js__("$$tre"),
type: $type,
props: $props,
ref: $ref
}
else return macro untyped {
"$$typeof": untyped __js__("$$tre"),
type: $type,
props: $props,
key: $key,
ref: $ref
}
}
else
else
{
if (attrs.length == 0) args.push(macro null);
else args.push({pos:pos, expr:EObjectDecl(attrs)});
if (ref != null) attrs.unshift({field:'ref', expr:ref});
if (key != null) attrs.unshift({field:'key', expr:key});

var props = makeProps(spread, attrs, pos);

var args = [type, props].concat(children);
return macro api.react.React.createElement($a{args});
}

}

static function canUseLiteral(ref:Expr)
{
// only refs as functions are allowed in literals, strings require the full createElement context
return switch (Context.typeof(ref)) {
case TFun(_): true;
default: false;
}
}

static function makeProps(spread:Array<Expr>, attrs:Array<{field:String, expr:Expr}>, pos:Position)
{
return spread.length > 0
? makeSpread(spread, attrs, pos)
: attrs.length == 0 ? macro null : {pos:pos, expr:EObjectDecl(attrs)}
}

static function makeSpread(spread:Array<Expr>, attrs:Array<{field:String, expr:Expr}>, pos:Position)
{
var args = [macro {}].concat(spread);
if (attrs.length > 0) args.push({pos:pos, expr:EObjectDecl(attrs)});
return macro untyped Object.assign($a{args});
}

static function parseChildren(xml:Xml, pos:Position)
{
var children = [];
for (node in xml)
{
if (node.nodeType == Xml.PCData)
Expand All @@ -205,37 +278,24 @@ class ReactMacro
if (line.length == 0) continue;
~/([^{]+|\{[^}]+\})/g.map(line, function (e){
var token = e.matched(0);
args.push(parseJsxExpr(token, pos));
children.push(parseJsxExpr(token, pos));
return '';
});
}
}
else if (node.nodeType == Xml.Element)
{
args.push(parseJsxNode(node, pos));
children.push(parseJsxNode(node, pos));
}

}
return macro api.react.React.createElement($a{args});
}

static function makeSpread(spread:Array<Expr>, attrs:Array<{field:String, expr:Expr}>, pos:Position)
{
var args = [macro {}].concat(spread);
if (attrs.length > 0) args.push({pos:pos, expr:EObjectDecl(attrs)});
return macro untyped api.react.React.__spread($a{args});
return children;
}

static function parseJsxExpr(value:String, pos:Position)
{
return if (value.charAt(0) == '{' && value.charAt(value.length - 1) == '}')
{
Context.parse(value.substr(1, value.length - 2), pos);
}
else
{
macro $v{value};
}
return value.charAt(0) == '{' && value.charAt(value.length - 1) == '}'
? Context.parse(value.substr(1, value.length - 2), pos)
: macro $v{value};
}

public static function setDisplayName()
Expand All @@ -258,4 +318,11 @@ class ReactMacro
return fields;
}
#end

#if (js && !react_no_inline)
static function __init__() {
// required magic value to tag literal react elements
untyped __js__("var $$tre = (typeof Symbol === \"function\" && Symbol.for && Symbol.for(\"react.element\")) || 0xeac7");
}
#end
}

0 comments on commit 6fee07c

Please sign in to comment.