With the rise of upstart Flutter, RN gradually lost its aura. While RN may one day be out of the picture, the idea that it enabled JavaScript to interact with Native speakers will live on.

Online articles about RN communication principle are almost explained from the perspective of the client. This article wants to talk about how JS interacts with Native from the perspective of the front end.

If you want to read RN’s source code, it is not recommended to choose the latest version, which rewrites some of the underlying code in C++, and the reading and debugging experience is not very good. Not much has changed in terms of communication.

The RN version I read was 0.43.4, and I wrote a working Demo based on it that contained only the core of JavaScript and Objective-C communication, with only a few hundred lines of code.

JS call Native

JS communication part of the core code is less than 200 lines, mainly composed of NativeModules. JS, MessageQueue. JS, BatchedBridge. Where BatchedBridge is the object instantiated by MessageQueue.

I put them all in the arch.js file of the Demo project. The BatchedBridge object provides methods for JS to trigger Native calls:

var enqueueNativeCall = function(moduleID, methodID, params, onFail, onSuccess) {
    if (onFail || onSuccess) {
        // If a callback exists, add it to the callbacks dictionary
        // OC performs the callback based on the callbackID
        if (onFail) {
            params.push(this.callbackID);
            this.callbacks[this.callbackID++] = onFail;
        }
        if (onSuccess) {
            params.push(this.callbackID);
            this.callbacks[this.callbackID++] = onSuccess; }}// Place the Native call in the message queue
        this.queue[MODULE_INDEX].push(moduleID);
        this.queue[METHOD_INDEX].push(methodID);
        this.queue[PARAMS].push(params);

        // Each time there is an ID
        this.callID++;

        const now = new Date().getTime();
        / / testing whether native client is the global nativeFlushQueueImmediate method to add
        / / if you have this method, and 5 ms in queue and unprocessed calls, will take the initiative to call nativeFlushQueueImmediate trigger Native calls
        if (global.nativeFlushQueueImmediate && now - this.lastFlush > MIN_TIME_BETWEEN_FLUSHES_MS) {
            global.nativeFlushQueueImmediate(this.queue);
            // Empty the queue after the call
            this.queue = [[], [], [], this.callID]; }}Copy the code

This function stores the Native instance module ID, method ID, and callback ID into three queues. This. queue holds all three queues.

if (global.nativeFlushQueueImmediate && now - this.lastFlush > MIN_TIME_BETWEEN_FLUSHES_MS) {
    global.nativeFlushQueueImmediate(this.queue);
    // Empty the queue after the call
    this.queue = [[], [], [], this.callID];
}
Copy the code

This code is the key to JS actively triggering Native calls, with the condition that they are executed when now-this. lasFlush > 5ms. That is clear for the queue time if you have more than 5 ms is executed nativeFlushQueueImmediate function.

In the next section of Native Call JS, we will talk about that every time Native calls JS, queue will be passed to Native execution as the return value. Assuming that there is no Native Call JS within 5ms, Then JS Call Native will not be executed.

Therefore, a 5ms threshold is set here. If there is no Native JS invocation within this period, JS will actively trigger the Native invocation.

In global nativeFlushQueueImmediate function body, it is on the primary side. Execution triggers a call to the native block, passing in the argument Queue and triggering the native call.

self->_context[@"nativeFlushQueueImmediate"] = ^ (NSArray<NSArray *> *calls) {
    AHJSExecutor *strongSelf = weakSelf;
    [strongSelf->_bridge handleBuffer:calls];
};
Copy the code

Now the question is, how does JS know what methods Native can call? Is injected into the JS environment by Native before it starts executing the JS code and stored in the __batchedBridgeConfig property of global.

It contains all the modules and methods that support JS calls, and distinguishes whether each method is synchronous, asynchronous, or Promise, which is provided during Native initialization.

Each module and method is associated with an ID, which is the subscript of the module and method in their respective lists. When invoking, the JS end stores the ID in the message queue, and Native gets the ID. In the configuration table of the Native end, it finds the corresponding Native class (instance) and method and invokes it.

Once you’ve agreed on the object and method, you also need to agree on the parameters, putting the values in an array in order. The user needs to pay attention to the number and order of parameters and keep the match with the method of the Native end. Otherwise, an error will be reported.

Finally, JS callback processing, JS and Native communication is unable to pass events, so choose to serialize the events, give each callback an ID, save a copy, and then pass the ID to Native. When Native wants to execute this callback, This ID is passed back to THE JS invokeJSCallback function, which looks up the corresponding callback based on the ID and executes it.

Native call JS

Native Call JS relies on JavaScriptCore. This framework provides the interface for creating JS context and executing JS code, which is relatively straightforward. However, since Native terminal is a multi-threaded environment, it needs to be discussed in different situations, which can be divided into three categories:

  1. Synchronous call JS;
  2. Asynchronous call JS;
  3. Callback to execute JS asynchronously

The scenario of synchronous invocation is very rare, as it is limited to invocation in THE JS thread, whereas the reality is that communication between Native and JS is almost always cross-thread. Because page refreshes and event callbacks occur on the main thread.

For Native terminal, JS thread is an ordinary thread, no different from other threads, but it is used to initialize the JS context and execute JS code.

A synchronous call supports a return value, whereas an asynchronous call API has no return value and only callback can be used.

Native asynchronous invocation JS is mainly composed of callFunctionReturnFlushedQueue function distribution:

var callFunctionReturnFlushedQueue = function(module, method, args) {
    this.__callFunction(module, method, args);
    return this.flushedQueue();
}
Copy the code

This function is defined on a BatchedBridge object and is held by the __batchedBridge attribute of global.

Native calls moduleName and each parameter value in an array, which is wrapped with JSValue. JSObjectCallAsFunction provided by JavaScriptCore is used to trigger THE JS call.

Here is different from JS call Native, do not need to use ID, but directly execute.

In the above method, return this.flushedQueue(), which cleans up the JS Call Native message queue mentioned earlier and returns the information in the queue to Native execution.

If a Native JS call has a return value, it will have two forms. One is to wait for the completion of the JS method and get the return value. The other option is to callback to the caller via callback without waiting for the JS method to complete.

In fact, both asynchronous and synchronous Native uses the following methods to execute JS calls:

- (void)_executeJSCall:(NSString *)method
             arguments:(NSArray *)args
          unwrapResult:(BOOL)unwrapResult
                    callback:(AHJSCallbackBlock)onComplete
Copy the code

If the method is called from a JS thread, the return value is processed synchronously; If another thread calls the method, the return value is returned via asynchronous callback.

Callback (); callback (); callback ();

var invokeCallbackAndReturnFlushedQueue = function(callbackId, args) {
    this.__invokeCallback(callbackId, args);
    return this.flushedQueue();
}
Copy the code

Take a callbackID, find the corresponding callback and execute.

So how does Native know which JS modules can be called? In fact, this only needs to be defined internally by RN framework. For RN development, mainly on the JS side, it is good to know what functions Native provides for JS calls.

To consider

  1. Why is communication between JS and Native only asynchronous?

The essence is that JS is executed single-threaded. The Native terminal is only responsible for the UI display of the main thread, cross-thread communication using synchronization will block the UI.

  1. Why would JS Call Native design a message queue to be executed when a Native call is made, as opposed to Native Call JS which executes directly every time it is called?

To improve performance, batch processing of JS Native calls can reduce the communication overhead between JS and Native.

Thanks for reading. If you’re interested in implementation details, take a look at the Demo I wrote.