在现在流行的多元框架中,最常见的就是JavaScript的应用了。这里就来分析下react-native的实现。
react-native并不是只有一种实现。因为他不仅仅支持JavaScriptCore来实现交互,也考虑到了某些场景下需要使用WebView来实现,同时也有很多debug工具,需要将JavaScript的执行环境转移到浏览器。大概的结构如下:
1 | ------------------------------ |
其中执行器部分(Executor)可随意替换为不同实现。这里我们来分析下JSContext中的实现。
Module
要实现react-native这样大型的框架,javascript就不能被散乱的放置,那么就必须进行分模块。调用模块时需要使用CommonJS或者ES6的方式。
1 | var module = require('module') |
同时也需要考虑到如此多的模块,一次性载入所带来的性能损耗,就必须采用惰性加载的方式。
队列
和其他项目的实现方式类似,react-native依然使用了message queue来实现通信,而不是JavaScriptCore自带的绑定功能,这是为了兼容上面说的多Executor。
与其他方案不太相同的是,react-native在module
,module-method
和callback
都使用了id: number
来取代名字,个人猜测可能是为了性能考虑。
那么我们就JSContext这种情况来说下整个通信实现的过程。
实现
这里使用console
来作为例子,这里使用JavaScriptCore的c接口是为了和react-native保持一致,同时忽略了内存问题。
模块表
观察发送给JSContext的数据发现会有很多类似这样的JSON数据:
1 | [ |
可以看出来,[0]表示的是module名字,而[2]表示的是module的方法,正式这一份表,才对应了javascript和native双方的indexId,所有的通信都是对应于这一份表来进行的。
所以双方都会有一份自己维护的模块,而js的模块表我们这里定义为
1 | // id => module 这是native调用js module时,传递的是id |
载入模块
在javascript端,如果需要载入模块,那么我们会使用
1 | var console = require('console') |
那么在JSContext还没有console模块的情况下如何进行初始化呢?这里就需要一个NativeRequire
,来载入native模块,结合上面的模块配置表,require
的实现如下:
1 | var NativeRequire |
NativeRequire
在初始化JSContext时,我们就需要为通信做好连接的准备,直接注入3个方法。(这里react-native其实还有另外一个方式触发require,通过nativeModuleProxy
对象的getProperty
来触发,这里讨论最原始的require
方式)
1 | JSClassDefinition definition = kJSClassDefinitionEmpty; |
关于NativeFlushQueueSync
和NativeFlushQueueAsync
到下面再解释。
这里native的模块表就不实现了,直接使用["console", null, ["log", "getName"], [1]]
。
1 | JSValueRef NativeRequire ( |
这里会同步调用初始化模块方法,并且将模块返回给JSContext。
但是可以发现模块表中的方法都是string,也就是方法名,我们如何去使用console.log()
这样的方法呢?这里就需要中间的初始化模块这个作用了。
初始化模块
回到上节的第三步,此时native传给js一个模块表,让js去构造这个模块。让我们回到js:
1 | function genNativeModules(moduleId, config) { |
这样便把string转换为function了,可以像正常的js方法那样使用了。
到这里注册js模块已经完成,下面来说说调用的过程。
同步方法的调用
同步方法的调用对于JSContext来说会简单很多,而对于很多基于webview的实现来说就会麻烦一些,因为参数不能直接编码在url中,最后我们来讨论下这个问题。
上节说到将方法名转换为function,那么function具体实现是怎么样的呢?
首先来看看同步方法的实现:
1 | module[method] = function (args) { |
这里的NativeFlushQueueSync
方法就是一开始我们注入的方法,作用是执行对应模块的对应方法。
1 | JSValueRef NativeFlushQueueSync ( |
然而react-native并没有完全严格上的同步执行方法。因为很多调用UI层的功能必须在主线程上,而JSContext是在自己的线程中执行,所以如果需要严格的同步执行,需要阻塞JS线程。而几乎所有功能都是不需要执行结果的(return void),所以只要触发native去执行该方法就行了,无需等待执行完再返回。而需要有返回值的接口都被设计成异步的了。
异步回调
说到异步回调,大家用的方案好像都是一样的,那就是callbackId
。
1 | var messageQueue = {} |
创建异步module方法的方式会有点不一样:
1 | module[method] = function (args) { |
然后来看看native的实现:
1 | JSValueRef NativeFlushQueueAsync ( |
可以看到和同步方式的区别是就是回调会缓存在队列里。
应用
1 | var console = require('console') |
1 | // output: |
装饰
实际情况不会这么简单,js也不会直接使用native提供的模块的,一般会包装一层。比如像这样
1 | var nativeLog = NativeRequire('NSLog') |
实际
真实情况不会像上面那么简单,需要考虑到多线程,每个module的运行线程,js消息队列等保证js的安全顺序执行。
WebView
其他项目的方案也是类似的,但也有少许的不同。
比如NativeRequire,在Web里面除了通过iframe来实现,还可以通过script
标签来导入模块文件。
1 | var script = document.createElement('script') |
同时由于web通过url传递参数的限制,所以web的参数传递是通过native去主动拉取的。大概的流程如下:
1 | [web] call native --> push <call info> --(iframe url)--> |
同时很多方案,会使用名字来传递模块和方法,这样做最简单也最直接。但是如果存在频繁交互的过程可能会降低性能。
最后
总的来说,javascript-native交互还是挺简单的,只要在初始的设计上比较符合现在与未来的发展,还是可以做到很灵活的。至于使用哪种方案,做到什么样的程度,可以依据自身的需求来判断。