The postings on this site are the contributor's and don’t necessarily represent IBM’s positions, strategies or opinions.
June 22nd, 2008 Alex Moffat Posted in GWT | No Comments »
This post looks at the JavaScript that gets generated when using the new DOM classes introduced in GWT 1.5. It’s interesting to see how how overlay types, inlining and deferred binding work together to give you JavaScript that is faster and more compact than anything you would generally write by hand. I’ve included all of the code for the example at the end of the post.
One of the new feature in GWT 1.5 is overlay types. This was mentioned in several talks at the recent Google IO conference. It’s what makes the new com.google.gwt.dom.client package possible.
The GWT Glossary describes overlay types thus
A Java class that directly models a JavaScript object, giving you the development-time benefits of static types for JavaScript without adding any memory or speed costs at runtime.
An example of an overlay type from the com.google.gwt.dom.client package is Element. This extends Node which in turn extends JavaScriptObject from the com.google.gwt.core.client package. I’m going to use Element’s setInnerText(String text) method to change “Hello World” to “Goodbye World” in the HTML shown below.
<div id="text">
<div>Hello World</div>
</div>
The code that will do this is shown below.
public void onModuleLoad() {
Element el = DOM.getElementById("text");
Element.as(el.getChildNodes().getItem(1)).setInnerText("Goodbye World");
}
There are two interesting aspects to the JavaScript code this produces. First, there are two different output JavaScript files produced depending on whether or not the target browser supports a native innerText property. Second, the generated JavaScript is very compact. No JavaScript wrapper class is needed round the native element to implement the getChildNodes(), and no NodeList wrapper object is created. Here are the important bits of the two versions as compiled in DETAILED mode with the generated names shortened by hand.
This first version, which is served to IE browsers, uses the innerText property. The whole Element.as(el.getChildNodes().getItem(1)).setInnerText("Goodbye World"); line compiles down to el_0.childNodes[1].innerText = $intern_3;. Which is what you’d write by hand if you were only writing for IE, no conditional statements executed at runtime to choose between different implementations based on the browser and no function calls.
var $intern_3 = 'Goodbye World', ...., $intern_2 = 'text';
function init() {
var el_0;
el_0 = $doc.getElementById($intern_2);
el_0.childNodes[1].innerText = $intern_3;
}
The second version, which is served to all other browsers, uses a setInnerText function.
var $intern_3 = 'Goodbye World', ...., $intern_2 = 'text';
function DOM_Impl_$setInnerText(elem, text) {
while (elem.firstChild) {
elem.removeChild(elem.firstChild);
}
if (text != null) {
elem.appendChild($doc.createTextNode(text));
}
}
function init() {
var el_0;
el_0 = $doc.getElementById($intern_2);
DOM_Impl_$setInnerText(el_0.childNodes[1], $intern_3);
}
However, I think we can do better than this for Firefox. The implementation for setInnerText in Element is
public final void setInnerText(String text) {
DOMImpl.impl.setInnerText(this, text);
}
so the code actually delegates to the browser specific DOMImpl created with GWT.create(...) deferred binding so that browser specific code is included at compile time. The base DOMImpl class has a default implementation for setInnerText of
public native void setInnerText(Element elem, String text) /*-{
// Remove all children first.
while (elem.firstChild) {
elem.removeChild(elem.firstChild);
}
// Add a new text node.
if (text != null) {
elem.appendChild($doc.createTextNode(text));
}
}-*/;
You can see that this is where the DOM_Impl_$setInnerText function comes from. This method is overridden only in the DOMImplIE6 class to use the innerText property.
@Override
public native void setInnerText(Element elem, String text) /*-{
elem.innerText = text || '';
}-*/;
In both cases the DOMImpl.impl.setInnerText call is completely removed by inlining during the compilation process. IE gets to use innerText, others use the setInnerText method.
Now, Firefox has a textContent property that works in the same way as innerText so it would be nice to be able to get GWT to use that when creating JavaScript for Firefox. This we can do in two steps. First, create a class called DOMImplMozillaOverride that extends DOMImplMozilla. This class needs to be in the com.google.gwt.dom.client, because DOMImplMozilla has package access, but of course the source can be anywhere. In this class override the setInnerText method to be
@Override
public native void setInnerText(Element element, String s) /*-{
element.textContent = s || '';
}-*/;
The second step is to arrange that DOMImplMozilla gets used by GWT.create(...) when it's looking for an implementation of DOMImpl. This is done in the module file. Looking at the DOM.gwt.xml module file in gwt-user.jar we find
<replace-with class="com.google.gwt.dom.client.DOMImplMozilla">
<when-type-is class="com.google.gwt.dom.client.DOMImpl"/>
<when-property-is name="user.agent" value="gecko1_8"/>
</replace-with>
which chooses the DOMImplMozilla implementation. This can be copied into the module file for our example and DOMImplMozilla replaced with DOMImplMozillaOverride. If the example is recompiled we now get three JavaScript files instead of two. The extra one is for Firefox and uses
el_0.childNodes[1].textContent = $intern_3;
instead of a function call.
The source code for this example is available as a zip file or as a gzipped tar file.
After writing this I found that there is a issue open against GWT Issue 940 to make exactly this change. It seems to be targeted to be fixed in the next 1.5 release candidate.
Leave a Reply