Weex從入門到超神(二)

距離我寫的上一篇文章 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 的一些經驗和心得,仍是但願寫出來和你們一塊兒分享。

應用層核心組件

Weex 運行時會先注入一段位於 pre-build 下的 native-bundle-main.js 代碼。不過在注入這段代碼以前會先註冊一些默認的 ComponentModuleHandler這就是 Weex 與 Native 應用層交互最核心的部分,能夠理解爲「組件」。其中 Component 是爲了映射 Html 的一些標籤,Module 中是提供一些 Native 的一些方法供 Weex 調用,Handler 是一些協議的實現。前端

註冊完 Weex 默認的「組件」 以後,注入剛纔那段 JS,這個時候 Vue 的標籤和動做才能被 Weex 所識別和轉換。
爲了便於下文的描述和理解,我把 Native 這邊的 SDK 稱做 Weex,前端的 Vue 和 Weex 庫以及 Vue 編譯後的 js 統稱爲 Vuereact

1. 組件:Component

目前 Weex 一共提供了26種 Component,比較常見的有 divimagescroller... ,有些跟 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來統一管理這些WXComponentConfigweex

這一步同時註冊了 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。

2. 組件:Module

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 中。

3. 組件:Handlers

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;
}
複製代碼

直接拿出便可。

相關文章
相關標籤/搜索