add a path hoisting mechanism that will hoist a node to it's highest compatible scope, a compatible scope is considered to be one where all references inside can be resolved to, also adds an optimisation.react.constantElements transformer that uses this to much success facebook/react#3226
This commit is contained in:
parent
6a698f7ae4
commit
52c6fe2bc1
@ -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"),
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
106
src/babel/traversal/path/hoister.js
Normal file
106
src/babel/traversal/path/hoister.js
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
var Foo = require("Foo");
|
||||
|
||||
function render() {
|
||||
return <Foo />;
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
"use strict";
|
||||
|
||||
var Foo = require("Foo");
|
||||
|
||||
var _ref = <Foo />;
|
||||
|
||||
function render() {
|
||||
return _ref;
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
function render(text) {
|
||||
return function () {
|
||||
return <foo>{text}</foo>;
|
||||
};
|
||||
}
|
||||
|
||||
var Foo2 = require("Foo");
|
||||
|
||||
function createComponent(text) {
|
||||
return function render() {
|
||||
return <Foo2>{text}</Foo2>;
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
"use strict";
|
||||
|
||||
function render(text) {
|
||||
var _ref = <foo>{text}</foo>;
|
||||
|
||||
return function () {
|
||||
return _ref;
|
||||
};
|
||||
}
|
||||
|
||||
var Foo2 = require("Foo");
|
||||
|
||||
function createComponent(text) {
|
||||
var _ref = <Foo2>{text}</Foo2>;
|
||||
|
||||
return function render() {
|
||||
return _ref;
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
function render() {
|
||||
return <foo />;
|
||||
}
|
||||
|
||||
function render() {
|
||||
return <div className="foo"><input type="checkbox" checked={true} /></div>;
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
"use strict";
|
||||
|
||||
var _ref = <foo />;
|
||||
|
||||
function render() {
|
||||
return _ref;
|
||||
}
|
||||
|
||||
var _ref = <div className="foo"><input type="checkbox" checked={true} /></div>;
|
||||
|
||||
function render() {
|
||||
return _ref;
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
function render() {
|
||||
var text = getText();
|
||||
return function () {
|
||||
return <foo>{text}</foo>;
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
"use strict";
|
||||
|
||||
function render() {
|
||||
var text = getText();
|
||||
|
||||
var _ref = <foo>{text}</foo>;
|
||||
|
||||
return function () {
|
||||
return _ref;
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
{
|
||||
"noCheckAst": true,
|
||||
"optional": ["optimisation.react.constantElements"],
|
||||
"blacklist": ["react"]
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
function render() {
|
||||
return <foo ref="foobar" />;
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
function render() {
|
||||
return <foo ref="foobar" />;
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
function render() {
|
||||
return <foo {...foobar} />;
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
function render() {
|
||||
return <foo {...foobar} />;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user