diff --git a/src/babel/transformation/transformers/index.js b/src/babel/transformation/transformers/index.js index 7b169e67a2..3b7cbbc842 100644 --- a/src/babel/transformation/transformers/index.js +++ b/src/babel/transformation/transformers/index.js @@ -18,6 +18,7 @@ export default { "spec.blockScopedFunctions": require("./spec/block-scoped-functions"), + "optimisation.react.constantElements": require("./optimisation/react.constant-elements"), reactCompat: require("./other/react-compat"), react: require("./other/react"), diff --git a/src/babel/transformation/transformers/optimisation/react.constant-elements.js b/src/babel/transformation/transformers/optimisation/react.constant-elements.js new file mode 100644 index 0000000000..03f1524194 --- /dev/null +++ b/src/babel/transformation/transformers/optimisation/react.constant-elements.js @@ -0,0 +1,42 @@ +import * as react from "../../helpers/react"; + +export var metadata = { + optional: true +}; + +var immutabilityVisitor = { + enter(node, parent, scope, state) { + var stop = () => { + state.isImmutable = false; + this.stop(); + }; + + if (this.isJSXClosingElement()) { + this.skip(); + return; + } + + if (this.isJSXIdentifier({ name: "ref" }) && this.parentPath.isJSXAttribute({ name: node })) { + return stop(); + } + + if (this.isJSXIdentifier() || this.isIdentifier() || this.isJSXMemberExpression()) { + return; + } + + if (!this.isImmutable()) stop(); + } +}; + +export function JSXElement(node, parent, scope, file) { + if (node._ignoreConstant) return; + + var state = { isImmutable: true }; + this.traverse(immutabilityVisitor, state); + this.skip(); + + if (state.isImmutable) { + this.hoist(); + node._ignoreConstant = true; + } +} diff --git a/src/babel/traversal/path/hoister.js b/src/babel/traversal/path/hoister.js new file mode 100644 index 0000000000..e47c971051 --- /dev/null +++ b/src/babel/traversal/path/hoister.js @@ -0,0 +1,106 @@ +import * as react from "../../transformation/helpers/react"; +import * as t from "../../types"; + +var referenceVisitor = { + enter(node, parent, scope, state) { + if (this.isJSXIdentifier() && react.isCompatTag(node.name)) { + return; + } + + if (this.isJSXIdentifier() || this.isIdentifier()) { + // direct references that we need to track to hoist this to the highest scope we can + if (this.isReferenced()) { + var bindingInfo = scope.getBinding(node.name); + + if (bindingInfo && bindingInfo.constant) { + state.bindings[node.name] = bindingInfo; + } else { + scope.dump(); + state.foundIncompatible = true; + this.stop(); + } + } + } + } +}; + +export default class PathHoister { + constructor(path) { + this.foundIncompatible = false; + this.bindings = {}; + this.scopes = []; + this.path = path; + } + + isCompatibleScope(scope) { + for (var key in this.bindings) { + var binding = this.bindings[key]; + if (!scope.bindingIdentifierEquals(key, binding.identifier)) { + return false; + } + } + return true; + } + + getCompatibleScopes() { + var checkScope = this.path.scope; + do { + if (this.isCompatibleScope(checkScope)) { + this.scopes.push(checkScope); + } else { + break; + } + } while(checkScope = checkScope.parent); + } + + getAttachmentPath() { + var scopes = this.scopes; + + var scope = scopes.pop(); + + if (scope.path.isFunction()) { + if (this.hasNonParamBindings()) { + // can't be attached to this scope + return this.getNextScopeStatementParent(); + } else { + // needs to be attached to the body + return scope.path.get("body").get("body")[0]; + } + } else if (scope.path.isProgram()) { + return this.getNextScopeStatementParent(); + } + } + + getNextScopeStatementParent() { + var scope = this.scopes.pop(); + if (scope) return scope.path.getStatementParent(); + } + + hasNonParamBindings() { + for (var name in this.bindings) { + var binding = this.bindings[name]; + if (binding.kind !== "param") return true; + } + return false; + } + + run() { + this.path.traverse(referenceVisitor, this); + if (this.foundIncompatible) return; + + this.getCompatibleScopes(); + + var path = this.getAttachmentPath(); + if (!path) return; + + var uid = path.scope.generateUidIdentifier("ref"); + + path.insertBefore([ + t.variableDeclaration("var", [ + t.variableDeclarator(uid, this.path.node) + ]) + ]); + + this.path.replaceWith(uid); + } +} diff --git a/src/babel/traversal/path/index.js b/src/babel/traversal/path/index.js index 54663b5224..46d08294b0 100644 --- a/src/babel/traversal/path/index.js +++ b/src/babel/traversal/path/index.js @@ -635,6 +635,14 @@ export default class TraversalPath { traverse(opts, state) { traverse(this.node, opts, this.scope, state, this); + + /** + * Description + */ + + hoist() { + var hoister = new PathHoister(this); + return hoister.run(); } /** diff --git a/src/babel/types/validators.js b/src/babel/types/validators.js index 20e458cbd6..d532bfaa3b 100644 --- a/src/babel/types/validators.js +++ b/src/babel/types/validators.js @@ -156,11 +156,11 @@ export function isSpecifierDefault(specifier: Object): boolean { export function isScope(node: Object, parent: Object): boolean { if (t.isBlockStatement(node)) { - if (t.isLoop(parent.block, { body: node })) { + if (t.isLoop(parent, { body: node })) { return false; } - if (t.isFunction(parent.block, { body: node })) { + if (t.isFunction(parent, { body: node })) { return false; } } diff --git a/test/core/fixtures/transformation/optimisation-react.constant-elements/constructor/actual.js b/test/core/fixtures/transformation/optimisation-react.constant-elements/constructor/actual.js new file mode 100644 index 0000000000..d8517281fc --- /dev/null +++ b/test/core/fixtures/transformation/optimisation-react.constant-elements/constructor/actual.js @@ -0,0 +1,5 @@ +var Foo = require("Foo"); + +function render() { + return ; +} diff --git a/test/core/fixtures/transformation/optimisation-react.constant-elements/constructor/expected.js b/test/core/fixtures/transformation/optimisation-react.constant-elements/constructor/expected.js new file mode 100644 index 0000000000..7ec3c9a6dd --- /dev/null +++ b/test/core/fixtures/transformation/optimisation-react.constant-elements/constructor/expected.js @@ -0,0 +1,9 @@ +"use strict"; + +var Foo = require("Foo"); + +var _ref = ; + +function render() { + return _ref; +} \ No newline at end of file diff --git a/test/core/fixtures/transformation/optimisation-react.constant-elements/function-parameter/actual.js b/test/core/fixtures/transformation/optimisation-react.constant-elements/function-parameter/actual.js new file mode 100644 index 0000000000..7978e91d06 --- /dev/null +++ b/test/core/fixtures/transformation/optimisation-react.constant-elements/function-parameter/actual.js @@ -0,0 +1,13 @@ +function render(text) { + return function () { + return {text}; + }; +} + +var Foo2 = require("Foo"); + +function createComponent(text) { + return function render() { + return {text}; + }; +} diff --git a/test/core/fixtures/transformation/optimisation-react.constant-elements/function-parameter/expected.js b/test/core/fixtures/transformation/optimisation-react.constant-elements/function-parameter/expected.js new file mode 100644 index 0000000000..6f4e3c43ec --- /dev/null +++ b/test/core/fixtures/transformation/optimisation-react.constant-elements/function-parameter/expected.js @@ -0,0 +1,19 @@ +"use strict"; + +function render(text) { + var _ref = {text}; + + return function () { + return _ref; + }; +} + +var Foo2 = require("Foo"); + +function createComponent(text) { + var _ref = {text}; + + return function render() { + return _ref; + }; +} \ No newline at end of file diff --git a/test/core/fixtures/transformation/optimisation-react.constant-elements/html-element/actual.js b/test/core/fixtures/transformation/optimisation-react.constant-elements/html-element/actual.js new file mode 100644 index 0000000000..6c078f681c --- /dev/null +++ b/test/core/fixtures/transformation/optimisation-react.constant-elements/html-element/actual.js @@ -0,0 +1,7 @@ +function render() { + return ; +} + +function render() { + return
; +} diff --git a/test/core/fixtures/transformation/optimisation-react.constant-elements/html-element/expected.js b/test/core/fixtures/transformation/optimisation-react.constant-elements/html-element/expected.js new file mode 100644 index 0000000000..1558b57492 --- /dev/null +++ b/test/core/fixtures/transformation/optimisation-react.constant-elements/html-element/expected.js @@ -0,0 +1,13 @@ +"use strict"; + +var _ref = ; + +function render() { + return _ref; +} + +var _ref =
; + +function render() { + return _ref; +} \ No newline at end of file diff --git a/test/core/fixtures/transformation/optimisation-react.constant-elements/inner-declaration/actual.js b/test/core/fixtures/transformation/optimisation-react.constant-elements/inner-declaration/actual.js new file mode 100644 index 0000000000..bdbd3103f6 --- /dev/null +++ b/test/core/fixtures/transformation/optimisation-react.constant-elements/inner-declaration/actual.js @@ -0,0 +1,6 @@ +function render() { + var text = getText(); + return function () { + return {text}; + }; +} diff --git a/test/core/fixtures/transformation/optimisation-react.constant-elements/inner-declaration/expected.js b/test/core/fixtures/transformation/optimisation-react.constant-elements/inner-declaration/expected.js new file mode 100644 index 0000000000..2ad2f041c5 --- /dev/null +++ b/test/core/fixtures/transformation/optimisation-react.constant-elements/inner-declaration/expected.js @@ -0,0 +1,11 @@ +"use strict"; + +function render() { + var text = getText(); + + var _ref = {text}; + + return function () { + return _ref; + }; +} diff --git a/test/core/fixtures/transformation/optimisation-react.constant-elements/options.json b/test/core/fixtures/transformation/optimisation-react.constant-elements/options.json new file mode 100644 index 0000000000..000e693290 --- /dev/null +++ b/test/core/fixtures/transformation/optimisation-react.constant-elements/options.json @@ -0,0 +1,5 @@ +{ + "noCheckAst": true, + "optional": ["optimisation.react.constantElements"], + "blacklist": ["react"] +} diff --git a/test/core/fixtures/transformation/optimisation-react.constant-elements/ref-deopt/actual.js b/test/core/fixtures/transformation/optimisation-react.constant-elements/ref-deopt/actual.js new file mode 100644 index 0000000000..21806690b6 --- /dev/null +++ b/test/core/fixtures/transformation/optimisation-react.constant-elements/ref-deopt/actual.js @@ -0,0 +1,3 @@ +function render() { + return ; +} diff --git a/test/core/fixtures/transformation/optimisation-react.constant-elements/ref-deopt/expected.js b/test/core/fixtures/transformation/optimisation-react.constant-elements/ref-deopt/expected.js new file mode 100644 index 0000000000..6135a207de --- /dev/null +++ b/test/core/fixtures/transformation/optimisation-react.constant-elements/ref-deopt/expected.js @@ -0,0 +1,5 @@ +"use strict"; + +function render() { + return ; +} \ No newline at end of file diff --git a/test/core/fixtures/transformation/optimisation-react.constant-elements/spread-deopt/actual.js b/test/core/fixtures/transformation/optimisation-react.constant-elements/spread-deopt/actual.js new file mode 100644 index 0000000000..baeabc5749 --- /dev/null +++ b/test/core/fixtures/transformation/optimisation-react.constant-elements/spread-deopt/actual.js @@ -0,0 +1,3 @@ +function render() { + return ; +} diff --git a/test/core/fixtures/transformation/optimisation-react.constant-elements/spread-deopt/expected.js b/test/core/fixtures/transformation/optimisation-react.constant-elements/spread-deopt/expected.js new file mode 100644 index 0000000000..17cdefb642 --- /dev/null +++ b/test/core/fixtures/transformation/optimisation-react.constant-elements/spread-deopt/expected.js @@ -0,0 +1,5 @@ +"use strict"; + +function render() { + return ; +} \ No newline at end of file