距离我写的上一篇文章 Weex从入门到超神(一) 已通过了挺久了(惭愧而不失礼貌的微笑),起初写那篇文章的初衷是由于项目中使用到了 Weex ,因此准备对 Weex 作一个心得式的笔记,后来无心间发现简书“霜神”已经对 Weex 写过几篇剖析比较深入的文章,还有其余一些缘由(懒),因此就没有继续写下去。
最近因为Facebook的 BSD license,React 被前端社区的同窗们推到了风口浪尖,React&RN、Vue&Weex 又成为了你们码前码后讨论的话题。Apache 社区还由于 Facebook 的 BSD license,全面封杀使用了 BSD license 的开源项目,貌似一切都很精彩,迫于前端同(da)学(lao)的淫威还有社区的强烈谴责,上周 Facebook 终于认怂了,承诺这周将 React 以及 gayhub 上面的其余几个项目的开源协议从 BSD 改为 MIT,下图是我脑补的场景:
html
Weex 运行时会先注入一段位于 pre-build
下的 native-bundle-main.js
代码。不过在注入这段代码以前会先注册一些默认的 Component
、Module
和Handler
,这就是 Weex 与 Native 应用层交互最核心的部分,能够理解为“组件”。其中 Component 是为了映射 Html 的一些标签,Module 中是提供一些 Native 的一些方法供 Weex 调用,Handler 是一些协议的实现。前端
注册完 Weex 默认的“组件” 以后,注入刚才那段 JS,这个时候 Vue 的标签和动做才能被 Weex 所识别和转换。
为了便于下文的描述和理解,我把 Native 这边的 SDK 称做 Weex,前端的 Vue 和 Weex 库以及 Vue 编译后的 js 统称为 Vuereact
目前 Weex 一共提供了26种 Component,比较常见的有 div
、image
、scroller
... ,有些跟 html 标签重名,有些是 Weex 自定义的。Weex 注册的 Component 有两种类型,一类是有 {@"append":@"tree"}
属性的标签,另外一类是没有{@"append":@"tree"}
属性的标签。要搞清楚这两类标签有什么不一样,咱们就要看一下 Component 的注册的源码实现。git
[WXComponentFactory registerComponent:name withClass:clazz withPros:properties];
NSMutableDictionary *dict = [WXComponentFactory componentMethodMapsWithName:name];
dict[@"type"] = name;
if (properties) {
NSMutableDictionary *props = [properties mutableCopy];
if ([dict[@"methods"] count]) {
[props addEntriesFromDictionary:dict];
}
[[WXSDKManager bridgeMgr] registerComponents:@[props]];
} else {
[[WXSDKManager bridgeMgr] registerComponents:@[dict]];
}
复制代码
首先经过一个工厂类WXComponentFactory
注册 Component,github
这个工厂类(单例)中管理了全部的 Component ,注册的每个 Component 都会用一个对应的
WXComponentConfig
来保存标签name、对应的class和属性,最后由WXComponentFactory
来统一管理这些WXComponentConfig
weex
这一步同时注册了 Component 中的 methods,关于 method 也有两类,一类是包含wx_export_method_sync_
前缀的同步方法,另外一类是包含wx_export_method_
前缀的异步方法(这两种方法有什么不一样,后面会有介绍)。在WXComponentConfig
的父类WXInvocationConfig
储存了 Component 的方法map:app
@property (nonatomic, strong) NSMutableDictionary *asyncMethods;
@property (nonatomic, strong) NSMutableDictionary *syncMethods;
复制代码
而后再从WXComponentFactory
拿到对应 Component 的方法列表字典,须要注意的是这里拿到的方法列表只是异步方法,获得的是这样的字典:dom
{
methods = (
resetLoadmore
);
type = scroller;
}
复制代码
不过大部分 Component 并无wx_export
前缀的 method,因此不少这里拿到的方法都为空。
最后也是最关键的一步,要将 Component 注册到WXBridgeContext
中。异步
if (self.frameworkLoadFinished) {
[self.jsBridge callJSMethod:method args:args];
} else {
[_methodQueue addObject:@{@"method":method, @"args":args}];
}
复制代码
最后将 Component 注册到了JSContext
中,async
还记得文章开头介绍的
native-bundle-main.js
吗?这里的注册调用了js中的registerComponents
方法,这个 Component 与 Vue 就联系起来了,在 Vue 就可使用这个 Component。
而且从上面的这段代码能够看出来,Component 的注册操做是在 JSFramework 加载完成才会进行,若是native-bundle-main.js
没有加载完成,全部的 Component 的方法注册操做都会被加到队列中等待。其中的第二个参数args
就是上面咱们拿到的字典。不过有属性的 和没属性的有点区别,有属性的会将属性添加到以前拿到的字典中做为args
再去注册。
要搞清楚这个属性干吗的,咱们先看一下WXComponentManager
中的相关源码:
- (void)_recursivelyAddComponent:(NSDictionary *)componentData toSupercomponent:(WXComponent *)supercomponent atIndex:(NSInteger)index appendingInTree:(BOOL)appendingInTree {
...
BOOL appendTree = !appendingInTree && [component.attributes[@"append"] isEqualToString:@"tree"];
// if ancestor is appending tree, child should not be laid out again even it is appending tree.
for(NSDictionary *subcomponentData in subcomponentsData){
[self _recursivelyAddComponent:subcomponentData toSupercomponent:component atIndex:-1 appendingInTree:appendTree || appendingInTree];
}
if (appendTree) {
// If appending tree,force layout in case of too much tasks piling up in syncQueue
[self _layoutAndSyncUI];
}
}
复制代码
这个方法是 Vue 页面渲染时所调用的方法,这个方法会递归添加 Component,同时会向视图中添加与 Component 相对应的 UIView。从代码的后半部分能够看到,若是当前 Component 有{@"append":@"tree"}
属性而且它的父 Component 没有这个属性将会强制对页面进行从新布局。能够看到这样作是为了防止UI绘制任务太多堆积在一块儿影响同步队列任务的执行。
搞清楚了 Component 的注册机制,下面重点扒一下 Component 的运行原理:Vue 标签是如何加载以及渲染到视图上的。
从刚才的注册过程当中发现,最后一步是经过_jsBridge
调用callJSMethod
这个方法来注册的,并且从WXBridgeContext
中能够看到,这个_jsBridge
就是WXJSCoreBridge
的实例。WXJSCoreBridge
能够认为是 Weex 与 Vue 进行通讯的最底层的部分。在调用callJSMethod
方法以前,_jsBridge
向 JavaScriptCore 中注册了不少全局 function,由于jsBridge
是懒加载的,因此这些操做只会执行一次,具体请看精简后的源码:
[_jsBridge registerCallNative:^NSInteger(NSString *instance, NSArray *tasks, NSString *callback) {
...
}];
[_jsBridge registerCallAddElement:^NSInteger(NSString *instanceId, NSString *parentRef, NSDictionary *elementData, NSInteger index) {
...
}];
[_jsBridge registerCallCreateBody:^NSInteger(NSString *instanceId, NSDictionary *bodyData) {
...
}];
[_jsBridge registerCallRemoveElement:^NSInteger(NSString *instanceId, NSString *ref) {
...
}];
[_jsBridge registerCallMoveElement:^NSInteger(NSString *instanceId,NSString *ref,NSString *parentRef,NSInteger index) {
...
}];
[_jsBridge registerCallUpdateAttrs:^NSInteger(NSString *instanceId,NSString *ref,NSDictionary *attrsData) {
...
}];
[_jsBridge registerCallUpdateStyle:^NSInteger(NSString *instanceId,NSString *ref,NSDictionary *stylesData) {
...
}];
[_jsBridge registerCallAddEvent:^NSInteger(NSString *instanceId,NSString *ref,NSString *event) {
...
}];
[_jsBridge registerCallRemoveEvent:^NSInteger(NSString *instanceId,NSString *ref,NSString *event) {
...
}];
[_jsBridge registerCallCreateFinish:^NSInteger(NSString *instanceId) {
...
}];
[_jsBridge registerCallNativeModule:^NSInvocation*(NSString *instanceId, NSString *moduleName, NSString *methodName, NSArray *arguments, NSDictionary *options) {
...
}];
[_jsBridge registerCallNativeComponent:^void(NSString *instanceId, NSString *componentRef, NSString *methodName, NSArray *args, NSDictionary *options) {
...
}];
复制代码
从这些方法名看,大多数都是一些与 Dom 更新相关的方法,咱们在WXJSCoreBridge
中更细致的看一下是怎么实现的:
- (void)registerCallAddElement:(WXJSCallAddElement)callAddElement
{
id callAddElementBlock = ^(JSValue *instanceId, JSValue *ref, JSValue *element, JSValue *index, JSValue *ifCallback) {
NSString *instanceIdString = [instanceId toString];
NSDictionary *componentData = [element toDictionary];
NSString *parentRef = [ref toString];
NSInteger insertIndex = [[index toNumber] integerValue];
[WXTracingManager startTracingWithInstanceId:instanceIdString ref:componentData[@"ref"] className:nil name:WXTJSCall phase:WXTracingBegin functionName:@"addElement" options:nil];
WXLogDebug(@"callAddElement...%@, %@, %@, %ld", instanceIdString, parentRef, componentData, (long)insertIndex);
return [JSValue valueWithInt32:(int32_t)callAddElement(instanceIdString, parentRef, componentData, insertIndex) inContext:[JSContext currentContext]];
};
_jsContext[@"callAddElement"] = callAddElementBlock;
}
复制代码
这是一个更新 Dom 添加 UIView 的方法,这里须要把 Native 的方法暴露给 JS 调用。可是有一个问题:
OC 的方法参数格式和 JS 的不同,不能直接提供给 JS 调用。
因此这里用了两个 Block 嵌套的方式,在 JS 中调用方法时会先 invoke 里层的 callAddElementBlock,这层 Block 将 JS 传进来的参数转换成 OC 的参数格式,再执行 callAddElement 并返回一个 JSValue 给 JS,callAddElement Block中是在WXComponentManager
中完成的关于 Component 的一些操做,这在上面介绍 Component 包含 tree
属性问题时已经介绍过了。
至此,简单来讲就是:Weex 的页面渲染是经过先向 JSCore 注入方法,Vue 加载完成就能够调用这些方法并传入相应的参数完成 Component 的渲染和视图的更新。
要注意,每个 WXSDKInstance
对应一个 Vue 页面,Vue 加载以前就会建立对应的 WXSDKInstance,全部的 Component 都继承自WXComponent
,他们的初始化方法都是
-(instancetype)initWithRef:(NSString *)ref
type:(NSString *)type
styles:(NSDictionary *)styles
attributes:(NSDictionary *)attributes
events:(NSArray *)events
weexInstance:(WXSDKInstance *)weexInstance
复制代码
这个方法会在 JS 调用callCreateBody
时被 invoke。
Module 注册流程和 Component 基本一致,首先经过WXModuleFactory
注册 Module
- (NSString *)_registerModule:(NSString *)name withClass:(Class)clazz
{
WXAssert(name && clazz, @"Fail to register the module, please check if the parameters are correct !");
[_moduleLock lock];
//allow to register module with the same name;
WXModuleConfig *config = [[WXModuleConfig alloc] init];
config.name = name;
config.clazz = NSStringFromClass(clazz);
[config registerMethods];
[_moduleMap setValue:config forKey:name];
[_moduleLock unlock];
return name;
}
复制代码
注册 Moudle 的registerMethods
方法与注册 Component 是同样的,都是将方法注册到WXInvocationConfig
中,wx_export_method_sync_
前缀的同步方法注册到 syncMethods 中,wx_export_method_
前缀的异步方法注册到 asyncMethods 中。再将 Moudle 的同步和异步方法取出来调用registerComponents
注入到JSContext
中
{
dom = (
addEventListener,
removeAllEventListeners,
addEvent,
removeElement,
getComponentRect,
updateFinish,
scrollToElement,
addRule,
updateAttrs,
addElement,
createFinish,
createBody,
updateStyle,
removeEvent,
refreshFinish,
moveElement
);
}
复制代码
这是WXDomModule
中全部的方法,Moudle 中的方法注册比 Component 更有意义,由于 Moudle 中基本上都是暴露给 Vue 调用的 Native 方法。
接下来咱们来看一下 Moudle 的方法如何被调用以及 syncMethods 和 asyncMethods 有什么不一样。
在前面的jsBridge
懒加载中,有一个注册方法是跟 Moudle 中方法有关的,Moudle 中的方法会在这个注册方法的回调中被 invoke,换言之,Vue 调用 Moudle 中的方法会在这个回调中被唤起
[_jsBridge registerCallNativeModule:^NSInvocation*(NSString *instanceId, NSString *moduleName, NSString *methodName, NSArray *arguments, NSDictionary *options) {
WXSDKInstance *instance = [WXSDKManager instanceForID:instanceId];
if (!instance) {
WXLogInfo(@"instance not found for callNativeModule:%@.%@, maybe already destroyed", moduleName, methodName);
return nil;
}
WXModuleMethod *method = [[WXModuleMethod alloc] initWithModuleName:moduleName methodName:methodName arguments:arguments instance:instance];
if(![moduleName isEqualToString:@"dom"] && instance.needPrerender){
[WXPrerenderManager storePrerenderModuleTasks:method forUrl:instance.scriptURL.absoluteString];
return nil;
}
return [method invoke];
}];
复制代码
在WXModuleMethod
中能够看到-(NSInvocation *)invoke
这个方法,Moudle 中的方法将会经过这个方法被 invoke
...
Class moduleClass = [WXModuleFactory classWithModuleName:_moduleName];
if (!moduleClass) {
NSString *errorMessage = [NSString stringWithFormat:@"Module:%@ doesn't exist, maybe it has not been registered", _moduleName];
WX_MONITOR_FAIL(WXMTJSBridge, WX_ERR_INVOKE_NATIVE, errorMessage);
return nil;
}
id<WXModuleProtocol> moduleInstance = [self.instance moduleForClass:moduleClass];
WXAssert(moduleInstance, @"No instance found for module name:%@, class:%@", _moduleName, moduleClass);
BOOL isSync = NO;
SEL selector = [WXModuleFactory selectorWithModuleName:self.moduleName methodName:self.methodName isSync:&isSync];
if (![moduleInstance respondsToSelector:selector]) {
// if not implement the selector, then dispatch default module method
if ([self.methodName isEqualToString:@"addEventListener"]) {
[self.instance _addModuleEventObserversWithModuleMethod:self];
} else if ([self.methodName isEqualToString:@"removeAllEventListeners"]) {
[self.instance _removeModuleEventObserverWithModuleMethod:self];
} else {
NSString *errorMessage = [NSString stringWithFormat:@"method:%@ for module:%@ doesn't exist, maybe it has not been registered", self.methodName, _moduleName];
WX_MONITOR_FAIL(WXMTJSBridge, WX_ERR_INVOKE_NATIVE, errorMessage);
}
return nil;
}
[self commitModuleInvoke];
NSInvocation *invocation = [self invocationWithTarget:moduleInstance selector:selector];
if (isSync) {
[invocation invoke];
return invocation;
} else {
[self _dispatchInvocation:invocation moduleInstance:moduleInstance];
return nil;
}
复制代码
先经过 WXModuleFactory
拿到对应的方法 Selector,而后再拿到这个方法对应的 NSInvocation ,最后 invoke 这个 NSInvocation。对于 syncMethods 和 asyncMethods 有两种 invoke 方式。若是是 syncMethod 会直接 invoke ,若是是 asyncMethod,会将它派发到某个指定的线程中进行 invoke,这样作的好处是不会阻塞当前线程。到这里 Moudle 的大概的运行原理都清除了,不过还有一个问题,Moudle 的方法是怎么暴露给 Vue 的呢?
在 Moudle 中咱们经过 Weex 提供的宏能够将方法暴露出来:
#define WX_EXPORT_METHOD(method) WX_EXPORT_METHOD_INTERNAL(method,wx_export_method_)
#define WX_EXPORT_METHOD_SYNC(method) WX_EXPORT_METHOD_INTERNAL(method,wx_export_method_sync_)
复制代码
分别提供了 syncMethod 和 asyncMethod 的宏,展开实际上是这样的
#define WX_EXPORT_METHOD_INTERNAL(method, token) \
+ (NSString *)WX_CONCAT_WRAPPER(token, __LINE__) { \
return NSStringFromSelector(method); \
}
复制代码
这里会自动将方法名和当前的行数拼成一个新的方法名,这样作的好处是能够保证方法的惟一性,例如 WXDomModule
中的 createBody:
方法利用宏暴露出来,最终展开形式是这样的
+ (NSString *)wx_export_method_40 { \
return NSStringFromSelector(createBody:); \
}
复制代码
在WXInvocationConfig
中调用- (void)registerMethods
注册方法的时候,首先拿到当前 class 中全部的类方法**(宏包装成的方法,并非实际要注册的方法)**,而后经过判断有无wx_export_method_sync_
前缀和wx_export_method_
前缀来判断是否为暴露的方法,而后再调用该类方法,得到最终的实例方法字符串
method = ((NSString* (*)(id, SEL))[currentClass methodForSelector:selector])(currentClass, selector);
复制代码
拿到须要注册的实例方法字符串,再将方法字符串注册到WXInvocationConfig
的对应方法 map 中。
Handlers 的注册和使用很是简单,直接将对应的 class 注册到 WXHandlerFactory
map中
[[WXHandlerFactory sharedInstance].handlers setObject:handler forKey:NSStringFromProtocol(protocol)];
复制代码
须要使用的时候也很是简单粗暴,经过WXHandlerFactory
的方法和相应的 protocol
+ (id)handlerForProtocol:(Protocol *)
{
id handler = [[WXHandlerFactory sharedInstance].handlers objectForKey:NSStringFromProtocol(protocol)];
return handler;
}
复制代码
直接拿出便可。