Weex 事件傳遞的那些事兒

前言

在前兩篇文章里面分別談了Weex如何在Native端初始化的和Weex是如何高效的渲染Native的原生UI的。Native這邊還缺一塊,那就是Native產(chǎn)生的一些事件,是怎么傳回給JS的。這篇文章就詳細(xì)分析這一部分。

目錄

  • 1.Weex的事件類(lèi)型
  • 2.Weex的事件傳遞

一.Weex的事件類(lèi)型

在Weex中,目前最新版本中事件總共分為4種類(lèi)型,通用事件,Appear 事件,Disappear 事件,Page 事件。

在Weex的組件里面只包含前三種事件,即通用事件,Appear 事件,Disappear 事件。

當(dāng)WXComponent添加事件的時(shí)候,會(huì)調(diào)用以下函數(shù):



- (void)_addEventOnMainThread:(NSString *)addEventName
{
    WX_ADD_EVENT(appear, addAppearEvent)
    WX_ADD_EVENT(disappear, addDisappearEvent)
    
    WX_ADD_EVENT(click, addClickEvent)
    WX_ADD_EVENT(swipe, addSwipeEvent)
    WX_ADD_EVENT(longpress, addLongPressEvent)
    
    WX_ADD_EVENT(panstart, addPanStartEvent)
    WX_ADD_EVENT(panmove, addPanMoveEvent)
    WX_ADD_EVENT(panend, addPanEndEvent)
    
    WX_ADD_EVENT(horizontalpan, addHorizontalPanEvent)
    WX_ADD_EVENT(verticalpan, addVerticalPanEvent)
    
    WX_ADD_EVENT(touchstart, addTouchStartEvent)
    WX_ADD_EVENT(touchmove, addTouchMoveEvent)
    WX_ADD_EVENT(touchend, addTouchEndEvent)
    WX_ADD_EVENT(touchcancel, addTouchCancelEvent)
    
    [self addEvent:addEventName];
}



WX_ADD_EVENT是一個(gè)宏:



#define WX_ADD_EVENT(eventName, addSelector) \
if ([addEventName isEqualToString:@#eventName]) {\
    [self addSelector];\
}

即是判斷待添加的事件addEventName的名字和默認(rèn)支持的事件名字eventName是否一致,如果一致,就執(zhí)行addSelector方法。

最后會(huì)執(zhí)行一個(gè)addEvent:方法,每個(gè)組件里面會(huì)可以重寫(xiě)這個(gè)方法。在這個(gè)方法里面做的就是對(duì)組件的狀態(tài)的標(biāo)識(shí)。

比如WXWebComponent組件里面的addEvent:方法:


- (void)addEvent:(NSString *)eventName
{
    if ([eventName isEqualToString:@"pagestart"]) {
        _startLoadEvent = YES;
    }
    else if ([eventName isEqualToString:@"pagefinish"]) {
        _finishLoadEvent = YES;
    }
    else if ([eventName isEqualToString:@"error"]) {
        _failLoadEvent = YES;
    }
}


在這個(gè)方法里面即對(duì)Web組件里面的狀態(tài)進(jìn)行了標(biāo)識(shí)。

接下來(lái)就看看這幾個(gè)組件是怎么識(shí)別事件的觸發(fā)的。

1. 通用事件

在WXComponent的定義里,定義了如下和事件相關(guān)的變量:


@interface WXComponent ()
{
@package

    BOOL _appearEvent;
    BOOL _disappearEvent;
    UITapGestureRecognizer *_tapGesture;
    NSMutableArray *_swipeGestures;
    UILongPressGestureRecognizer *_longPressGesture;
    UIPanGestureRecognizer *_panGesture;
    
    BOOL _listenPanStart;
    BOOL _listenPanMove;
    BOOL _listenPanEnd;
    
    BOOL _listenHorizontalPan;
    BOOL _listenVerticalPan;
    
    WXTouchGestureRecognizer* _touchGesture;
}


上述變量里面就包含有4個(gè)手勢(shì)識(shí)別器和1個(gè)自定義手勢(shì)識(shí)別器。所以Weex的通用事件里面就包含這5種,點(diǎn)擊事件,輕掃事件,長(zhǎng)按事件,拖動(dòng)事件,通用觸摸事件。

(一)點(diǎn)擊事件

首先看點(diǎn)擊事件:



    WX_ADD_EVENT(click, addClickEvent)


點(diǎn)擊事件是通過(guò)上面這個(gè)宏加到指定視圖上的。這個(gè)宏上面提到過(guò)了。這里直接把宏展開(kāi)


#define WX_ADD_EVENT(click, addClickEvent) \
if ([addEventName isEqualToString:@“click”]) {\
    [self addClickEvent];\
}

如果addEventName傳進(jìn)來(lái)event的是@“click”,那么就是執(zhí)行addClickEvent方法。


- (void)addClickEvent
{
    if (!_tapGesture) {
        _tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onClick:)];
        _tapGesture.delegate = self;
        [self.view addGestureRecognizer:_tapGesture];
    }
}


給當(dāng)前的視圖增加一個(gè)點(diǎn)擊手勢(shì),觸發(fā)的方法是onClick:方法。



- (void)onClick:(__unused UITapGestureRecognizer *)recognizer
{
    NSMutableDictionary *position = [[NSMutableDictionary alloc] initWithCapacity:4];
    CGFloat scaleFactor = self.weexInstance.pixelScaleFactor;
    
    if (!CGRectEqualToRect(self.calculatedFrame, CGRectZero)) {
        CGRect frame = [self.view.superview convertRect:self.calculatedFrame toView:self.view.window];
        position[@"x"] = @(frame.origin.x/scaleFactor);
        position[@"y"] = @(frame.origin.y/scaleFactor);
        position[@"width"] = @(frame.size.width/scaleFactor);
        position[@"height"] = @(frame.size.height/scaleFactor);
    }

    [self fireEvent:@"click" params:@{@"position":position}];
}

一旦用戶(hù)點(diǎn)擊屏幕,就會(huì)觸發(fā)點(diǎn)擊手勢(shì),點(diǎn)擊手勢(shì)就會(huì)執(zhí)行上述的onClick:方法。在這個(gè)方法中,Weex會(huì)計(jì)算點(diǎn)擊出點(diǎn)擊到的視圖的坐標(biāo)以及寬高尺寸。

說(shuō)到這里就需要提到Weex的坐標(biāo)計(jì)算方法了。

(1)計(jì)算縮放比例因子

在日常iOS開(kāi)發(fā)中,開(kāi)發(fā)者使用的計(jì)算單位是pt。

iPhone5分辨率320pt x 568pt
iPhone6分辨率375pt x 667pt
iPhone6 Plus分辨率414pt x 736pt

由于每個(gè)屏幕的ppi不同(ppi:Pixels Per Inch,即每英寸所擁有的像素?cái)?shù)目,屏幕像素密度。),最終會(huì)導(dǎo)致分辨率的不同。

這也就是我們?nèi)粘Uf(shuō)的@1x,@2x,@3x,目前iPhone手機(jī)也就3種ppi

@1x,163ppi(iPhone3gs)
@2x,326ppi(iPhone4、4s、5、5s、6,6s,7)
@3x,401ppi(iPhone6+、6s+、7+)

px即pixels像素,1px代表屏幕上一個(gè)物理的像素點(diǎn)。

