Attention: Shared Google-externally
Author: gsathya@chromium.org
Status: final
Tracking bug: v8:5785
Last updated: 2017/08/20
Dynamic import is a new language feature that allows asynchronous loading of ES6 Modules. It returns a promise for the module namespace object of the requested module, which is created after fetching, instantiating, and evaluating all of the module's dependencies, as well as the module itself.
button.addEventListener('click', event => {
import('./dialogBox.js')
.then(dialogBox => {
dialogBox.open();
})
.catch(error => {
/* Error handling */
})
});
A new ImportCallExpression ASTnode is used to store the module specifier. In the bytecode generator, we call out to a runtime function DynamicImportCall which takes the module specifier and the closure function as arguments.
The script or module url is obtained from the Script object present in the SharedFunctionInfo of the closure function. A new promise is created and returned from the runtime call. HostImportModuleDynamicallyCallback is also called to initiate the module loading.
Note: The resource_name in v8::ScriptOrigin passed to the v8::Source is the url.
typedef void (*HostImportModuleDynamicallyCallback)(
Isolate* isolate, Local<String> referrer, Local<String> specifier,
Local<DynamicImportResult> result);
This HostImportModuleDynamicallyCallback is set up as part of creating the isolate through Isolate::CreateParams.
V8 calls out to this when a new module is dynamically imported. DynamicImportResult is used to fulfil or reject the import with a module record or an exception, by calling its appropriate methods. Note: DynamicImportResult is just a reinterpret cast of i::JSPromise.
class V8_EXPORT DynamicImportResult {
public:
bool FinishDynamicImportSuccess(Local<Context> context,
Local<Module> module);
bool FinishDynamicImportFailure(Local<Context> context,
Local<Value> exception);
};
FinishDynamicImportSuccess obtains the module namespace object and resolves the promise with it. FinishDynamicImportFailure rejects the promise with the given exception.
Update: This is now the implemented API along with some changes described below in the V8 API: Version 3 section.
Instead of exposing a bespoke DynamicImportResult type that we reinterpret cast from a i::JSPromise, the embedder can create a promise that we return to userland. There would be no need for DynamicImportResult.
typedef Local<Promise> (*HostImportModuleDynamicallyCallback)(
Local<Context> context, Local<String> referrer, Local<String> specifier);
The embedder directly resolves this promise with the module namespace object or rejects with an exception using the V8 Promises API.
This would involve exposing i::Module::GetModuleNamespace API to the embedder and have the embedder call this to get the module namespace object and then resolve the promise with it.
This provides a simpler and more intuitive API for the embedder, but we lose out on type safety and allow the embedder resolve the promise with any arbitrary javascript object. As opposed to a type Module in the case of DynamicImportResult::FinishDynamicImportSuccess (although this could be any arbitrary module).
This also slightly violates the EcmaScript/ HTML spec boundaries and forces the embedder to implement part of the EcmaScript spec -- creating the promise, getting the module namespace object and resolving the promise are all specified in EcmaScript.
Version 3 of the API extends the above Alternate API to include additional metadata required by Node and Blink for module loading. Apart from the referrer name, Blink requires 2 additional fields namely:
Node requires a way to distinguish different scripts that have the same or no referrer name.
We introduce a new Array called PrimitiveArray that stores primitives defined by the V8 API such as Number, String, Bool, and others (as per the EcmaScript spec). Embedders can use a PrimitiveArray to store the additional metadata and pass it to V8 using ScriptOrigin during script compilation. This PrimitiveArray is passed back to the embedder as part of the HostImportDynamicallyCallback.
The PrimitiveArray has the standard Array interface :
class PrimitivesArray {
public:
static Local<PrimitivesArray> New(Isolate* isolate, int length);
int Length();
void Set(int index, Local<Primitive> item);
Local<Primitive> Get(int index);
};
We also introduce a new container type ScriptOrModule, which provides the referrer url and host defined options.
class ScriptOrModule {
public:
Local<Value> GetName();
Local<PrimitiveArray> GetHostDefinedOptions();
};
The HostImportModuleDynamicallyCallback changes to:
typedef Local<Promise> (*HostImportModuleDynamicallyCallback)(
Local<Context> context,
Local<ScriptOrModule> referrer,
Local<String> specifier);
Blink can create a PrimitiveArray to store the security nonce as a v8::String and parser inserted enum as a v8::Boolean. Node can store extra information (such as a hash) in this array to disambiguate scripts.
Internally, in V8 this PrimitiveArray is just a restricted FixedArray which is stored on a newly created slot in the Script object.
Tracking bug: 658558
Update: devtools has implemented top level await support which means import will just work seamlessly without any changes.
A big ergonomic win with using dynamic import is the seamless interop with async await as import just returns a promise.
(async () => {
const myModule = await import('./myModule.js');
})();
The context specific keyword await is only allowed inside an async function making this a bit unwieldy in the devtools console. It would be much nicer to have support for:
const myModule = await import('./myModule.js');
The blink/HTML side of this has already been implemented in D8, along with tests. The implementation lives here.