距離我寫的上一篇文章 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;
}
複製代碼
直接拿出便可。