iPhone5像素640px x 1136px
iPhone6像素750px x 1334px
iPhone6 Plus像素1242px x 2208px

而Weex的開(kāi)發(fā)中,目前都是用的px,而且Weex 對(duì)于長(zhǎng)度值目前只支持像素px值,還不支持相對(duì)單位(em、rem)

那么就需要pt和px的換算了。

在Weex的世界里,定義了一個(gè)默認(rèn)屏幕尺寸,用來(lái)適配iOS,Android各種不同大小的屏幕。


// The default screen width which helps us to calculate the real size or scale in different devices.
static const CGFloat WXDefaultScreenWidth = 750.0;


在Weex中定義的默認(rèn)的屏幕寬度是750,注意是寬度。



+ (CGFloat)defaultPixelScaleFactor
{
    static CGFloat defaultScaleFactor;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        defaultScaleFactor = [self portraitScreenSize].width / WXDefaultScreenWidth;
    });
    
    return defaultScaleFactor;
}


這里計(jì)算了一個(gè)默認(rèn)的縮放比例因子,portraitScreenSize里面計(jì)算出了屏幕在portrait方向下的大小,即如果方向是landscape,那么縮放比例因子應(yīng)該等于WXScreenSize().height / WXDefaultScreenWidth,反之應(yīng)該等于WXScreenSize().width / WXDefaultScreenWidth。

這里計(jì)算的是pt。

iPhone 4、4s、5、5s、5c、SE的比例因子是0.42666667
iPhone 6、6s、7比例因子是0.5
iPhone 6+、6s+、7+比例因子是0.552

(2)計(jì)算視圖的縮放尺寸

計(jì)算視圖的縮放尺寸主要在這個(gè)方法里面被計(jì)算。



- (void)_calculateFrameWithSuperAbsolutePosition:(CGPoint)superAbsolutePosition
                           gatherDirtyComponents:(NSMutableSet<WXComponent *> *)dirtyComponents
{
    if (!_cssNode->layout.should_update) {
        return;
    }
    _cssNode->layout.should_update = false;
    _isLayoutDirty = NO;
    
    // 計(jì)算視圖的Frame
    CGRect newFrame = CGRectMake(WXRoundPixelValue(_cssNode->layout.position[CSS_LEFT]),
                                 WXRoundPixelValue(_cssNode->layout.position[CSS_TOP]),
                                 WXRoundPixelValue(_cssNode->layout.dimensions[CSS_WIDTH]),
                                 WXRoundPixelValue(_cssNode->layout.dimensions[CSS_HEIGHT]));
    
    BOOL isFrameChanged = NO;
    // 比較newFrame和_calculatedFrame,第一次_calculatedFrame為CGRectZero
    if (!CGRectEqualToRect(newFrame, _calculatedFrame)) {
        isFrameChanged = YES;
        _calculatedFrame = newFrame;
        [dirtyComponents addObject:self];
    }
    
    CGPoint newAbsolutePosition = [self computeNewAbsolutePosition:superAbsolutePosition];
    
    _cssNode->layout.dimensions[CSS_WIDTH] = CSS_UNDEFINED;
    _cssNode->layout.dimensions[CSS_HEIGHT] = CSS_UNDEFINED;
    _cssNode->layout.position[CSS_LEFT] = 0;
    _cssNode->layout.position[CSS_TOP] = 0;
    
    [self _frameDidCalculated:isFrameChanged];
    
    for (WXComponent *subcomponent in _subcomponents) {
        [subcomponent _calculateFrameWithSuperAbsolutePosition:newAbsolutePosition gatherDirtyComponents:dirtyComponents];
    }
}


newFrame就是計(jì)算出來(lái)的縮放過(guò)的Frame。

如果嘗試自己手動(dòng)計(jì)算Vue.js上設(shè)置的px與實(shí)際的視圖坐標(biāo)值相比,你會(huì)發(fā)現(xiàn)永遠(yuǎn)都差一點(diǎn),雖然偏差不多,但是總有誤差,原因在哪里呢?就在WXRoundPixelValue這個(gè)函數(shù)里面。


CGFloat WXRoundPixelValue(CGFloat value)
{
    CGFloat scale = WXScreenScale();
    return round(value * scale) / scale;
}


WXRoundPixelValue這個(gè)函數(shù)里面進(jìn)行了一次四舍五入的計(jì)算,這里會(huì)對(duì)精度有所損失,所以就會(huì)導(dǎo)致最終Native的組件的坐標(biāo)會(huì)偏差一點(diǎn)。

舉個(gè)例子:



<style>

    .pic{
        width: 200px;
        height: 200px;
        margin-top: 100px;
        left: 200px;
        background-color: #a88859;
    }

</style>



這里是一個(gè)imageComponent,坐標(biāo)是距離上邊距100px,距離左邊距200px,寬200px,高200px。

假設(shè)我們是在iPhone 7+的屏幕上,ppi對(duì)應(yīng)的應(yīng)該是scale = 3(即@3x)。

按照Weex的上述的計(jì)算方法算,那么對(duì)應(yīng)縮放的px為:


x = 200 * ( 414.0 / 750.0 ) = 110.400000
y = 100 * ( 414.0 / 750.0 ) = 55.200000
width = 200 * ( 414.0 / 750.0 ) = 110.400000
height = 200 * ( 414.0 / 750.0 ) = 110.400000

再轉(zhuǎn)換成pt:


x = round ( 110.400000 * 3 ) / 3 = 110.333333
y = round ( 55.200000 * 3 ) / 3 = 55.333333
width = round ( 110.400000 * 3 ) / 3 = 110.333333
height = round ( 110.400000 * 3 ) / 3 = 110.333333

如果只是單純的認(rèn)為是針對(duì)750的成比縮放,那么這里110.333333 / ( 414.0 / 750.0 ) = 199.87922101,你會(huì)發(fā)現(xiàn)這個(gè)數(shù)字距離200還是差了零點(diǎn)幾。精度就是損失在了round函數(shù)上了

那么當(dāng)前的imageComponent在父視圖里面的Frame = (110.333333,55.333333,110.333333,110.333333)。

回到onClick:方法里面。


- (void)onClick:(__unused UITapGestureRecognizer *)recognizer
{
    NSMutableDictionary *position = [[NSMutableDictionary alloc] initWithCapacity:4];
    CGFloat scaleFactor = self.weexInstance.pixelScaleFactor;
    
    if (!CGRectEqualToRect(self.calculatedFrame, CGRectZero)) {
        CGRect frame = [self.view.superview convertRect:self.calculatedFrame toView:self.view.window];
        position[@"x"] = @(frame.origin.x/scaleFactor);
        position[@"y"] = @(frame.origin.y/scaleFactor);
        position[@"width"] = @(frame.size.width/scaleFactor);
        position[@"height"] = @(frame.size.height/scaleFactor);
    }

    [self fireEvent:@"click" params:@{@"position":position}];
}

如果點(diǎn)擊到視圖,就會(huì)觸發(fā)點(diǎn)擊手勢(shì)的處理方法,就會(huì)進(jìn)入到上述方法里。

這里會(huì)計(jì)算出點(diǎn)擊到的視圖相對(duì)于window的絕對(duì)坐標(biāo)。


CGRect frame = [self.view.superview convertRect:self.calculatedFrame toView:self.view.window];

