From 6fee07c65a57afc13310e838f3f5fe7e5e96a294 Mon Sep 17 00:00:00 2001 From: Philippe Elsass Date: Sun, 17 Apr 2016 21:04:56 +0100 Subject: [PATCH] Issue #28: runtime performance improvement by generate react literals --- samples/todoapp/bin/index.html | 4 +- samples/todoapp/bin/index.js | 106 ++++++++++++++++++----- samples/todoapp/build.hxml | 5 +- samples/todoapp/src/view/TodoApp.hx | 7 +- src/lib/api/react/ReactMacro.hx | 127 +++++++++++++++++++++------- 5 files changed, 192 insertions(+), 57 deletions(-) diff --git a/samples/todoapp/bin/index.html b/samples/todoapp/bin/index.html index 0eba9ff..89686d6 100644 --- a/samples/todoapp/bin/index.html +++ b/samples/todoapp/bin/index.html @@ -2,7 +2,7 @@
- - + + \ No newline at end of file diff --git a/samples/todoapp/bin/index.js b/samples/todoapp/bin/index.js index 0c4455f..1584323 100644 --- a/samples/todoapp/bin/index.js +++ b/samples/todoapp/bin/index.js @@ -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() { }; @@ -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 ""; + 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; @@ -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) { @@ -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; } }; @@ -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; @@ -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; @@ -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 = []; @@ -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; } @@ -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; @@ -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(); @@ -445,5 +509,3 @@ view_TodoList.displayName = "TodoList"; view_TodoListItem.displayName = "TodoListItem"; Main.main(); })(typeof console != "undefined" ? console : {log:function(){}}); - -//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/samples/todoapp/build.hxml b/samples/todoapp/build.hxml index e1a7dae..855f289 100644 --- a/samples/todoapp/build.hxml +++ b/samples/todoapp/build.hxml @@ -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 diff --git a/samples/todoapp/src/view/TodoApp.hx b/samples/todoapp/src/view/TodoApp.hx index aa93c3b..6775355 100644 --- a/samples/todoapp/src/view/TodoApp.hx +++ b/samples/todoapp/src/view/TodoApp.hx @@ -41,12 +41,17 @@ class TodoApp extends ReactComponentOfStateAndRefs - <$TodoList {...listProps}/> + <$TodoList ref={mountList} {...listProps}/>
$unchecked task(s) left
'); } + function mountList(comp:ReactComponent) + { + trace('List mounted ' + comp.props); + } + function addItem() { var text = refs.input.value; diff --git a/src/lib/api/react/ReactMacro.hx b/src/lib/api/react/ReactMacro.hx index 24190a0..dbaebf0 100644 --- a/src/lib/api/react/ReactMacro.hx +++ b/src/lib/api/react/ReactMacro.hx @@ -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, 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, 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) @@ -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, 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() @@ -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 }