上面這句話(huà)會(huì)進(jìn)行一個(gè)坐標(biāo)轉(zhuǎn)換。坐標(biāo)系轉(zhuǎn)換到全局的window的左邊。

還是按照上面舉的例子,如果imageComponent經(jīng)過(guò)轉(zhuǎn)換以后,frame = (110.33333333333333, 119.33333333333334, 110.33333333333333, 110.33333333333331),這里就是y軸的距離發(fā)生了變化,因?yàn)榫图由狭薾avigation + statusBar 的64的高度。

計(jì)算出了這個(gè)window絕對(duì)坐標(biāo)之后,還要還原成相對(duì)于750.0寬度的“尺寸”。這里之所以打引號(hào),就是因?yàn)檫@里有精度損失,在round函數(shù)那里丟了一些精度。


x = 110.33333333333333 / ( 414.0 / 750.0 ) = 199.8792270531401
y = 119.33333333333334 / ( 414.0 / 750.0 ) = 216.1835748792271
width = 110.33333333333333 / ( 414.0 / 750.0 ) = 199.8792270531401
height = 110.33333333333333 / ( 414.0 / 750.0 ) = 199.8792270531401


上述就是點(diǎn)擊以后經(jīng)過(guò)轉(zhuǎn)換最終得到的坐標(biāo),這個(gè)坐標(biāo)會(huì)傳遞給JS。

(二)輕掃事件

接著是輕掃事件。



    WX_ADD_EVENT(swipe, addSwipeEvent)


這個(gè)宏和上面點(diǎn)擊事件的展開(kāi)原理一樣,這里不再贅述。

如果addEventName傳進(jìn)來(lái)event的是@“swipe”,那么就是執(zhí)行addSwipeEvent方法。


- (void)addSwipeEvent
{
    if (_swipeGestures) {
        return;
    }
    
    _swipeGestures = [NSMutableArray arrayWithCapacity:4];
    

    // 下面的代碼寫(xiě)的比較“奇怪”,原因在于UISwipeGestureRecognizer的direction屬性,是一個(gè)可選的位掩碼,但是每個(gè)手勢(shì)識(shí)別器又只能處理一個(gè)方向的手勢(shì),所以就導(dǎo)致了下面需要生成四個(gè)UISwipeGestureRecognizer的手勢(shì)識(shí)別器。

    SEL selector = @selector(onSwipe:);

    // 新建一個(gè)upSwipeRecognizer
    UISwipeGestureRecognizer *upSwipeRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self
                                                                                            action:selector];
    upSwipeRecognizer.direction = UISwipeGestureRecognizerDirectionUp;
    upSwipeRecognizer.delegate = self;
    [_swipeGestures addObject:upSwipeRecognizer];
    [self.view addGestureRecognizer:upSwipeRecognizer];
    

    // 新建一個(gè)downSwipeRecognizer
    UISwipeGestureRecognizer *downSwipeRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self
                                                                                              action:selector];
    downSwipeRecognizer.direction = UISwipeGestureRecognizerDirectionDown;
    downSwipeRecognizer.delegate = self;
    [_swipeGestures addObject:downSwipeRecognizer];
    [self.view addGestureRecognizer:downSwipeRecognizer];
    
    // 新建一個(gè)rightSwipeRecognizer
    UISwipeGestureRecognizer *rightSwipeRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self
                                                                                               action:selector];
    rightSwipeRecognizer.direction = UISwipeGestureRecognizerDirectionRight;
    rightSwipeRecognizer.delegate = self;
    [_swipeGestures addObject:rightSwipeRecognizer];
    [self.view addGestureRecognizer:rightSwipeRecognizer];
    
    // 新建一個(gè)leftSwipeRecognizer
    UISwipeGestureRecognizer *leftSwipeRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self
                                                                                              action:selector];
    leftSwipeRecognizer.direction = UISwipeGestureRecognizerDirectionLeft;
    leftSwipeRecognizer.delegate = self;
    [_swipeGestures addObject:leftSwipeRecognizer];
    [self.view addGestureRecognizer:leftSwipeRecognizer];
}

上面會(huì)新建4個(gè)方向上的手勢(shì)識(shí)別器。因?yàn)槊總€(gè)手勢(shì)識(shí)別器又只能處理一個(gè)方向的手勢(shì),所以就導(dǎo)致了需要生成四個(gè)UISwipeGestureRecognizer的手勢(shì)識(shí)別器。

給當(dāng)前的視圖增加一個(gè)輕掃手勢(shì),觸發(fā)的方法是onSwipe:方法。



- (void)onSwipe:(UISwipeGestureRecognizer *)gesture
{
    UISwipeGestureRecognizerDirection direction = gesture.direction;
    
    NSString *directionString;
    switch(direction) {
        case UISwipeGestureRecognizerDirectionLeft:
            directionString = @"left";
            break;
        case UISwipeGestureRecognizerDirectionRight:
            directionString = @"right";
            break;
        case UISwipeGestureRecognizerDirectionUp:
            directionString = @"up";
            break;
        case UISwipeGestureRecognizerDirectionDown:
            directionString = @"down";
            break;
        default:
            directionString = @"unknown";
    }
    
    CGPoint screenLocation = [gesture locationInView:self.view.window];
    CGPoint pageLoacation = [gesture locationInView:self.weexInstance.rootView];
    NSDictionary *resultTouch = [self touchResultWithScreenLocation:screenLocation pageLocation:pageLoacation identifier:gesture.wx_identifier];
    [self fireEvent:@"swipe" params:@{@"direction":directionString, @"changedTouches":resultTouch ? @[resultTouch] : @[]}];
}


當(dāng)用戶(hù)輕掃以后,會(huì)觸發(fā)輕掃手勢(shì),于是會(huì)在window上和rootView上會(huì)獲取到2個(gè)坐標(biāo)。


- (NSDictionary *)touchResultWithScreenLocation:(CGPoint)screenLocation pageLocation:(CGPoint)pageLocation identifier:(NSNumber *)identifier
{
    NSMutableDictionary *resultTouch = [[NSMutableDictionary alloc] initWithCapacity:5];
    CGFloat scaleFactor = self.weexInstance.pixelScaleFactor;
    resultTouch[@"screenX"] = @(screenLocation.x/scaleFactor);
    resultTouch[@"screenY"] = @(screenLocation.y/scaleFactor);
    resultTouch[@"pageX"] = @(pageLocation.x/scaleFactor);
    resultTouch[@"pageY"] = @(pageLocation.y/scaleFactor);
    resultTouch[@"identifier"] = identifier;
    
    return resultTouch;
}



screenLocation和pageLocation兩個(gè)坐標(biāo)點(diǎn),還是會(huì)根據(jù)縮放比例還原成相對(duì)于750寬度的頁(yè)面的坐標(biāo)。screenLocation的X值和Y值、pageLocation的X值和Y值分別封裝到resultTouch字典里。




@implementation UIGestureRecognizer (WXGesture)

- (NSNumber *)wx_identifier
{
    NSNumber *identifier = objc_getAssociatedObject(self, _cmd);
    if (!identifier) {
        static NSUInteger _gestureIdentifier;
        identifier = @(_gestureIdentifier++);
        self.wx_identifier = identifier;
    }
    
    return identifier;
}

- (void)setWx_identifier:(NSNumber *)wx_identifier
{
    objc_setAssociatedObject(self, @selector(wx_identifier), wx_identifier, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

最后resultTouch里面還包含一個(gè)identifier的參數(shù),這個(gè)identifier是一個(gè)全局唯一的NSUInteger。wx_identifier被關(guān)聯(lián)到了各個(gè)手勢(shì)識(shí)別器上了。

(三)長(zhǎng)按事件

接著是輕掃事件。



    WX_ADD_EVENT(longpress, addLongPressEvent)


這個(gè)宏和上面點(diǎn)擊事件的展開(kāi)原理一樣,這里不再贅述。

如果addEventName傳進(jìn)來(lái)event的是@“l(fā)ongpress”,那么就是執(zhí)行addLongPressEvent方法。


- (void)addLongPressEvent
{
    if (!_longPressGesture) {
        _longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onLongPress:)];
        _longPressGesture.delegate = self;
        [self.view addGestureRecognizer:_longPressGesture];
    }
}


給當(dāng)前的視圖增加一個(gè)長(zhǎng)按手勢(shì),觸發(fā)的方法是onLongPress:方法。



- (void)onLongPress:(UILongPressGestureRecognizer *)gesture
{
    if (gesture.state == UIGestureRecognizerStateBegan) {
        CGPoint screenLocation = [gesture locationInView:self.view.window];
        CGPoint pageLoacation = [gesture locationInView:self.weexInstance.rootView];
        NSDictionary *resultTouch = [self touchResultWithScreenLocation:screenLocation pageLocation:pageLoacation identifier:gesture.wx_identifier];
        [self fireEvent:@"longpress" params:@{@"changedTouches":resultTouch ? @[resultTouch] : @[]}];
    } else if (gesture.state == UIGestureRecognizerStateEnded) {
        gesture.wx_identifier = nil;
    }
}


長(zhǎng)按手勢(shì)傳給JS的參數(shù)和輕掃的參數(shù)changedTouches幾乎一致。在長(zhǎng)按手勢(shì)開(kāi)始的時(shí)候就傳遞給JS兩個(gè)Point,screenLocation和pageLoacation,以及手勢(shì)的wx_identifier。這部分和輕掃手勢(shì)基本一樣,不多贅述。

(四)拖動(dòng)事件

拖動(dòng)事件在Weex里面包含5個(gè)事件。分別對(duì)應(yīng)著拖動(dòng)的5種狀態(tài):拖動(dòng)開(kāi)始,拖動(dòng)中,拖動(dòng)結(jié)束,水平拖動(dòng),豎直拖動(dòng)。


    WX_ADD_EVENT(panstart, addPanStartEvent)
    WX_ADD_EVENT(panmove, addPanMoveEvent)
    WX_ADD_EVENT(panend, addPanEndEvent)
    WX_ADD_EVENT(horizontalpan, addHorizontalPanEvent)
    WX_ADD_EVENT(verticalpan, addVerticalPanEvent)


為了區(qū)分上面5種狀態(tài),Weex還對(duì)每個(gè)狀態(tài)增加了一個(gè)BOOL變量來(lái)判斷當(dāng)前的狀態(tài)。分別如下:



    BOOL _listenPanStart;
    BOOL _listenPanMove;
    BOOL _listenPanEnd;
    BOOL _listenHorizontalPan;
    BOOL _listenVerticalPan;

通過(guò)宏增加的5個(gè)事件,實(shí)質(zhì)都是執(zhí)行了addPanGesture方法,只不過(guò)每個(gè)狀態(tài)的事件都會(huì)跟對(duì)應(yīng)的BOOL變量。



- (void)addPanStartEvent
{
   // 拖動(dòng)開(kāi)始
    _listenPanStart = YES;
    [self addPanGesture];
}

- (void)addPanMoveEvent
{
   // 拖動(dòng)中
    _listenPanMove = YES;
    [self addPanGesture];
}

- (void)addPanEndEvent
{
   // 拖動(dòng)結(jié)束
    _listenPanEnd = YES;
    [self addPanGesture];
}

- (void)addHorizontalPanEvent
{
   // 水平拖動(dòng)
    _listenHorizontalPan = YES;
    [self addPanGesture];
}

- (void)addVerticalPanEvent
{
   // 豎直拖動(dòng)
    _listenVerticalPan = YES;
    [self addPanGesture];
}


最終都是調(diào)用addPanGesture方法:



- (void)addPanGesture
{
    if (!_panGesture) {
        _panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(onPan:)];
        _panGesture.delegate = self;
        [self.view addGestureRecognizer:_panGesture];
    }
}


給當(dāng)前的視圖增加一個(gè)拖動(dòng)手勢(shì),觸發(fā)的方法是onPan:方法。


- (void)onPan:(UIPanGestureRecognizer *)gesture
{
    CGPoint screenLocation = [gesture locationInView:self.view.window];
    CGPoint pageLoacation = [gesture locationInView:self.weexInstance.rootView];
    NSString *eventName;
    NSString *state = @"";
    NSDictionary *resultTouch = [self touchResultWithScreenLocation:screenLocation pageLocation:pageLoacation identifier:gesture.wx_identifier];
    
    if (gesture.state == UIGestureRecognizerStateBegan) {
        if (_listenPanStart) {
            eventName = @"panstart";
        }
        state = @"start";
    } else if (gesture.state == UIGestureRecognizerStateEnded) {
        if (_listenPanEnd) {
            eventName = @"panend";
        }
        state = @"end";
        gesture.wx_identifier = nil;
    } else if (gesture.state == UIGestureRecognizerStateChanged) {
        if (_listenPanMove) {
             eventName = @"panmove";
        }
        state = @"move";
    }
    
    
    CGPoint translation = [_panGesture translationInView:self.view];
    
    if (_listenHorizontalPan && fabs(translation.y) <= fabs(translation.x)) {
        [self fireEvent:@"horizontalpan" params:@{@"state":state, @"changedTouches":resultTouch ? @[resultTouch] : @[]}];
    }
        
    if (_listenVerticalPan && fabs(translation.y) > fabs(translation.x)) {
        [self fireEvent:@"verticalpan" params:@{@"state":state, @"changedTouches":resultTouch ? @[resultTouch] : @[]}];
    }
        
    if (eventName) {
        [self fireEvent:eventName params:@{@"changedTouches":resultTouch ? @[resultTouch] : @[]}];
    }
}


拖動(dòng)事件最終傳給JS的resultTouch字典和前兩個(gè)手勢(shì)的原理一樣,也是需要傳入兩個(gè)Point,screenLocation和pageLoacation,這里不再贅述。

根據(jù)_listenPanStart,_listenPanEnd,_listenPanMove判斷當(dāng)前的狀態(tài),并生成與之對(duì)應(yīng)的eventName和state字符串。

根據(jù)_panGesture在當(dāng)前視圖上拖動(dòng)形成的有方向的向量,進(jìn)行判斷當(dāng)前拖動(dòng)的方向。

(五)通用觸摸事件

最后就是通用的觸摸事件。

Weex里面對(duì)每個(gè)Component都新建了一個(gè)手勢(shì)識(shí)別器。


@interface WXTouchGestureRecognizer : UIGestureRecognizer

@property (nonatomic, assign) BOOL listenTouchStart;
@property (nonatomic, assign) BOOL listenTouchMove;
@property (nonatomic, assign) BOOL listenTouchEnd;
@property (nonatomic, assign) BOOL listenTouchCancel;
@property (nonatomic, assign) BOOL listenPseudoTouch;
{
    __weak WXComponent *_component;
    NSUInteger _touchIdentifier;
}

- (instancetype)initWithComponent:(WXComponent *)component NS_DESIGNATED_INITIALIZER;

@end

WXTouchGestureRecognizer是繼承自UIGestureRecognizer。里面就5個(gè)BOOL。分別表示5種狀態(tài)。

WXTouchGestureRecognizer會(huì)弱引用當(dāng)前的WXComponent,并且也依舊有touchIdentifier。

Weex通過(guò)以下4個(gè)宏注冊(cè)觸摸事件方法。


    WX_ADD_EVENT(touchstart, addTouchStartEvent)
    WX_ADD_EVENT(touchmove, addTouchMoveEvent)
    WX_ADD_EVENT(touchend, addTouchEndEvent)
    WX_ADD_EVENT(touchcancel, addTouchCancelEvent)

通過(guò)上述宏增加的4個(gè)事件,實(shí)質(zhì)都是改變每個(gè)狀態(tài)的事件都會(huì)跟對(duì)應(yīng)的BOOL變量。


- (void)addTouchStartEvent
{
    self.touchGesture.listenTouchStart = YES;
}

- (void)addTouchMoveEvent
{
    self.touchGesture.listenTouchMove = YES;
}

- (void)addTouchEndEvent
{
    self.touchGesture.listenTouchEnd = YES;
}

- (void)addTouchCancelEvent
{
    self.touchGesture.listenTouchCancel = YES;
}


當(dāng)用戶(hù)開(kāi)始觸摸屏幕,在屏幕上移動(dòng),手指從屏幕上結(jié)束觸摸,取消觸摸,分別都會(huì)觸發(fā)touchesBegan:,touchesMoved:,touchesEnded:,touchesCancelled:方法。


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesBegan:touches withEvent:event];
    
    if (_listenTouchStart) {
        [self fireTouchEvent:@"touchstart" withTouches:touches];
    }
    if(_listenPseudoTouch) {
        NSMutableDictionary *styles = [_component getPseudoClassStyles:@"active"];
        [_component updatePseudoClassStyles:styles];
    }

}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesMoved:touches withEvent:event];
    
    if (_listenTouchMove) {
        [self fireTouchEvent:@"touchmove" withTouches:touches];
    }
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    
    [super touchesEnded:touches withEvent:event];
    
    if (_listenTouchEnd) {
        [self fireTouchEvent:@"touchend" withTouches:touches];
    }
    if(_listenPseudoTouch) {
        [self recoveryPseudoStyles:_component.styles];
    }

}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesCancelled:touches withEvent:event];
    
    if (_listenTouchCancel) {
        [self fireTouchEvent:@"touchcancel" withTouches:touches];
    }
    if(_listenPseudoTouch) {
        [self recoveryPseudoStyles:_component.styles];
    }
}




上述的4個(gè)事件里面實(shí)質(zhì)都是在調(diào)用fireTouchEvent:withTouches:方法:


- (void)fireTouchEvent:(NSString *)eventName withTouches:(NSSet<UITouch *> *)touches
{
    NSMutableArray *resultTouches = [NSMutableArray new];
    
    for (UITouch *touch in touches) {
        CGPoint screenLocation = [touch locationInView:touch.window];
        CGPoint pageLocation = [touch locationInView:_component.weexInstance.rootView];
        if (!touch.wx_identifier) {
            touch.wx_identifier = @(_touchIdentifier++);
        }
        NSDictionary *resultTouch = [_component touchResultWithScreenLocation:screenLocation pageLocation:pageLocation identifier:touch.wx_identifier];
        [resultTouches addObject:resultTouch];
    }
    
    [_component fireEvent:eventName params:@{@"changedTouches":resultTouches ?: @[]}];
}

最終這個(gè)方法和前3個(gè)手勢(shì)一樣,都需要給resultTouches傳入2個(gè)Point和1個(gè)wx_identifier。原理一致。

至于坐標(biāo)如何傳遞給JS見(jiàn)第二章。

2. Appear 事件

如果一個(gè)位于某個(gè)可滾動(dòng)區(qū)域內(nèi)的組件被綁定了 appear 事件,那么當(dāng)這個(gè)組件的狀態(tài)變?yōu)樵谄聊簧峡梢?jiàn)時(shí),該事件將被觸發(fā)。

所以綁定了Appear 事件的都是可以滾動(dòng)的視圖。



    WX_ADD_EVENT(appear, addAppearEvent)

通過(guò)上述的宏給可以滾動(dòng)的視圖增加Appear 事件。也就是當(dāng)前視圖執(zhí)行addAppearEvent方法。


- (void)addAppearEvent
{
    _appearEvent = YES;
    [self.ancestorScroller addScrollToListener:self];
}


在Weex的每個(gè)組件里面都有2個(gè)BOOL記錄著當(dāng)前_appearEvent和_disappearEvent的狀態(tài)。


    BOOL _appearEvent;
    BOOL _disappearEvent;

當(dāng)增加對(duì)應(yīng)的事件的時(shí)候,就會(huì)把對(duì)應(yīng)的BOOL變成YES。


- (id<WXScrollerProtocol>)ancestorScroller
{
    if(!_ancestorScroller) {
        WXComponent *supercomponent = self.supercomponent;
        while (supercomponent) {
            if([supercomponent conformsToProtocol:@protocol(WXScrollerProtocol)]) {
                _ancestorScroller = (id<WXScrollerProtocol>)supercomponent;
                break;
            }
            supercomponent = supercomponent.supercomponent;
        }
    }
    
    return _ancestorScroller;
}

由于Appear 事件和 Disappear 事件都必須要求是滾動(dòng)視圖,所以這里會(huì)遍歷當(dāng)前視圖的supercomponent,直到找到一個(gè)遵循WXScrollerProtocol的supercomponent。



- (void)addScrollToListener:(WXComponent *)target
{
    BOOL has = NO;
    for (WXScrollToTarget *targetData in self.listenerArray) {
        if (targetData.target == target) {
            has = YES;
            break;
        }
    }
    if (!has) {
        WXScrollToTarget *scrollTarget = [[WXScrollToTarget alloc] init];
        scrollTarget.target = target;
        scrollTarget.hasAppear = NO;
        [self.listenerArray addObject:scrollTarget];
    }
}

在滾動(dòng)視圖里面包含有一個(gè)listenerArray,數(shù)組里面裝的都是被監(jiān)聽(tīng)的對(duì)象。添加進(jìn)這個(gè)數(shù)組會(huì)先判斷當(dāng)前是否有相同的WXScrollToTarget,避免重復(fù)添加,如果沒(méi)有重復(fù)的就新建一個(gè)WXScrollToTarget,再添加進(jìn)listenerArray中。



@interface WXScrollToTarget : NSObject
@property (nonatomic, weak)   WXComponent *target;
@property (nonatomic, assign) BOOL hasAppear;
@end

WXScrollToTarget是一個(gè)普通的對(duì)象,里面弱引用了當(dāng)前需要監(jiān)聽(tīng)的WXComponent,以及一個(gè)BOOL變量記錄當(dāng)前是否Appear了。

當(dāng)滾動(dòng)視圖滾動(dòng)的時(shí)候,就會(huì)觸發(fā)scrollViewDidScroll:方法。


- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    //apply block which are registered
    WXSDKInstance *instance = self.weexInstance;
    if ([self.ref isEqualToString:WX_SDK_ROOT_REF] &&
        [self isKindOfClass:[WXScrollerComponent class]]) {
        if (instance.onScroll) {
            instance.onScroll(scrollView.contentOffset);
        }
    }
    
    if (_lastContentOffset.x > scrollView.contentOffset.x) {
        _direction = @"right";
    } else if (_lastContentOffset.x < scrollView.contentOffset.x) {
        _direction = @"left";
    } else if(_lastContentOffset.y > scrollView.contentOffset.y) {
        _direction = @"down";
    } else if(_lastContentOffset.y < scrollView.contentOffset.y) {
        _direction = @"up";
    }
    
    _lastContentOffset = scrollView.contentOffset;
    
    // check sticky
    [self adjustSticky];
    [self handleLoadMore];
    [self handleAppear];
    
    if (self.onScroll) {
        self.onScroll(scrollView);
    }
}


在上面的方法中[self handleAppear]就是觸發(fā)了判斷是否Appear了。


- (void)handleAppear
{
    if (![self isViewLoaded]) {
        return;
    }
    UIScrollView *scrollView = (UIScrollView *)self.view;
    CGFloat vx = scrollView.contentInset.left + scrollView.contentOffset.x;
    CGFloat vy = scrollView.contentInset.top + scrollView.contentOffset.y;
    CGFloat vw = scrollView.frame.size.width - scrollView.contentInset.left - scrollView.contentInset.right;
    CGFloat vh = scrollView.frame.size.height - scrollView.contentInset.top - scrollView.contentInset.bottom;
    CGRect scrollRect = CGRectMake(vx, vy, vw, vh);;
    
    // notify action for appear
    for(WXScrollToTarget *target in self.listenerArray){
        [self scrollToTarget:target scrollRect:scrollRect];
    }
}


上面這個(gè)方法會(huì)把listenerArray數(shù)組里面的每個(gè)WXScrollToTarget對(duì)象都調(diào)用scrollToTarget:scrollRect:方法。根據(jù)當(dāng)前滾動(dòng)的情況傳入一個(gè)CGRect,這個(gè)CGRect就是當(dāng)前滾動(dòng)到那個(gè)矩形區(qū)域的坐標(biāo)信息以及寬和高。


- (void)scrollToTarget:(WXScrollToTarget *)target scrollRect:(CGRect)rect
{
    WXComponent *component = target.target;
    if (![component isViewLoaded]) {
        return;
    }
    
    // 計(jì)算出當(dāng)前的可見(jiàn)區(qū)域的頂部坐標(biāo)
    CGFloat ctop;
    if (component && component->_view && component->_view.superview) {
        ctop = [component->_view.superview convertPoint:component->_view.frame.origin toView:_view].y;
    } else {
        ctop = 0.0;
    }
    // 計(jì)算出當(dāng)前的可見(jiàn)區(qū)域的底部坐標(biāo)
    CGFloat cbottom = ctop + CGRectGetHeight(component.calculatedFrame);
    // 計(jì)算出當(dāng)前的可見(jiàn)區(qū)域的左邊界坐標(biāo)
    CGFloat cleft;
    if (component && component->_view && component->_view.superview) {
        cleft = [component->_view.superview convertPoint:component->_view.frame.origin toView:_view].x;
    } else {
        cleft = 0.0;
    }
    // 計(jì)算出當(dāng)前的可見(jiàn)區(qū)域的右邊界坐標(biāo)
    CGFloat cright = cleft + CGRectGetWidth(component.calculatedFrame);
    
    // 獲取傳入的滾動(dòng)的區(qū)域
    CGFloat vtop = CGRectGetMinY(rect), vbottom = CGRectGetMaxY(rect), vleft = CGRectGetMinX(rect), vright = CGRectGetMaxX(rect);
    
    // 判斷當(dāng)前可見(jiàn)區(qū)域是否包含在傳入的滾動(dòng)區(qū)域內(nèi),如果在,并且監(jiān)聽(tīng)了appear事件,就觸發(fā)appear事件,否則如果監(jiān)聽(tīng)了disappear事件就觸發(fā)disappear事件
    if(cbottom > vtop && ctop <= vbottom && cleft <= vright && cright > vleft){
        if(!target.hasAppear && component){
            target.hasAppear = YES;
            // 如果當(dāng)前監(jiān)聽(tīng)了appear,就觸發(fā)appear事件
            if (component->_appearEvent) {
                [component fireEvent:@"appear" params:_direction ? @{@"direction":_direction} : nil];
            }
        }
    } else {
        if(target.hasAppear && component){
            target.hasAppear = NO;
            // 如果當(dāng)前監(jiān)聽(tīng)了disappear,就觸發(fā)disappear事件
            if(component->_disappearEvent){
                [component fireEvent:@"disappear" params:_direction ? @{@"direction":_direction} : nil];
            }
        }
    }
}


scrollToTarget:scrollRect:方法的核心就是拿當(dāng)前可視區(qū)域和傳入的滾動(dòng)區(qū)域進(jìn)行對(duì)比,如果在該區(qū)域內(nèi),且監(jiān)聽(tīng)了appear事件,就會(huì)觸發(fā)appear事件,如果不在該區(qū)域內(nèi),且監(jiān)聽(tīng)了disappear事件,就會(huì)觸發(fā)disappear事件。

3. Disappear 事件

如果一個(gè)位于某個(gè)可滾動(dòng)區(qū)域內(nèi)的組件被綁定了 disappear 事件,那么當(dāng)這個(gè)組件被滑出屏幕變?yōu)椴豢梢?jiàn)狀態(tài)時(shí),該事件將被觸發(fā)。

同理,綁定了Disappear 事件的都是可以滾動(dòng)的視圖。



    WX_ADD_EVENT(disappear, addDisappearEvent)

通過(guò)上述的宏給可以滾動(dòng)的視圖增加Disappear 事件。也就是當(dāng)前視圖執(zhí)行addDisappearEvent方法。


- (void)addDisappearEvent
{
    _disappearEvent = YES;
    [self.ancestorScroller addScrollToListener:self];
}


接下去的和Appear 事件的原理就一模一樣了。

4. Page 事件

暫時(shí)Weex只支持 iOS 和 Android,H5 暫不支持。

Weex 通過(guò) viewappear 和 viewdisappear 事件提供了簡(jiǎn)單的頁(yè)面狀態(tài)管理能力。

viewappear 事件會(huì)在頁(yè)面就要顯示或配置的任何頁(yè)面動(dòng)畫(huà)被執(zhí)行前觸發(fā),例如,當(dāng)調(diào)用 navigator 模塊的 push 方法時(shí),該事件將會(huì)在打開(kāi)新頁(yè)面時(shí)被觸發(fā)。viewdisappear 事件會(huì)在頁(yè)面就要關(guān)閉時(shí)被觸發(fā)。

與組件Component的 appear 和 disappear 事件不同的是,viewappear 和 viewdisappear 事件關(guān)注的是整個(gè)頁(yè)面的狀態(tài),所以它們必須綁定到頁(yè)面的根元素上。

特殊情況下,這兩個(gè)事件也能被綁定到非根元素的body組件上,例如wxc-navpage組件。

舉個(gè)例子:


- (void)_updateInstanceState:(WXState)state
{
    if (_instance && _instance.state != state) {
        _instance.state = state;
        
        if (state == WeexInstanceAppear) {
            [[WXSDKManager bridgeMgr] fireEvent:_instance.instanceId ref:WX_SDK_ROOT_REF type:@"viewappear" params:nil domChanges:nil];
        } else if (state == WeexInstanceDisappear) {
            [[WXSDKManager bridgeMgr] fireEvent:_instance.instanceId ref:WX_SDK_ROOT_REF type:@"viewdisappear" params:nil domChanges:nil];
        }
    }
}


比如在WXBaseViewController里面,有這樣一個(gè)更新當(dāng)前Instance狀態(tài)的方法,這個(gè)方法里面就會(huì)觸發(fā) viewappear 和 viewdisappear 事件。

其中WX_SDK_ROOT_REF就是_root


#define WX_SDK_ROOT_REF     @"_root"

上述更新?tīng)顟B(tài)的方法同樣出現(xiàn)在WXEmbedComponent組件中。


- (void)_updateState:(WXState)state
{
    if (_renderFinished && _embedInstance && _embedInstance.state != state) {
        _embedInstance.state = state;
        
        if (state == WeexInstanceAppear) {
            [self setNavigationWithStyles:self.embedInstance.naviBarStyles];
            [[WXSDKManager bridgeMgr] fireEvent:self.embedInstance.instanceId ref:WX_SDK_ROOT_REF type:@"viewappear" params:nil domChanges:nil];
        }
        else if (state == WeexInstanceDisappear) {
            [[WXSDKManager bridgeMgr] fireEvent:self.embedInstance.instanceId ref:WX_SDK_ROOT_REF type:@"viewdisappear" params:nil domChanges:nil];
        }
    }
}

二.Weex的事件傳遞

在Weex中,iOS Native把事件傳遞給JS目前只有2種方式,一是Module模塊的callback,二是通過(guò)Component組件自定義的通知事件。

(1)callback

在WXModuleProtocol中定義了2種可以callback給JS的閉包。



/**
 * @abstract the module callback , result can be string or dictionary.
 * @discussion callback data to js, the id of callback function will be removed to save memory.
 */
typedef void (^WXModuleCallback)(id result);

/**
 * @abstract the module callback , result can be string or dictionary.
 * @discussion callback data to js, you can specify the keepAlive parameter to keep callback function id keepalive or not. If the keepAlive is true, it won't be removed until instance destroyed, so you can call it repetitious.
 */
typedef void (^WXModuleKeepAliveCallback)(id result, BOOL keepAlive);

兩個(gè)閉包都可以callback把data傳遞回給JS,data可以是字符串或者字典。

這兩個(gè)閉包的區(qū)別在于:

  1. WXModuleCallback用于Module組件,為了節(jié)約內(nèi)存,該回調(diào)只能回調(diào)通知JS一次,之后會(huì)被釋放,多用于一次結(jié)果。
  2. WXModuleKeepAliveCallback同樣是用于Module組件,但是該回調(diào)可以設(shè)置是否為多次回調(diào)類(lèi)型,如果設(shè)置了keepAlive,那么可以進(jìn)行持續(xù)監(jiān)聽(tīng)變化,多次回調(diào),并返回給 JS。

在Weex中使用WXModuleCallback回調(diào),很多情況是把狀態(tài)回調(diào)給JS,比如成功或者失敗的狀態(tài),還有一些出錯(cuò)的信息回調(diào)給JS。

比如在WXStorageModule中



- (void)setItem:(NSString *)key value:(NSString *)value callback:(WXModuleCallback)callback
{
    if ([self checkInput:key]) {
        callback(@{@"result":@"failed",@"data":@"key must a string or number!"});
        return;
    }
    if ([self checkInput:value]) {
        callback(@{@"result":@"failed",@"data":@"value must a string or number!"});
        return;
    }
    
    if ([key isKindOfClass:[NSNumber class]]) {
        key = [((NSNumber *)key) stringValue];
    }
    
    if ([value isKindOfClass:[NSNumber class]]) {
        value = [((NSNumber *)value) stringValue];
    }
    
    if ([WXUtility isBlankString:key]) {
        callback(@{@"result":@"failed",@"data":@"invalid_param"});
        return ;
    }
    [self setObject:value forKey:key persistent:NO callback:callback];
}

在調(diào)用setItem:value:callback:方法里面,如果setKey-value的時(shí)候失敗了,會(huì)把錯(cuò)誤信息通過(guò)WXModuleCallback回調(diào)給JS。

當(dāng)然,如果調(diào)用存儲(chǔ)模塊WXStorageModule的某些查詢(xún)信息的方法:



- (void)length:(WXModuleCallback)callback
{
    callback(@{@"result":@"success",@"data":@([[WXStorageModule memory] count])});
}

- (void)getAllKeys:(WXModuleCallback)callback
{
    callback(@{@"result":@"success",@"data":[WXStorageModule memory].allKeys});
}

length:和getAllKeys:方法調(diào)用成功,會(huì)把成功的狀態(tài)和數(shù)據(jù)通過(guò)WXModuleCallback回調(diào)給JS。

在Weex中使用了WXModuleKeepAliveCallback的模塊總共只有以下4個(gè):

WXDomModule,WXStreamModule,WXWebSocketModule,WXGlobalEventModule

在WXDomModule模塊中,JS調(diào)用獲取Component組件的位置信息和寬高信息的時(shí)候,需要把這些坐標(biāo)和尺寸信息回調(diào)給JS,不過(guò)這里雖然用到了WXModuleKeepAliveCallback,但是keepAlive是false,并沒(méi)有用到多次回調(diào)的功能。

在WXStreamModule模塊中,由于這是一個(gè)傳輸流的模塊,所以肯定需要用到WXModuleKeepAliveCallback,需要持續(xù)不斷的監(jiān)聽(tīng)數(shù)據(jù)的變化,并把進(jìn)度回調(diào)給JS,這里用到了keepAlive。WXStreamModule模塊中也會(huì)用到WXModuleCallback,WXModuleCallback會(huì)即時(shí)把各個(gè)狀態(tài)回調(diào)給JS。

在WXWebSocketModule模塊中



@interface WXWebSocketModule()

@property(nonatomic,copy)WXModuleKeepAliveCallback errorCallBack;
@property(nonatomic,copy)WXModuleKeepAliveCallback messageCallBack;
@property(nonatomic,copy)WXModuleKeepAliveCallback openCallBack;
@property(nonatomic,copy)WXModuleKeepAliveCallback closeCallBack;

@end

用到了4個(gè)WXModuleKeepAliveCallback回調(diào),這4個(gè)callback分別是把error錯(cuò)誤信息,message收到的數(shù)據(jù),open打開(kāi)鏈接的狀態(tài),close關(guān)閉鏈接的狀態(tài),持續(xù)的回調(diào)給JS。

在WXGlobalEventModule模塊中,有一個(gè)fireGlobalEvent:方法。



- (void)fireGlobalEvent:(NSNotification *)notification
{
    NSDictionary * userInfo = notification.userInfo;
    NSString * userWeexInstanceId = userInfo[@"weexInstance"];

    WXSDKInstance * userWeexInstance = [WXSDKManager instanceForID:userWeexInstanceId];
   // 防止userInstanceId存在,但是instance實(shí)際已經(jīng)被銷(xiāo)毀了
    if (!userWeexInstanceId || userWeexInstance == weexInstance) {
        
        for (WXModuleKeepAliveCallback callback in _eventCallback[notification.name]) {
            callback(userInfo[@"param"], true);
        }
    }
}

開(kāi)發(fā)者可以通過(guò)WXGlobalEventModule進(jìn)行全局的通知,在userInfo里面可以?shī)A帶weexInstance的參數(shù)。native是不需要關(guān)心userWeexInstanceId,這個(gè)參數(shù)是給JS用的。

Native開(kāi)發(fā)者只需要在用到了WXGlobalEventModule的模塊里加上事件的監(jiān)聽(tīng)者,然后發(fā)送全局通知即可。userInfo[@"param"]會(huì)被回調(diào)給JS。

(2)fireEvent:params:domChanges:

在開(kāi)頭我們介紹的Weex事件的4種類(lèi)型,通用事件,Appear 事件,Disappear 事件,Page 事件,全部都是通過(guò)fireEvent:params:domChanges:這種方式,Native觸發(fā)事件之后,Native把參數(shù)傳遞給JS的。

在WXComponent里面定義了2個(gè)可以給JS發(fā)送消息的方法:



/**
 * @abstract Fire an event to the component in Javascript.
 *
 * @param eventName The name of the event to fire
 * @param params The parameters to fire with
 **/
- (void)fireEvent:(NSString *)eventName params:(nullable NSDictionary *)params;

/**
 * @abstract Fire an event to the component and tell Javascript which value has been changed. 
 * Used for two-way data binding.
 *
 * @param eventName The name of the event to fire
 * @param params The parameters to fire with
 * @param domChanges The values has been changed, used for two-way data binding.
 **/
- (void)fireEvent:(NSString *)eventName params:(nullable NSDictionary *)params domChanges:(nullable NSDictionary *)domChanges;


這兩個(gè)方法的區(qū)別就在于最后一個(gè)domChanges的參數(shù),有這個(gè)參數(shù)的方法主要多用于Weex的Native和JS的雙向數(shù)據(jù)綁定。



- (void)fireEvent:(NSString *)eventName params:(NSDictionary *)params
{
    [self fireEvent:eventName params:params domChanges:nil];
}

- (void)fireEvent:(NSString *)eventName params:(NSDictionary *)params domChanges:(NSDictionary *)domChanges
{
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    NSTimeInterval timeSp = [[NSDate date] timeIntervalSince1970] * 1000;
    [dict setObject:@(timeSp) forKey:@"timestamp"];
    if (params) {
        [dict addEntriesFromDictionary:params];
    }
    
    [[WXSDKManager bridgeMgr] fireEvent:self.weexInstance.instanceId ref:self.ref type:eventName params:dict domChanges:domChanges];
}

上述就是兩個(gè)方法的具體實(shí)現(xiàn)。可以看到fireEvent:params:方法就是調(diào)用了fireEvent:params:domChanges:方法,只不過(guò)最后的domChanges參數(shù)傳了nil。

在fireEvent:params:domChanges:方法中會(huì)對(duì)params字典做了一次加工,加上了timestamp的鍵值。最終還是會(huì)調(diào)用WXBridgeManager 里面的fireEvent:ref: type:params:domChanges:方法。

在WXBridgeManager中具體實(shí)現(xiàn)了上述的兩個(gè)方法。



- (void)fireEvent:(NSString *)instanceId ref:(NSString *)ref type:(NSString *)type params:(NSDictionary *)params
{
    [self fireEvent:instanceId ref:ref type:type params:params domChanges:nil];
}

- (void)fireEvent:(NSString *)instanceId ref:(NSString *)ref type:(NSString *)type params:(NSDictionary *)params domChanges:(NSDictionary *)domChanges
{
    if (!type || !ref) {
        WXLogError(@"Event type and component ref should not be nil");
        return;
    }
    
    NSArray *args = @[ref, type, params?:@{}, domChanges?:@{}];
    WXSDKInstance *instance = [WXSDKManager instanceForID:instanceId];
    
    WXCallJSMethod *method = [[WXCallJSMethod alloc] initWithModuleName:nil methodName:@"fireEvent" arguments:args instance:instance];
    [self callJsMethod:method];
}


入?yún)ef, type, params, domChanges封裝到最終的args參數(shù)數(shù)組里面,最后會(huì)封裝出WXCallJSMethod方法,通過(guò)WXBridgeManager的callJsMethod調(diào)用到JS的fireEvent方法。

這里可以舉個(gè)例子:

假設(shè)一個(gè)場(chǎng)景,用戶(hù)點(diǎn)擊了一張圖片,于是就會(huì)改變label上的一段文字。

首先圖片是imageComponent,用戶(hù)點(diǎn)擊會(huì)觸發(fā)該Component的onclick:方法

組件里面會(huì)調(diào)用fireEvent:params:方法:


[self fireEvent:@"click" params:@{@"position":position}];

最終通過(guò)fireEvent:params:domChanges:方法,發(fā)送給JS的參數(shù)字典大概如下:


args:(
    0,
        (
                {
            args =             (
                3,
                click,
                                {
                    position =                     {
                        height = "199.8792270531401";
                        width = "199.8792270531401";
                        x = "274.7584541062802";
                        y = "115.9420289855072";
                    };
                    timestamp = "1489932655404.133";
                },
                                {
                }
            );
            method = fireEvent;
            module = "";
        }
    )
)

JSFramework收到了fireEvent方法調(diào)用以后,處理完,知道label需要更新,于是又會(huì)開(kāi)始call Native,調(diào)用Native的方法。調(diào)用Native的callNative方法,發(fā)過(guò)來(lái)的參數(shù)如下:



(
        {
        args =         (
            4,
                        {
                value = "\U56fe\U7247\U88ab\U70b9\U51fb";
            }
        );
        method = updateAttrs;
        module = dom;
    }
)

最終會(huì)調(diào)用Dom的updateAttrs方法,會(huì)去更新id為4的value,id為4對(duì)應(yīng)的就是label,更新它的值就是刷新label。

接著JSFramework還會(huì)繼續(xù)調(diào)用Native的callNative方法,發(fā)過(guò)來(lái)的參數(shù)如下:



(
        {
        args =         (
        );
        method = updateFinish;
        module = dom;
    }
)


調(diào)用Dom的updateFinish方法,即頁(yè)面刷新完畢。

最后

至此,Weex從View的創(chuàng)建,到渲染,產(chǎn)生事件回調(diào)JSFramework,這一系列的流程源碼都解析完成了。

中間涉及到了3個(gè)子線(xiàn)程,mainThread,com.taobao.weex.component,com.taobao.weex.bridge,分別是UI主線(xiàn)程,DOM線(xiàn)程,JSbridge線(xiàn)程。

Native端目前還差神秘的JSFramework的源碼解析。請(qǐng)大家多多指點(diǎn)。


Weex 源碼解析系列文章:

Weex 是如何在 iOS 客戶(hù)端上跑起來(lái)的
由 FlexBox 算法強(qiáng)力驅(qū)動(dòng)的 Weex 布局引擎
Weex 事件傳遞的那些事兒
Weex 中別具匠心的 JS Framework
iOS 開(kāi)發(fā)者的 Weex 偽最佳實(shí)踐指北


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容